@@ -9,6 +9,51 @@ import type {NextApiRequest, NextApiResponse} from 'next';
99import fs from 'fs' ;
1010import 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+
1257export 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