diff --git a/app/blog/[slug]/loading.tsx b/app/blog/[slug]/loading.tsx new file mode 100644 index 0000000..dfdb4c6 --- /dev/null +++ b/app/blog/[slug]/loading.tsx @@ -0,0 +1,41 @@ +export default function BlogPostLoading() { + return ( +
+ {/* Header skeleton */} +
+
+
+
+
+
+
+
+ + {/* Cover image skeleton */} +
+ + {/* Content skeleton */} +
+
+
+
+
+
+ +
+ +
+
+
+
+
+ +
+
+
+
+
+
+
+ ); +} diff --git a/app/blog/[slug]/page.tsx b/app/blog/[slug]/page.tsx index c5e9a90..3c76773 100644 --- a/app/blog/[slug]/page.tsx +++ b/app/blog/[slug]/page.tsx @@ -92,6 +92,8 @@ export default async function BlogPostPage({ params }: Props) { alt={post.title} width={1200} height={600} + sizes="(max-width: 768px) 100vw, (max-width: 1200px) 90vw, 1200px" + priority className="w-full rounded-xl shadow-lg" /> diff --git a/app/error.tsx b/app/error.tsx new file mode 100644 index 0000000..cf9f1db --- /dev/null +++ b/app/error.tsx @@ -0,0 +1,61 @@ +'use client'; + +import { useEffect } from 'react'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { faTriangleExclamation } from '@fortawesome/free-solid-svg-icons'; + +export default function Error({ + error, + reset, +}: { + error: Error & { digest?: string }; + reset: () => void; +}) { + useEffect(() => { + // Log the error to an error reporting service + console.error('Application error:', error); + }, [error]); + + return ( +
+
+
+ +
+ +

+ 發生錯誤 +

+ +

+ {error.message || '頁面載入時發生問題,請稍後再試。'} +

+ +
+ + + + 返回首頁 + +
+ + {error.digest && ( +

+ 錯誤代碼: {error.digest} +

+ )} +
+
+ ); +} diff --git a/app/loading.tsx b/app/loading.tsx new file mode 100644 index 0000000..a10dcaa --- /dev/null +++ b/app/loading.tsx @@ -0,0 +1,12 @@ +export default function Loading() { + return ( +
+
+
+

+ 載入中... +

+
+
+ ); +} diff --git a/app/pages/[slug]/page.tsx b/app/pages/[slug]/page.tsx index bc18f5f..7871d88 100644 --- a/app/pages/[slug]/page.tsx +++ b/app/pages/[slug]/page.tsx @@ -86,6 +86,8 @@ export default async function StaticPage({ params }: Props) { alt={page.title} width={1200} height={600} + sizes="(max-width: 768px) 100vw, (max-width: 1200px) 90vw, 1200px" + priority className="w-full rounded-xl shadow-lg" /> diff --git a/app/template.tsx b/app/template.tsx index a4e1e09..77ba947 100644 --- a/app/template.tsx +++ b/app/template.tsx @@ -1,15 +1,24 @@ 'use client'; -import { motion } from 'framer-motion'; +import { useEffect, useRef } from 'react'; export default function Template({ children }: { children: React.ReactNode }) { - return ( - - {children} - - ); + const containerRef = useRef(null); + + useEffect(() => { + const container = containerRef.current; + if (!container) return; + + // Trigger animation on mount + container.style.animation = 'none'; + // Force reflow + void container.offsetHeight; + container.style.animation = 'pageEnter 0.3s cubic-bezier(0.32, 0.72, 0, 1) forwards'; + }, [children]); + + return ( +
+ {children} +
+ ); } diff --git a/components/post-card.tsx b/components/post-card.tsx index b5c70f0..9d8f084 100644 --- a/components/post-card.tsx +++ b/components/post-card.tsx @@ -26,6 +26,8 @@ export function PostCard({ post, showTags = true }: PostCardProps) { alt={post.title} width={640} height={360} + sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw" + loading="lazy" className="mx-auto max-h-60 w-full object-contain transition-transform duration-300 ease-out group-hover:scale-105" /> diff --git a/components/post-list-item.tsx b/components/post-list-item.tsx index e9be654..3a0c687 100644 --- a/components/post-list-item.tsx +++ b/components/post-list-item.tsx @@ -28,6 +28,8 @@ export function PostListItem({ post }: Props) { alt={post.title} width={320} height={240} + sizes="(max-width: 640px) 96px, 160px" + loading="lazy" className="h-full w-full object-cover transition-transform duration-300 ease-out group-hover:scale-105" /> diff --git a/components/scroll-reveal.tsx b/components/scroll-reveal.tsx index f1bc181..2a221ec 100644 --- a/components/scroll-reveal.tsx +++ b/components/scroll-reveal.tsx @@ -1,6 +1,6 @@ 'use client'; -import { useEffect, useRef, useState } from 'react'; +import { useEffect, useRef } from 'react'; import clsx from 'clsx'; interface ScrollRevealProps { @@ -15,26 +15,25 @@ export function ScrollReveal({ once = true }: ScrollRevealProps) { const ref = useRef(null); - const [visible, setVisible] = useState(false); useEffect(() => { const el = ref.current; if (!el) return; + // Fallback for browsers without IntersectionObserver if (!('IntersectionObserver' in window)) { - setVisible(true); + el.classList.add('is-visible'); return; } - let cancelled = false; const observer = new IntersectionObserver( (entries) => { entries.forEach((entry) => { if (entry.isIntersecting) { - if (!cancelled) setVisible(true); + entry.target.classList.add('is-visible'); if (once) observer.unobserve(entry.target); } else if (!once) { - if (!cancelled) setVisible(false); + entry.target.classList.remove('is-visible'); } }); }, @@ -46,12 +45,12 @@ export function ScrollReveal({ observer.observe(el); + // Fallback timeout for slow connections const fallback = window.setTimeout(() => { - if (!cancelled) setVisible(true); + el.classList.add('is-visible'); }, 500); return () => { - cancelled = true; observer.disconnect(); window.clearTimeout(fallback); }; @@ -60,13 +59,7 @@ export function ScrollReveal({ return (
{children}
diff --git a/components/site-footer.tsx b/components/site-footer.tsx index 27f4bfd..2e97ab8 100644 --- a/components/site-footer.tsx +++ b/components/site-footer.tsx @@ -9,6 +9,9 @@ import { } from '@fortawesome/free-brands-svg-icons'; import { faEnvelope } from '@fortawesome/free-solid-svg-icons'; +// Calculate year at build time for PPR compatibility +const currentYear = new Date().getFullYear(); + export function SiteFooter() { const { social } = siteConfig; @@ -59,7 +62,7 @@ export function SiteFooter() { return (