import { docsSource, guidesSource, tutorialsSource, integrationsSource, changelogSource, blogSource, getPageMarkdownUrl, getPageImage, getBlogPageImage, } from '@/lib/source'; import { createSection } from '@/lib/section'; import BlogPostMeta from '@/src/components/BlogPostMeta'; import ClaudeCodeTerminal from '@/src/sections/home/ClaudeCodeTerminal'; import SchemaInjector from '@/src/components/SchemaInjector/SchemaInjector'; import { buildArticleSchema, buildBlogHomeSchema, } from '@/src/utils/schema-helpers'; import { getAuthor, type AuthorId } from '@/lib/authors'; import type { BlogCategoryId } from '@/lib/blog-categories'; type BlogFrontmatter = { title: string; description?: string; authors?: AuthorId[]; date?: Date | string; category?: BlogCategoryId; lastModified?: number | string | Date | null; // Optional per-post cover image (absolute URL). When present it // overrides the site-wide `og:image` fallback set in `app/layout.tsx` // so social previews show the post's hero art instead of the generic // social card. Validated in `blogPageSchema` (source.config.ts). image?: string; }; /** * Pull the publish / modified dates off a blog page as ISO strings. * `date` is author-supplied frontmatter; `lastModified` is injected by * the `fumadocs-mdx/plugins/last-modified` plugin (git-derived). */ function toIso(value: unknown): string | undefined { if (!value) return undefined; if (value instanceof Date) return value.toISOString(); const parsed = new Date(value as string); return Number.isNaN(parsed.getTime()) ? undefined : parsed.toISOString(); } export const docsSection = createSection({ source: docsSource, contentDir: 'content/docs', getMarkdownUrl: (page) => getPageMarkdownUrl(page, docsSource).url, getImageUrl: (page) => getPageImage(page).url, showContributors: true, }); export const guidesSection = createSection({ source: guidesSource, contentDir: 'content/guides', getMarkdownUrl: (page) => getPageMarkdownUrl(page, guidesSource).url, showContributors: true, // Guides-only global MDX components — authors can use these in any // guide without an explicit `import`. Scoped to guides so they don't // leak into docs/tutorials/etc. mdxComponents: { ClaudeCodeTerminal }, }); export const tutorialsSection = createSection({ source: tutorialsSource, contentDir: 'content/tutorials', getMarkdownUrl: (page) => getPageMarkdownUrl(page, tutorialsSource).url, showContributors: true, }); export const integrationsSection = createSection({ source: integrationsSource, contentDir: 'content/integrations', getMarkdownUrl: (page) => getPageMarkdownUrl(page, integrationsSource).url, showContributors: true, }); export const changelogSection = createSection({ source: changelogSource, contentDir: 'content/changelog', getMarkdownUrl: (page) => getPageMarkdownUrl(page, changelogSource).url, }); export const blogSection = createSection({ source: blogSource, contentDir: 'content/blog', getMarkdownUrl: (page) => getPageMarkdownUrl(page, blogSource).url, // Per-post `og:image` fallback: a generated title/description card // served from `/og/blog//image.png`. A post's `image:` // frontmatter still wins — `extendMetadata` sets `openGraph.images` // to it, and `generateMetadata` merges that over this base. Without // this fallback, posts with no `image:` emit no `og:image` (the blog // `openGraph` override clobbers the root layout's default), so social // scrapers like LinkedIn grab the author avatar off the page instead. getImageUrl: (page) => getBlogPageImage(page).url, // Blog-only global MDX components — authors can use these in any post // without an explicit `import`. Scoped to the blog so they don't leak // into docs/guides/etc. mdxComponents: { ClaudeCodeTerminal }, renderBeforeBody: (page) => { const data = page.data as BlogFrontmatter; const { authors, category, title, description, date } = data; // Blog index (`/blog`) — no authors/date; emit a `Blog` JSON-LD // listing all posts instead so Google can surface the post set // directly. Matches what the old Docusaurus blog plugin emitted. if (!authors) { const posts = blogSource .getPages() .filter((p) => { const d = p.data as BlogFrontmatter; return Array.isArray(d.authors) && d.authors.length > 0; }) .map((p) => { const d = p.data as BlogFrontmatter; return { title: d.title, description: d.description ?? '', slug: p.slugs[p.slugs.length - 1] ?? '', authors: (d.authors ?? []).map((id) => getAuthor(id).name), date: toIso(d.date) ?? '', }; }); return ; } // Per-post byline (unchanged) + Article / TechArticle JSON-LD. // `date` is still required in frontmatter for the git-less publish // sort / OG metadata, but we don't display it in the byline row. const authorNames = authors.map((id) => getAuthor(id).name); const articleSchema = buildArticleSchema({ title, description, url: page.url, datePublished: toIso(date), dateModified: toIso(data.lastModified ?? undefined), authors: authorNames, }); return ( <> ); }, // Individual posts get `openGraph.type = 'article'` + publish / // modified timestamps + author list, so social previews render as // proper article cards instead of a generic website card. If the // post sets `image:` in frontmatter we also promote it to // `openGraph.images` / `twitter.images` so the share card shows the // post's hero art instead of the generic site-wide social_card.png. extendMetadata: (page) => { const data = page.data as BlogFrontmatter; if (!data.authors) return {}; const publishedTime = toIso(data.date); const modifiedTime = toIso(data.lastModified ?? undefined); const authorNames = data.authors.map((id) => getAuthor(id).name); const image = data.image; return { openGraph: { type: 'article', ...(publishedTime ? { publishedTime } : {}), ...(modifiedTime ? { modifiedTime } : {}), authors: authorNames, // Per-post hero art overrides the site-wide `/img/social_card.png` // default set in `app/layout.tsx`. We intentionally DO NOT also // override `twitter.images` here: Next.js replaces (doesn't // deep-merge) the `twitter` object across nested `generateMetadata` // calls, so setting it would also wipe the layout's `card`, // `site`, and `creator`. X/Twitter's card renderer falls back // to `og:image` when `twitter:image` is absent, and other // `summary_large_image` consumers (LinkedIn, Slack, Discord) // read `og:image` directly — so the single override covers // every surface. ...(image ? { images: image } : {}), }, }; }, });