Skip to content

Commit 86893af

Browse files
committed
feat: Edit links in markdown when serving .md files
1 parent dcc5deb commit 86893af

File tree

1 file changed

+55
-4
lines changed

1 file changed

+55
-4
lines changed

src/pages/api/md/[...path].ts

Lines changed: 55 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,51 @@ import type {NextApiRequest, NextApiResponse} from 'next';
99
import fs from 'fs';
1010
import path from 'path';
1111

12+
import remark from 'remark';
13+
import visit from 'unist-util-visit';
14+
15+
const CONTENT_ROOT = path.join(process.cwd(), 'src/content');
16+
const NOOP_ORIGIN = 'https://noop';
17+
18+
function rewriteInternalLinks(markdown: string): string {
19+
const processor = remark().use(() => (tree) => {
20+
visit(tree, 'link', (node: unknown) => {
21+
if (typeof node !== 'object' || node === null || !('url' in node)) {
22+
return;
23+
}
24+
if (typeof node.url !== 'string') {
25+
return;
26+
}
27+
28+
if (!node.url.startsWith('/')) {
29+
return;
30+
}
31+
32+
let url: URL;
33+
try {
34+
url = new URL(node.url, NOOP_ORIGIN);
35+
} catch {
36+
return;
37+
}
38+
39+
const pathname = url.pathname;
40+
41+
// Skip links that already have a file extension (e.g. .png, .svg)
42+
if (/\.\w+$/.test(pathname)) {
43+
return;
44+
}
45+
46+
url.pathname = pathname.endsWith('/')
47+
? `${pathname.slice(0, -1)}.md`
48+
: `${pathname}.md`;
49+
50+
node.url = url.toString().replace(NOOP_ORIGIN, '');
51+
});
52+
});
53+
54+
return processor.processSync(markdown).toString();
55+
}
56+
1257
export default function handler(req: NextApiRequest, res: NextApiResponse) {
1358
const pathSegments = req.query.path;
1459
if (!pathSegments) {
@@ -26,13 +71,19 @@ export default function handler(req: NextApiRequest, res: NextApiResponse) {
2671

2772
// Try exact path first, then with /index
2873
const candidates = [
29-
path.join(process.cwd(), 'src/content', filePath + '.md'),
30-
path.join(process.cwd(), 'src/content', filePath, 'index.md'),
74+
path.join(CONTENT_ROOT, filePath + '.md'),
75+
path.join(CONTENT_ROOT, filePath, 'index.md'),
3176
];
3277

33-
for (const fullPath of candidates) {
78+
for (const candidate of candidates) {
79+
const fullPath = path.resolve(candidate);
80+
if (!fullPath.startsWith(CONTENT_ROOT + path.sep)) {
81+
continue;
82+
}
83+
3484
try {
35-
const content = fs.readFileSync(fullPath, 'utf8');
85+
const raw = fs.readFileSync(fullPath, 'utf8');
86+
const content = rewriteInternalLinks(raw);
3687
res.setHeader('Content-Type', 'text/plain; charset=utf-8');
3788
res.setHeader('Cache-Control', 'public, max-age=3600');
3889
return res.status(200).send(content);

0 commit comments

Comments
 (0)