-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathsource.config.ts
More file actions
135 lines (126 loc) · 4.95 KB
/
Copy pathsource.config.ts
File metadata and controls
135 lines (126 loc) · 4.95 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
import { execFile as execFileCb } from 'node:child_process';
import { promisify } from 'node:util';
import {
defineConfig,
defineDocs,
frontmatterSchema,
} from 'fumadocs-mdx/config';
import lastModified from 'fumadocs-mdx/plugins/last-modified';
import { metaSchema } from 'fumadocs-core/source/schema';
import { remarkMdxMermaid } from 'fumadocs-core/mdx-plugins';
import { z } from 'zod';
const execFile = promisify(execFileCb);
// Inlined from `@tetherto/docs-seo-schema`. The package is published with raw
// TypeScript as its entry (`main: ./src/index.ts`), and Node ≥24 refuses to
// strip types from anything inside `node_modules` — so importing it from this
// file (which fumadocs-mdx loads via raw Node, before Next's webpack /
// `transpilePackages` get a chance) crashes the build with
// `ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING`. Inlining keeps source.config.ts
// pure-zod and decouples this file from the package's distribution shape.
// Keep in sync with the upstream definition.
const jsonLdSchemaTypeSchema = z.enum([
'TechArticle',
'APIReference',
'WebPage',
]);
const docTypeSchema = z.enum([
'tutorial',
'how-to',
'reference',
'explanation',
'page',
'faq',
'getting-started',
]);
const tetherSeoFrontmatterSchema = z.object({
description: z
.string()
.trim()
.min(1, 'description is required for SEO (meta, Open Graph, JSON-LD)'),
noIndex: z.boolean().optional(),
ogImage: z.string().optional(),
schemaType: jsonLdSchemaTypeSchema.optional(),
docType: docTypeSchema.optional(),
lastModified: z.union([z.string(), z.coerce.date()]).optional(),
});
/**
* Resolve the last commit date for an MDX page, with three fallbacks the
* default `git`-mode of `fumadocs-mdx/plugins/last-modified` doesn't handle:
*
* 1. `--follow` so renames (e.g. `.md` → `.mdx`) walk through history.
* 2. If the `.mdx` rename isn't committed yet (or git only kept the `.md`
* blob due to a shallow clone), query the matching `.md` path.
* 3. If neither resolves (e.g. Sevalla / managed CI doing a shallow clone
* where per-file history is truncated), fall back to the HEAD commit's
* timestamp. That's still a meaningful "last deploy" date for the
* sitemap and avoids dropping `<lastmod>` entirely.
*/
async function gitLastModified(filePath: string): Promise<Date | null> {
const tryGit = async (args: string[]): Promise<Date | null> => {
try {
const { stdout } = await execFile('git', args, { cwd: process.cwd() });
const trimmed = stdout.trim();
if (!trimmed) return null;
const d = new Date(trimmed);
return Number.isNaN(d.getTime()) ? null : d;
} catch {
return null;
}
};
const fileLog = (target: string) => [
'log',
'-1',
'--follow',
'--format=%cI',
'--',
target,
];
const direct = await tryGit(fileLog(filePath));
if (direct) return direct;
if (filePath.endsWith('.mdx')) {
const md = await tryGit(fileLog(filePath.slice(0, -1)));
if (md) return md;
}
return tryGit(['log', '-1', '--format=%cI', 'HEAD']);
}
// SEO frontmatter is layered on top of Fumadocs' base schema:
// `description` becomes required, plus optional `noIndex`, `ogImage`,
// `schemaType`, `docType`, `lastModified`. Drives metadata, sitemap, robots,
// JSON-LD, and Takumi OG via @tetherto/docs-seo-*.
// see https://fumadocs.dev/docs/mdx/collections
export const docs = defineDocs({
dir: 'content',
docs: {
// Underscore-prefixed `.mdx` files are partials — meant to be inlined via
// <include>./_partial.mdx</include> (see https://fumadocs.dev/docs/markdown#include)
// rather than rendered as their own page, so we skip them from the collection.
files: ['**/*.{md,mdx}', '!**/_*.{md,mdx}'],
schema: frontmatterSchema
.extend(tetherSeoFrontmatterSchema.shape)
.passthrough(),
postprocess: {
includeProcessedMarkdown: true,
},
},
meta: {
schema: metaSchema,
},
});
export default defineConfig({
// `lastModified` injects `page.data.lastModified` from the latest `git log`
// commit time of each MDX file, which @tetherto/docs-seo-* feeds into the
// sitemap (`<lastmod>`) and JSON-LD (`dateModified` / `datePublished`).
// No per-page frontmatter required.
// On Vercel, set `VERCEL_DEEP_CLONE=true` so git history is available.
plugins: [lastModified({ versionControl: gitLastModified })],
mdxOptions: {
// `remarkImage` rewrites `` to `mdxJsxFlowElement` inside paragraphs.
// That breaks `remark-structure` → mdast-util-to-markdown when building processed
// markdown (postprocess.includeProcessedMarkdown). Use plain markdown images, or
// opt into `<Image />` / `<img />` as block-level JSX where you need Next-optimized images.
remarkImageOptions: false,
// Convert ` ```mermaid ` fenced code blocks into `<Mermaid chart="…" />`
// JSX so they render via the client component in `src/components/mermaid.tsx`.
remarkPlugins: (v) => [remarkMdxMermaid, ...v],
},
});