@bliztek/mdx-utils 1.0.0 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +318 -2
- package/dist/index-is67XHX5.d.cts +178 -0
- package/dist/index-is67XHX5.d.ts +178 -0
- package/dist/index.d.cts +1 -42
- package/dist/index.d.ts +1 -42
- package/dist/node.cjs +332 -6
- package/dist/node.d.cts +76 -3
- package/dist/node.d.ts +76 -3
- package/dist/node.js +328 -4
- package/package.json +7 -5
package/README.md
CHANGED
|
@@ -88,15 +88,331 @@ Reads a directory and returns slugs (filenames without extension) of matching co
|
|
|
88
88
|
|
|
89
89
|
Reads a file and returns its content as a UTF-8 string.
|
|
90
90
|
|
|
91
|
+
## Collections
|
|
92
|
+
|
|
93
|
+
For content trees with more than a flat directory of posts — game guides, docs, multi-product changelogs — use `createMdxCollection` to get a typed handle that walks the tree, parses frontmatter, and (optionally) validates against a schema.
|
|
94
|
+
|
|
95
|
+
### Flat blog
|
|
96
|
+
|
|
97
|
+
```ts
|
|
98
|
+
import { createMdxCollection } from "@bliztek/mdx-utils/node";
|
|
99
|
+
|
|
100
|
+
const posts = createMdxCollection({ root: "content/blog" });
|
|
101
|
+
|
|
102
|
+
const all = await posts.getAll();
|
|
103
|
+
const post = await posts.get({ slug: "my-first-post" });
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
### Typed frontmatter
|
|
107
|
+
|
|
108
|
+
Pass a type parameter to make every getter return your shape:
|
|
109
|
+
|
|
110
|
+
```ts
|
|
111
|
+
interface PostFrontmatter {
|
|
112
|
+
title: string;
|
|
113
|
+
publishedAt: string;
|
|
114
|
+
description?: string;
|
|
115
|
+
aliases?: string[];
|
|
116
|
+
relatedSlugs?: string[];
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const posts = createMdxCollection<PostFrontmatter>({
|
|
120
|
+
root: "content/blog",
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
const all = await posts.getAll();
|
|
124
|
+
// all[0].metadata is typed as PostFrontmatter
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
If you pair the generic with `frontmatterSchema`, the schema is the runtime enforcer and the generic is the static source of truth. They should agree — the package does not infer one from the other.
|
|
128
|
+
|
|
129
|
+
### Nested namespaces
|
|
130
|
+
|
|
131
|
+
```ts
|
|
132
|
+
// content/games/{game}/{slug}.mdx
|
|
133
|
+
const guides = createMdxCollection({
|
|
134
|
+
root: "content/games",
|
|
135
|
+
namespaceDepth: 1,
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
const arkGuides = await guides.getAllByNamespace("ark");
|
|
139
|
+
const specific = await guides.get({ namespace: "ark", slug: "boss-guide" });
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
Files that sit at the wrong depth throw a clear error — there is no silent coercion.
|
|
143
|
+
|
|
144
|
+
### Schema validation
|
|
145
|
+
|
|
146
|
+
The schema interface is structural: anything with a `parse(input): T` method works. No dependency on Zod, Valibot, or Yup.
|
|
147
|
+
|
|
148
|
+
```ts
|
|
149
|
+
import { z } from "zod";
|
|
150
|
+
|
|
151
|
+
const schema = z.object({
|
|
152
|
+
title: z.string(),
|
|
153
|
+
publishedAt: z.string(),
|
|
154
|
+
aliases: z.array(z.string()).optional(),
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
const posts = createMdxCollection({
|
|
158
|
+
root: "content/blog",
|
|
159
|
+
frontmatterSchema: schema,
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
// In next.config.ts — fail the build if any file drifts:
|
|
163
|
+
await posts.validate();
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
Or hand-rolled, no dependencies at all:
|
|
167
|
+
|
|
168
|
+
```ts
|
|
169
|
+
const schema = {
|
|
170
|
+
parse(input: unknown) {
|
|
171
|
+
const obj = input as Record<string, unknown>;
|
|
172
|
+
if (typeof obj.title !== "string") throw new Error("title required");
|
|
173
|
+
return obj as { title: string };
|
|
174
|
+
},
|
|
175
|
+
};
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
`validate()` aggregates every issue and throws `MdxValidationError` with a structured `.issues` array, so you can render your own output instead of parsing the message string. `getAll()` never throws on schema violations — it returns the raw frontmatter, cast to your declared type. Validation is an explicit build-time gate, not a runtime foot-gun.
|
|
179
|
+
|
|
180
|
+
Catching it in a build script:
|
|
181
|
+
|
|
182
|
+
```ts
|
|
183
|
+
import { MdxValidationError } from "@bliztek/mdx-utils/node";
|
|
184
|
+
|
|
185
|
+
try {
|
|
186
|
+
await posts.validate();
|
|
187
|
+
} catch (err) {
|
|
188
|
+
if (err instanceof MdxValidationError) {
|
|
189
|
+
for (const issue of err.issues) {
|
|
190
|
+
console.error(`${issue.filePath} · ${issue.path}: ${issue.message}`);
|
|
191
|
+
}
|
|
192
|
+
process.exit(1);
|
|
193
|
+
}
|
|
194
|
+
throw err;
|
|
195
|
+
}
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
### Related entries
|
|
199
|
+
|
|
200
|
+
Use `resolveRelated` to turn a slug list on one entry into full entries from the same namespace:
|
|
201
|
+
|
|
202
|
+
```ts
|
|
203
|
+
const post = await posts.get({ slug: "boss-guide" });
|
|
204
|
+
const related = await posts.resolveRelated(
|
|
205
|
+
post?.namespace ?? "",
|
|
206
|
+
post?.metadata.relatedSlugs ?? [],
|
|
207
|
+
);
|
|
208
|
+
// related: MdxEntry[] — unknown slugs skipped with a console.warn
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
### Redirects from renamed slugs
|
|
212
|
+
|
|
213
|
+
```ts
|
|
214
|
+
// next.config.ts
|
|
215
|
+
import { collectRedirects } from "@bliztek/mdx-utils/node";
|
|
216
|
+
|
|
217
|
+
export default {
|
|
218
|
+
async redirects() {
|
|
219
|
+
return collectRedirects({
|
|
220
|
+
root: "content/games",
|
|
221
|
+
namespaceDepth: 1,
|
|
222
|
+
basePath: "/games/{namespace}/guides/{slug}",
|
|
223
|
+
});
|
|
224
|
+
},
|
|
225
|
+
};
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
Any entry with an `aliases: [...]` field in its frontmatter gets one 301 per alias pointing at the current slug. Tokens `{namespace}` and `{slug}` are substituted from the entry's real location.
|
|
229
|
+
|
|
230
|
+
### Composing the default frontmatter parser
|
|
231
|
+
|
|
232
|
+
The built-in parser handles a minimal YAML subset (quoted/unquoted scalars, inline arrays, block arrays). For the long tail, pass a `parseFrontmatter` override — or compose on top of the default:
|
|
233
|
+
|
|
234
|
+
```ts
|
|
235
|
+
import {
|
|
236
|
+
createMdxCollection,
|
|
237
|
+
defaultParseFrontmatter,
|
|
238
|
+
} from "@bliztek/mdx-utils/node";
|
|
239
|
+
|
|
240
|
+
const posts = createMdxCollection({
|
|
241
|
+
root: "content/blog",
|
|
242
|
+
parseFrontmatter: (raw) => {
|
|
243
|
+
const base = defaultParseFrontmatter(raw);
|
|
244
|
+
// coerce numbers, parse dates, etc.
|
|
245
|
+
return base;
|
|
246
|
+
},
|
|
247
|
+
});
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
Or replace it entirely with `gray-matter`:
|
|
251
|
+
|
|
252
|
+
```ts
|
|
253
|
+
import matter from "gray-matter";
|
|
254
|
+
createMdxCollection({
|
|
255
|
+
root: "content/blog",
|
|
256
|
+
parseFrontmatter: (raw) => matter(raw).data,
|
|
257
|
+
});
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
### Caching
|
|
261
|
+
|
|
262
|
+
`createMdxCollection` reads and parses on the first `getAll()` and caches the result in process. Call `collection.invalidate()` to force a re-read — useful for dev-mode HMR. Production builds typically call each getter once so caching is a pure win.
|
|
263
|
+
|
|
264
|
+
## Next.js App Router integration
|
|
265
|
+
|
|
266
|
+
End-to-end wiring for a static blog at `app/blog/[slug]/page.tsx`:
|
|
267
|
+
|
|
268
|
+
```ts
|
|
269
|
+
// lib/posts.ts
|
|
270
|
+
import { createMdxCollection } from "@bliztek/mdx-utils/node";
|
|
271
|
+
|
|
272
|
+
export interface PostFrontmatter {
|
|
273
|
+
title: string;
|
|
274
|
+
description?: string;
|
|
275
|
+
publishedAt: string;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
export const posts = createMdxCollection<PostFrontmatter>({
|
|
279
|
+
root: "content/blog",
|
|
280
|
+
});
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
```tsx
|
|
284
|
+
// app/blog/[slug]/page.tsx
|
|
285
|
+
import { notFound } from "next/navigation";
|
|
286
|
+
import { readMdxFile } from "@bliztek/mdx-utils/node";
|
|
287
|
+
import { posts } from "@/lib/posts";
|
|
288
|
+
|
|
289
|
+
export async function generateStaticParams() {
|
|
290
|
+
const all = await posts.getAll();
|
|
291
|
+
return all.map((p) => ({ slug: p.slug }));
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
export async function generateMetadata({
|
|
295
|
+
params,
|
|
296
|
+
}: {
|
|
297
|
+
params: Promise<{ slug: string }>;
|
|
298
|
+
}) {
|
|
299
|
+
const { slug } = await params;
|
|
300
|
+
const post = await posts.get({ slug });
|
|
301
|
+
if (!post) return {};
|
|
302
|
+
return {
|
|
303
|
+
title: post.metadata.title,
|
|
304
|
+
description: post.metadata.description,
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
export default async function Page({
|
|
309
|
+
params,
|
|
310
|
+
}: {
|
|
311
|
+
params: Promise<{ slug: string }>;
|
|
312
|
+
}) {
|
|
313
|
+
const { slug } = await params;
|
|
314
|
+
const post = await posts.get({ slug });
|
|
315
|
+
if (!post) notFound();
|
|
316
|
+
|
|
317
|
+
const raw = await readMdxFile(post.filePath);
|
|
318
|
+
// render `raw` with your MDX compiler of choice — see below
|
|
319
|
+
return (
|
|
320
|
+
<article>
|
|
321
|
+
<h1>{post.metadata.title}</h1>
|
|
322
|
+
<p>{post.readTime.minutes} min read</p>
|
|
323
|
+
{/* <MDXRemote source={raw} /> */}
|
|
324
|
+
</article>
|
|
325
|
+
);
|
|
326
|
+
}
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
### Rendering the MDX
|
|
330
|
+
|
|
331
|
+
This package deliberately does not compile MDX — that is the job of an MDX compiler, and you should pick the one that fits your runtime. Hand the `readMdxFile` output to:
|
|
332
|
+
|
|
333
|
+
- **[next-mdx-remote](https://github.com/hashicorp/next-mdx-remote)** — server-component-friendly, works inside App Router
|
|
334
|
+
- **[@mdx-js/mdx](https://mdxjs.com/packages/mdx/)** — lower-level, use when you want full control over the compile pipeline
|
|
335
|
+
- **[@next/mdx](https://nextjs.org/docs/app/building-your-application/configuring/mdx)** — if you want file-based MDX routing instead of a collection, though this conflicts with `createMdxCollection`'s indexing model
|
|
336
|
+
|
|
337
|
+
```tsx
|
|
338
|
+
import { MDXRemote } from "next-mdx-remote/rsc";
|
|
339
|
+
|
|
340
|
+
const raw = await readMdxFile(post.filePath);
|
|
341
|
+
return <MDXRemote source={raw} />;
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
### Sitemap
|
|
345
|
+
|
|
346
|
+
```ts
|
|
347
|
+
// app/sitemap.ts
|
|
348
|
+
import type { MetadataRoute } from "next";
|
|
349
|
+
import { posts } from "@/lib/posts";
|
|
350
|
+
|
|
351
|
+
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
|
|
352
|
+
const all = await posts.getAll();
|
|
353
|
+
return all.map((post) => ({
|
|
354
|
+
url: `https://example.com/blog/${post.slug}`,
|
|
355
|
+
lastModified: post.metadata.publishedAt,
|
|
356
|
+
}));
|
|
357
|
+
}
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
### API
|
|
361
|
+
|
|
362
|
+
#### `createMdxCollection(options)`
|
|
363
|
+
|
|
364
|
+
| Option | Type | Default | Description |
|
|
365
|
+
|---|---|---|---|
|
|
366
|
+
| `root` | `string` | required | Path to the content root |
|
|
367
|
+
| `namespaceDepth` | `number` | `0` | Directory segments between root and file to treat as namespace |
|
|
368
|
+
| `frontmatterSchema` | `{ parse(input): T }` | — | Optional structural validator |
|
|
369
|
+
| `parseFrontmatter` | `(raw) => object` | `defaultParseFrontmatter` | Frontmatter parser override |
|
|
370
|
+
| `wordsPerMinute` | `number` | `238` | Override for read-time calculation |
|
|
371
|
+
|
|
372
|
+
Returns `{ getAll, getAllByNamespace, get, resolveRelated, validate, invalidate }`.
|
|
373
|
+
|
|
374
|
+
#### `collectRedirects(options)`
|
|
375
|
+
|
|
376
|
+
| Option | Type | Default | Description |
|
|
377
|
+
|---|---|---|---|
|
|
378
|
+
| `root` | `string` | required | Path to the content root |
|
|
379
|
+
| `namespaceDepth` | `number` | `0` | Same meaning as above |
|
|
380
|
+
| `basePath` | `string` | required | URL template with `{namespace}`/`{slug}` tokens |
|
|
381
|
+
| `permanent` | `boolean` | `true` | `true` → 301, `false` → 302 |
|
|
382
|
+
| `aliasField` | `string` | `"aliases"` | Frontmatter field to read aliases from |
|
|
383
|
+
|
|
384
|
+
Returns `NextRedirect[]` — compatible with Next.js `redirects()` config.
|
|
385
|
+
|
|
386
|
+
### Well-known frontmatter fields
|
|
387
|
+
|
|
388
|
+
The package treats these fields as well-known when present, but never requires them unless your schema does:
|
|
389
|
+
|
|
390
|
+
| Field | Type | Purpose |
|
|
391
|
+
|---|---|---|
|
|
392
|
+
| `title` | `string` | Display title |
|
|
393
|
+
| `description` | `string` | Excerpt / meta description |
|
|
394
|
+
| `publishedAt` | `string` (ISO) | Used for sort order |
|
|
395
|
+
| `updatedAt` | `string` (ISO) | For JSON-LD `dateModified` |
|
|
396
|
+
| `aliases` | `string[]` | Consumed by `collectRedirects` |
|
|
397
|
+
| `relatedSlugs` | `string[]` | Consumed by `resolveRelated` |
|
|
398
|
+
|
|
399
|
+
All other fields are passed through untouched.
|
|
400
|
+
|
|
91
401
|
## Types
|
|
92
402
|
|
|
93
403
|
```ts
|
|
94
|
-
type TableOfContentsEntry = {
|
|
404
|
+
type TableOfContentsEntry = {
|
|
405
|
+
level: number;
|
|
406
|
+
text: string;
|
|
407
|
+
slug: string;
|
|
408
|
+
children?: TableOfContentsEntry[];
|
|
409
|
+
};
|
|
95
410
|
type TableOfContents = TableOfContentsEntry[];
|
|
96
411
|
|
|
97
412
|
interface ReadTimeOptions { wordsPerMinute?: number }
|
|
98
413
|
interface ReadTimeResult { minutes: number; words: number }
|
|
99
414
|
interface GetContentSlugsOptions { extensions?: string[]; recursive?: boolean }
|
|
415
|
+
interface FrontmatterSchema<T> { parse(input: unknown): T }
|
|
100
416
|
```
|
|
101
417
|
|
|
102
418
|
## Entry Points
|
|
@@ -104,7 +420,7 @@ interface GetContentSlugsOptions { extensions?: string[]; recursive?: boolean }
|
|
|
104
420
|
| Import | Environment | Contents |
|
|
105
421
|
|--------|------------|----------|
|
|
106
422
|
| `@bliztek/mdx-utils` | Any (browser, edge, Node) | `calculateReadTime`, `stripMdxSyntax`, `sortByDateDescending`, types |
|
|
107
|
-
| `@bliztek/mdx-utils/node` | Node.js | `getContentSlugs`, `readMdxFile` + re-exports from main |
|
|
423
|
+
| `@bliztek/mdx-utils/node` | Node.js | `createMdxCollection`, `collectRedirects`, `defaultParseFrontmatter`, `MdxValidationError`, `getContentSlugs`, `readMdxFile` + re-exports from main |
|
|
108
424
|
|
|
109
425
|
## License
|
|
110
426
|
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
type TableOfContentsEntry = {
|
|
2
|
+
/** Heading level: 2 for h2, 3 for h3, etc. */
|
|
3
|
+
level: number;
|
|
4
|
+
/** Rendered heading text with inline markup stripped. */
|
|
5
|
+
text: string;
|
|
6
|
+
/** URL-safe anchor id. */
|
|
7
|
+
slug: string;
|
|
8
|
+
/** Nested subheadings. */
|
|
9
|
+
children?: TableOfContentsEntry[];
|
|
10
|
+
};
|
|
11
|
+
type TableOfContents = TableOfContentsEntry[];
|
|
12
|
+
interface ReadTimeOptions {
|
|
13
|
+
/** Words per minute. Defaults to 238. */
|
|
14
|
+
wordsPerMinute?: number;
|
|
15
|
+
}
|
|
16
|
+
interface ReadTimeResult {
|
|
17
|
+
/** Estimated read time in minutes (minimum 1). */
|
|
18
|
+
minutes: number;
|
|
19
|
+
/** Total word count after stripping MDX/JSX syntax. */
|
|
20
|
+
words: number;
|
|
21
|
+
}
|
|
22
|
+
interface GetContentSlugsOptions {
|
|
23
|
+
/** File extensions to include. Defaults to `[".mdx"]`. */
|
|
24
|
+
extensions?: string[];
|
|
25
|
+
/** Recursively search subdirectories. Defaults to `false`. */
|
|
26
|
+
recursive?: boolean;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Structural schema interface. Any object with a `parse` method that
|
|
30
|
+
* returns the validated value (or throws) satisfies it — works for Zod,
|
|
31
|
+
* Valibot, Yup, and hand-rolled validators. No dependency added.
|
|
32
|
+
*/
|
|
33
|
+
interface FrontmatterSchema<T> {
|
|
34
|
+
parse(input: unknown): T;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* A parsed MDX entry with its metadata and derived fields.
|
|
38
|
+
*/
|
|
39
|
+
interface MdxEntry<TFrontmatter> {
|
|
40
|
+
/** The namespace path segment(s), or "" when `namespaceDepth` is 0. */
|
|
41
|
+
namespace: string;
|
|
42
|
+
/** Filename without the `.mdx` extension. */
|
|
43
|
+
slug: string;
|
|
44
|
+
/** Parsed frontmatter, typed if a schema was provided. */
|
|
45
|
+
metadata: TFrontmatter;
|
|
46
|
+
/** Estimated read time in minutes. */
|
|
47
|
+
readTime: number;
|
|
48
|
+
/**
|
|
49
|
+
* Table of contents extracted from headings.
|
|
50
|
+
*
|
|
51
|
+
* Reserved for a future release — always `undefined` in 1.1.
|
|
52
|
+
* Consumers should treat it as optional and fall back to a
|
|
53
|
+
* client-side walker until the server-side implementation lands.
|
|
54
|
+
*/
|
|
55
|
+
tableOfContents?: TableOfContents;
|
|
56
|
+
/** Absolute filesystem path. */
|
|
57
|
+
filePath: string;
|
|
58
|
+
}
|
|
59
|
+
interface CreateMdxCollectionOptions<TFrontmatter> {
|
|
60
|
+
/**
|
|
61
|
+
* Filesystem path to the content root. Resolved against `process.cwd()`
|
|
62
|
+
* if relative.
|
|
63
|
+
*/
|
|
64
|
+
root: string;
|
|
65
|
+
/**
|
|
66
|
+
* How many directory segments between `root` and each `.mdx` file are
|
|
67
|
+
* treated as the namespace.
|
|
68
|
+
*
|
|
69
|
+
* - 0 → flat directory (`content/blog/{slug}.mdx`)
|
|
70
|
+
* - 1 → one nested dir (`content/games/{namespace}/{slug}.mdx`)
|
|
71
|
+
* - 2 → two nested dirs (`content/docs/{section}/{subsection}/{slug}.mdx`)
|
|
72
|
+
*
|
|
73
|
+
* Files whose depth doesn't match throw a walker error — there is no
|
|
74
|
+
* silent coercion.
|
|
75
|
+
*
|
|
76
|
+
* Defaults to `0`.
|
|
77
|
+
*/
|
|
78
|
+
namespaceDepth?: number;
|
|
79
|
+
/**
|
|
80
|
+
* Optional schema to validate frontmatter against. Accepts anything with
|
|
81
|
+
* a `parse(input: unknown)` method, so Zod/Valibot/Yup/hand-rolled all
|
|
82
|
+
* work without adding a dependency.
|
|
83
|
+
*
|
|
84
|
+
* Note: `getAll()` does NOT throw on schema violations — it casts and
|
|
85
|
+
* returns the raw frontmatter. Call `validate()` explicitly (typically
|
|
86
|
+
* from `next.config`) to fail the build on drift.
|
|
87
|
+
*/
|
|
88
|
+
frontmatterSchema?: FrontmatterSchema<TFrontmatter>;
|
|
89
|
+
/**
|
|
90
|
+
* Override the frontmatter parser. Receives the full file contents,
|
|
91
|
+
* returns a plain object. Default is `defaultParseFrontmatter`.
|
|
92
|
+
*/
|
|
93
|
+
parseFrontmatter?: (raw: string) => Record<string, unknown>;
|
|
94
|
+
/** Words-per-minute override for read-time calculation. Defaults to 238. */
|
|
95
|
+
wordsPerMinute?: number;
|
|
96
|
+
}
|
|
97
|
+
interface MdxCollectionGetArgs {
|
|
98
|
+
/** Required when `namespaceDepth > 0`. Omit for depth-0 collections. */
|
|
99
|
+
namespace?: string;
|
|
100
|
+
slug: string;
|
|
101
|
+
}
|
|
102
|
+
interface MdxCollection<TFrontmatter> {
|
|
103
|
+
/**
|
|
104
|
+
* All entries in the collection. Entries with a parseable `publishedAt`
|
|
105
|
+
* are sorted descending first; the rest are appended in walk order.
|
|
106
|
+
*/
|
|
107
|
+
getAll(): Promise<MdxEntry<TFrontmatter>[]>;
|
|
108
|
+
/** Entries filtered to a single namespace. */
|
|
109
|
+
getAllByNamespace(namespace: string): Promise<MdxEntry<TFrontmatter>[]>;
|
|
110
|
+
/**
|
|
111
|
+
* Fetch a single entry by namespace + slug (or just slug for depth-0
|
|
112
|
+
* collections). Returns `null` if not found.
|
|
113
|
+
*/
|
|
114
|
+
get(args: MdxCollectionGetArgs): Promise<MdxEntry<TFrontmatter> | null>;
|
|
115
|
+
/**
|
|
116
|
+
* Resolve a list of related slugs within a namespace to full entries.
|
|
117
|
+
* Unknown slugs are skipped with a `console.warn`.
|
|
118
|
+
*
|
|
119
|
+
* Note: cross-namespace related content is a foreseeable future need
|
|
120
|
+
* and will require a signature change. Flagged intentionally.
|
|
121
|
+
*/
|
|
122
|
+
resolveRelated(namespace: string, slugs: string[]): Promise<MdxEntry<TFrontmatter>[]>;
|
|
123
|
+
/**
|
|
124
|
+
* Validate every file against `frontmatterSchema` and throw an
|
|
125
|
+
* aggregated `MdxValidationError` if any fail. No-op when no schema
|
|
126
|
+
* is configured.
|
|
127
|
+
*/
|
|
128
|
+
validate(): Promise<void>;
|
|
129
|
+
/** Drop the in-process cache so the next call re-reads the filesystem. */
|
|
130
|
+
invalidate(): void;
|
|
131
|
+
}
|
|
132
|
+
interface MdxValidationIssue {
|
|
133
|
+
/** Absolute path of the offending file. */
|
|
134
|
+
filePath: string;
|
|
135
|
+
/** Dotted path into the frontmatter object where validation failed. */
|
|
136
|
+
path: string;
|
|
137
|
+
/** Human-readable explanation. */
|
|
138
|
+
message: string;
|
|
139
|
+
}
|
|
140
|
+
interface CollectRedirectsOptions {
|
|
141
|
+
root: string;
|
|
142
|
+
namespaceDepth?: number;
|
|
143
|
+
/**
|
|
144
|
+
* URL template. Supported tokens: `{namespace}`, `{slug}`.
|
|
145
|
+
* Example: `"/games/{namespace}/guides/{slug}"`
|
|
146
|
+
*/
|
|
147
|
+
basePath: string;
|
|
148
|
+
/** Defaults to `true` (301). Set `false` for 302. */
|
|
149
|
+
permanent?: boolean;
|
|
150
|
+
/** Frontmatter field to read aliases from. Defaults to `"aliases"`. */
|
|
151
|
+
aliasField?: string;
|
|
152
|
+
}
|
|
153
|
+
interface NextRedirect {
|
|
154
|
+
source: string;
|
|
155
|
+
destination: string;
|
|
156
|
+
permanent: boolean;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Strip MDX/markdown syntax from content, returning plain text.
|
|
161
|
+
* Removes export blocks, import statements, JSX tags, and markdown
|
|
162
|
+
* formatting characters. Useful for generating excerpts, search
|
|
163
|
+
* indexes, or OpenGraph descriptions from raw MDX.
|
|
164
|
+
*/
|
|
165
|
+
declare function stripMdxSyntax(content: string): string;
|
|
166
|
+
/**
|
|
167
|
+
* Calculate estimated read time for MDX/markdown content.
|
|
168
|
+
* Strips export blocks, import statements, JSX tags, and markdown syntax
|
|
169
|
+
* before counting words.
|
|
170
|
+
*/
|
|
171
|
+
declare function calculateReadTime(content: string, options?: ReadTimeOptions): ReadTimeResult;
|
|
172
|
+
/**
|
|
173
|
+
* Sort items by date in descending order (newest first).
|
|
174
|
+
* Returns a new array without mutating the original.
|
|
175
|
+
*/
|
|
176
|
+
declare function sortByDateDescending<T>(items: T[], getDate: (item: T) => string): T[];
|
|
177
|
+
|
|
178
|
+
export { type CreateMdxCollectionOptions as C, type FrontmatterSchema as F, type GetContentSlugsOptions as G, type MdxCollection as M, type NextRedirect as N, type ReadTimeOptions as R, type TableOfContents as T, type CollectRedirectsOptions as a, type MdxValidationIssue as b, type MdxCollectionGetArgs as c, type MdxEntry as d, type ReadTimeResult as e, type TableOfContentsEntry as f, calculateReadTime as g, stripMdxSyntax as h, sortByDateDescending as s };
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
type TableOfContentsEntry = {
|
|
2
|
+
/** Heading level: 2 for h2, 3 for h3, etc. */
|
|
3
|
+
level: number;
|
|
4
|
+
/** Rendered heading text with inline markup stripped. */
|
|
5
|
+
text: string;
|
|
6
|
+
/** URL-safe anchor id. */
|
|
7
|
+
slug: string;
|
|
8
|
+
/** Nested subheadings. */
|
|
9
|
+
children?: TableOfContentsEntry[];
|
|
10
|
+
};
|
|
11
|
+
type TableOfContents = TableOfContentsEntry[];
|
|
12
|
+
interface ReadTimeOptions {
|
|
13
|
+
/** Words per minute. Defaults to 238. */
|
|
14
|
+
wordsPerMinute?: number;
|
|
15
|
+
}
|
|
16
|
+
interface ReadTimeResult {
|
|
17
|
+
/** Estimated read time in minutes (minimum 1). */
|
|
18
|
+
minutes: number;
|
|
19
|
+
/** Total word count after stripping MDX/JSX syntax. */
|
|
20
|
+
words: number;
|
|
21
|
+
}
|
|
22
|
+
interface GetContentSlugsOptions {
|
|
23
|
+
/** File extensions to include. Defaults to `[".mdx"]`. */
|
|
24
|
+
extensions?: string[];
|
|
25
|
+
/** Recursively search subdirectories. Defaults to `false`. */
|
|
26
|
+
recursive?: boolean;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Structural schema interface. Any object with a `parse` method that
|
|
30
|
+
* returns the validated value (or throws) satisfies it — works for Zod,
|
|
31
|
+
* Valibot, Yup, and hand-rolled validators. No dependency added.
|
|
32
|
+
*/
|
|
33
|
+
interface FrontmatterSchema<T> {
|
|
34
|
+
parse(input: unknown): T;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* A parsed MDX entry with its metadata and derived fields.
|
|
38
|
+
*/
|
|
39
|
+
interface MdxEntry<TFrontmatter> {
|
|
40
|
+
/** The namespace path segment(s), or "" when `namespaceDepth` is 0. */
|
|
41
|
+
namespace: string;
|
|
42
|
+
/** Filename without the `.mdx` extension. */
|
|
43
|
+
slug: string;
|
|
44
|
+
/** Parsed frontmatter, typed if a schema was provided. */
|
|
45
|
+
metadata: TFrontmatter;
|
|
46
|
+
/** Estimated read time in minutes. */
|
|
47
|
+
readTime: number;
|
|
48
|
+
/**
|
|
49
|
+
* Table of contents extracted from headings.
|
|
50
|
+
*
|
|
51
|
+
* Reserved for a future release — always `undefined` in 1.1.
|
|
52
|
+
* Consumers should treat it as optional and fall back to a
|
|
53
|
+
* client-side walker until the server-side implementation lands.
|
|
54
|
+
*/
|
|
55
|
+
tableOfContents?: TableOfContents;
|
|
56
|
+
/** Absolute filesystem path. */
|
|
57
|
+
filePath: string;
|
|
58
|
+
}
|
|
59
|
+
interface CreateMdxCollectionOptions<TFrontmatter> {
|
|
60
|
+
/**
|
|
61
|
+
* Filesystem path to the content root. Resolved against `process.cwd()`
|
|
62
|
+
* if relative.
|
|
63
|
+
*/
|
|
64
|
+
root: string;
|
|
65
|
+
/**
|
|
66
|
+
* How many directory segments between `root` and each `.mdx` file are
|
|
67
|
+
* treated as the namespace.
|
|
68
|
+
*
|
|
69
|
+
* - 0 → flat directory (`content/blog/{slug}.mdx`)
|
|
70
|
+
* - 1 → one nested dir (`content/games/{namespace}/{slug}.mdx`)
|
|
71
|
+
* - 2 → two nested dirs (`content/docs/{section}/{subsection}/{slug}.mdx`)
|
|
72
|
+
*
|
|
73
|
+
* Files whose depth doesn't match throw a walker error — there is no
|
|
74
|
+
* silent coercion.
|
|
75
|
+
*
|
|
76
|
+
* Defaults to `0`.
|
|
77
|
+
*/
|
|
78
|
+
namespaceDepth?: number;
|
|
79
|
+
/**
|
|
80
|
+
* Optional schema to validate frontmatter against. Accepts anything with
|
|
81
|
+
* a `parse(input: unknown)` method, so Zod/Valibot/Yup/hand-rolled all
|
|
82
|
+
* work without adding a dependency.
|
|
83
|
+
*
|
|
84
|
+
* Note: `getAll()` does NOT throw on schema violations — it casts and
|
|
85
|
+
* returns the raw frontmatter. Call `validate()` explicitly (typically
|
|
86
|
+
* from `next.config`) to fail the build on drift.
|
|
87
|
+
*/
|
|
88
|
+
frontmatterSchema?: FrontmatterSchema<TFrontmatter>;
|
|
89
|
+
/**
|
|
90
|
+
* Override the frontmatter parser. Receives the full file contents,
|
|
91
|
+
* returns a plain object. Default is `defaultParseFrontmatter`.
|
|
92
|
+
*/
|
|
93
|
+
parseFrontmatter?: (raw: string) => Record<string, unknown>;
|
|
94
|
+
/** Words-per-minute override for read-time calculation. Defaults to 238. */
|
|
95
|
+
wordsPerMinute?: number;
|
|
96
|
+
}
|
|
97
|
+
interface MdxCollectionGetArgs {
|
|
98
|
+
/** Required when `namespaceDepth > 0`. Omit for depth-0 collections. */
|
|
99
|
+
namespace?: string;
|
|
100
|
+
slug: string;
|
|
101
|
+
}
|
|
102
|
+
interface MdxCollection<TFrontmatter> {
|
|
103
|
+
/**
|
|
104
|
+
* All entries in the collection. Entries with a parseable `publishedAt`
|
|
105
|
+
* are sorted descending first; the rest are appended in walk order.
|
|
106
|
+
*/
|
|
107
|
+
getAll(): Promise<MdxEntry<TFrontmatter>[]>;
|
|
108
|
+
/** Entries filtered to a single namespace. */
|
|
109
|
+
getAllByNamespace(namespace: string): Promise<MdxEntry<TFrontmatter>[]>;
|
|
110
|
+
/**
|
|
111
|
+
* Fetch a single entry by namespace + slug (or just slug for depth-0
|
|
112
|
+
* collections). Returns `null` if not found.
|
|
113
|
+
*/
|
|
114
|
+
get(args: MdxCollectionGetArgs): Promise<MdxEntry<TFrontmatter> | null>;
|
|
115
|
+
/**
|
|
116
|
+
* Resolve a list of related slugs within a namespace to full entries.
|
|
117
|
+
* Unknown slugs are skipped with a `console.warn`.
|
|
118
|
+
*
|
|
119
|
+
* Note: cross-namespace related content is a foreseeable future need
|
|
120
|
+
* and will require a signature change. Flagged intentionally.
|
|
121
|
+
*/
|
|
122
|
+
resolveRelated(namespace: string, slugs: string[]): Promise<MdxEntry<TFrontmatter>[]>;
|
|
123
|
+
/**
|
|
124
|
+
* Validate every file against `frontmatterSchema` and throw an
|
|
125
|
+
* aggregated `MdxValidationError` if any fail. No-op when no schema
|
|
126
|
+
* is configured.
|
|
127
|
+
*/
|
|
128
|
+
validate(): Promise<void>;
|
|
129
|
+
/** Drop the in-process cache so the next call re-reads the filesystem. */
|
|
130
|
+
invalidate(): void;
|
|
131
|
+
}
|
|
132
|
+
interface MdxValidationIssue {
|
|
133
|
+
/** Absolute path of the offending file. */
|
|
134
|
+
filePath: string;
|
|
135
|
+
/** Dotted path into the frontmatter object where validation failed. */
|
|
136
|
+
path: string;
|
|
137
|
+
/** Human-readable explanation. */
|
|
138
|
+
message: string;
|
|
139
|
+
}
|
|
140
|
+
interface CollectRedirectsOptions {
|
|
141
|
+
root: string;
|
|
142
|
+
namespaceDepth?: number;
|
|
143
|
+
/**
|
|
144
|
+
* URL template. Supported tokens: `{namespace}`, `{slug}`.
|
|
145
|
+
* Example: `"/games/{namespace}/guides/{slug}"`
|
|
146
|
+
*/
|
|
147
|
+
basePath: string;
|
|
148
|
+
/** Defaults to `true` (301). Set `false` for 302. */
|
|
149
|
+
permanent?: boolean;
|
|
150
|
+
/** Frontmatter field to read aliases from. Defaults to `"aliases"`. */
|
|
151
|
+
aliasField?: string;
|
|
152
|
+
}
|
|
153
|
+
interface NextRedirect {
|
|
154
|
+
source: string;
|
|
155
|
+
destination: string;
|
|
156
|
+
permanent: boolean;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Strip MDX/markdown syntax from content, returning plain text.
|
|
161
|
+
* Removes export blocks, import statements, JSX tags, and markdown
|
|
162
|
+
* formatting characters. Useful for generating excerpts, search
|
|
163
|
+
* indexes, or OpenGraph descriptions from raw MDX.
|
|
164
|
+
*/
|
|
165
|
+
declare function stripMdxSyntax(content: string): string;
|
|
166
|
+
/**
|
|
167
|
+
* Calculate estimated read time for MDX/markdown content.
|
|
168
|
+
* Strips export blocks, import statements, JSX tags, and markdown syntax
|
|
169
|
+
* before counting words.
|
|
170
|
+
*/
|
|
171
|
+
declare function calculateReadTime(content: string, options?: ReadTimeOptions): ReadTimeResult;
|
|
172
|
+
/**
|
|
173
|
+
* Sort items by date in descending order (newest first).
|
|
174
|
+
* Returns a new array without mutating the original.
|
|
175
|
+
*/
|
|
176
|
+
declare function sortByDateDescending<T>(items: T[], getDate: (item: T) => string): T[];
|
|
177
|
+
|
|
178
|
+
export { type CreateMdxCollectionOptions as C, type FrontmatterSchema as F, type GetContentSlugsOptions as G, type MdxCollection as M, type NextRedirect as N, type ReadTimeOptions as R, type TableOfContents as T, type CollectRedirectsOptions as a, type MdxValidationIssue as b, type MdxCollectionGetArgs as c, type MdxEntry as d, type ReadTimeResult as e, type TableOfContentsEntry as f, calculateReadTime as g, stripMdxSyntax as h, sortByDateDescending as s };
|