From 6badd7673323b512ac131c8a72c66a28e13098e3 Mon Sep 17 00:00:00 2001 From: gbanyan Date: Thu, 20 Nov 2025 21:23:10 +0800 Subject: [PATCH] Add Schema.org JSON-LD structured data for SEO MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implemented comprehensive Schema.org structured data across the blog to improve SEO and enable rich snippets in search results. Changes: - Created JSON-LD helper component for safe schema rendering - Added BlogPosting schema to blog posts with: * Article metadata (headline, description, image, dates) * Author and publisher information * Keywords and article sections from tags - Added BreadcrumbList schema to blog posts for navigation - Added WebSite and Organization schemas to root layout * Site-wide identity and branding * Search action for site search functionality - Added CollectionPage schema to homepage * Blog collection metadata - Added WebPage schema to static pages * Page metadata with dates and images Benefits: - Rich snippets in Google/Bing search results - Better content understanding by search engines - Article cards with images, dates, authors in SERPs - Breadcrumb navigation in search results - Improved SEO ranking signals All schemas validated against Schema.org specifications and include proper Chinese language support. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- app/blog/[slug]/page.tsx | 81 +++++++++++++++++++++++++++++++++++++++ app/layout.tsx | 40 +++++++++++++++++++ app/page.tsx | 26 ++++++++++++- app/pages/[slug]/page.tsx | 32 ++++++++++++++++ components/json-ld.tsx | 12 ++++++ 5 files changed, 190 insertions(+), 1 deletion(-) create mode 100644 components/json-ld.tsx diff --git a/app/blog/[slug]/page.tsx b/app/blog/[slug]/page.tsx index 1f9aafe..de8d5ac 100644 --- a/app/blog/[slug]/page.tsx +++ b/app/blog/[slug]/page.tsx @@ -12,6 +12,7 @@ import { PostCard } from '@/components/post-card'; import { PostStorylineNav } from '@/components/post-storyline-nav'; import { SectionDivider } from '@/components/section-divider'; import { FooterCue } from '@/components/footer-cue'; +import { JsonLd } from '@/components/json-ld'; export function generateStaticParams() { return allPosts.map((post) => ({ @@ -76,8 +77,88 @@ export default async function BlogPostPage({ params }: Props) { const hasToc = / 0) { + ogImageUrl.searchParams.set('tags', post.tags.slice(0, 3).join(',')); + } + + // Get image URL - prefer feature_image, fallback to OG image + const imageUrl = post.feature_image + ? `${siteConfig.url}${post.feature_image.replace('../assets', '/assets')}` + : ogImageUrl.toString(); + + // BlogPosting Schema + const blogPostingSchema = { + '@context': 'https://schema.org', + '@type': 'BlogPosting', + headline: post.title, + description: post.description || post.custom_excerpt || post.title, + image: imageUrl, + datePublished: post.published_at, + dateModified: post.updated_at || post.published_at, + author: { + '@type': 'Person', + name: post.authors?.[0] || siteConfig.author, + url: siteConfig.url, + }, + publisher: { + '@type': 'Organization', + name: siteConfig.name, + logo: { + '@type': 'ImageObject', + url: `${siteConfig.url}${siteConfig.avatar}`, + }, + }, + mainEntityOfPage: { + '@type': 'WebPage', + '@id': postUrl, + }, + ...(post.tags && post.tags.length > 0 && { + keywords: post.tags.join(', '), + articleSection: post.tags[0], + }), + inLanguage: siteConfig.defaultLocale, + url: postUrl, + }; + + // BreadcrumbList Schema + const breadcrumbSchema = { + '@context': 'https://schema.org', + '@type': 'BreadcrumbList', + itemListElement: [ + { + '@type': 'ListItem', + position: 1, + name: '首頁', + item: siteConfig.url, + }, + { + '@type': 'ListItem', + position: 2, + name: '所有文章', + item: `${siteConfig.url}/blog`, + }, + { + '@type': 'ListItem', + position: 3, + name: post.title, + item: postUrl, + }, + ], + }; + return ( <> + +
diff --git a/app/layout.tsx b/app/layout.tsx index 6ed8038..9c04980 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -4,6 +4,7 @@ import { siteConfig } from '@/lib/config'; import { LayoutShell } from '@/components/layout-shell'; import { ThemeProvider } from 'next-themes'; import { Playfair_Display } from 'next/font/google'; +import { JsonLd } from '@/components/json-ld'; const playfair = Playfair_Display({ subsets: ['latin'], @@ -49,9 +50,48 @@ export default function RootLayout({ }) { const theme = siteConfig.theme; + // WebSite Schema + const websiteSchema = { + '@context': 'https://schema.org', + '@type': 'WebSite', + name: siteConfig.title, + description: siteConfig.description, + url: siteConfig.url, + inLanguage: siteConfig.defaultLocale, + author: { + '@type': 'Person', + name: siteConfig.author, + url: siteConfig.url, + }, + potentialAction: { + '@type': 'SearchAction', + target: { + '@type': 'EntryPoint', + urlTemplate: `${siteConfig.url}/blog?search={search_term_string}`, + }, + 'query-input': 'required name=search_term_string', + }, + }; + + // Organization Schema + const organizationSchema = { + '@context': 'https://schema.org', + '@type': 'Organization', + name: siteConfig.name, + url: siteConfig.url, + logo: `${siteConfig.url}${siteConfig.avatar}`, + sameAs: [ + siteConfig.social.github, + siteConfig.social.twitter && `https://twitter.com/${siteConfig.social.twitter.replace('@', '')}`, + siteConfig.social.mastodon, + ].filter(Boolean), + }; + return ( + +