187 lines
7.0 KiB
TypeScript
187 lines
7.0 KiB
TypeScript
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/<slug>/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 <SchemaInjector schema={buildBlogHomeSchema(posts)} />;
|
|
}
|
|
|
|
// 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 (
|
|
<>
|
|
<SchemaInjector schema={articleSchema} />
|
|
<BlogPostMeta authors={authors} category={category} />
|
|
</>
|
|
);
|
|
},
|
|
// 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 } : {}),
|
|
},
|
|
};
|
|
},
|
|
});
|