diff --git a/apps/docs/app/(home)/blog/[slug]/page.tsx b/apps/docs/app/(home)/blog/[slug]/page.tsx index 2bbec26f..3e5564c5 100644 --- a/apps/docs/app/(home)/blog/[slug]/page.tsx +++ b/apps/docs/app/(home)/blog/[slug]/page.tsx @@ -6,6 +6,7 @@ import { getRelatedPosts, postSlug, } from '@/lib/blog'; +import { jsonLdHtml } from '@/lib/json-ld'; import { appName, siteUrl } from '@/lib/shared'; import { DocsBody } from 'fumadocs-ui/layouts/docs/page'; @@ -21,63 +22,96 @@ export default async function BlogPostPage(props: PageProps<'/blog/[slug]'>) { const MDX = post.data.body; const related = getRelatedPosts(post); - return ( -
- - ← Back to blog - -
-
-

- {post.data.title} -

-

- {post.data.author} - · - -

-
- - - - -
+ // Per-post structured data. The root layout emits site-wide WebSite + + // SoftwareApplication nodes; this classifies each post as an Article so a + // crawler can model author, publish date, and headline instead of guessing — + // the content-quality signal that helps Google prioritize a young blog's URLs + // for indexing (and unlocks Article rich results). `image`/`url` must resolve, + // so they mirror the OG route and canonical exactly. + const canonical = `${siteUrl}/blog/${slug}`; + const jsonLd = { + '@context': 'https://schema.org', + '@type': 'BlogPosting', + headline: post.data.title, + ...(post.data.description + ? { description: post.data.description } + : {}), + datePublished: post.data.date, + dateModified: post.data.date, + author: { '@type': 'Person', name: post.data.author }, + publisher: { '@type': 'Organization', name: appName, url: siteUrl }, + image: `${siteUrl}/og/blog/${slug}`, + mainEntityOfPage: { '@type': 'WebPage', '@id': canonical }, + url: canonical, + ...(post.data.tags?.length + ? { keywords: post.data.tags.join(', ') } + : {}), + inLanguage: 'en-US', + }; - {related.length > 0 ? ( -