Skip to content

Commit 7e9434d

Browse files
roderikclaude
andauthored
feat(mdx): add remark plugin for automatic link transformation (#1003)
## What Adds automatic link transformation for aggregated documentation from multiple sources. Internal links are transformed based on content section: - `/docs/...` in ATK content → `/asset-tokenization-kit/...` - `/docs/...` in SDK content → `/sdk/...` - `/docs/...` in blockchain-platform content → `/blockchain-platform/...` Also adds a link validation script that runs in CI to catch broken internal links. ## Why Documentation is aggregated from multiple sources (ATK via Docker, SDK from npm packages, blockchain-platform static content). Each source may use different link conventions (e.g., `/docs/...`) that need to be normalized for the unified documentation site. ## How - **remark-transform-links.ts**: A remark plugin that detects the content section from file path and applies section-specific URL transformations. Handles both standard markdown links and MDX JSX component `href` attributes. - **check-links.ts**: Script to validate internal links at build time - **migrate-links.ts**: Utility script to assist with link format migration ## Files Changed - `src/lib/remark-transform-links.ts` - Core remark plugin (new) - `source.config.ts` - Plugin integration - `scripts/check-links.ts` - Link validation script (new) - `scripts/migrate-links.ts` - Link migration utility (new) - `.github/workflows/qa.yml` - Added link checking step - `package.json` - Added dependencies and script - `.gitignore` - Ignore generated directories ## Breaking Changes None ## Testing - [x] Type checking passes - [x] Link transformation working in dev server - [x] Glossary links correctly resolve to `/documentation/asset-tokenization-kit/executive-overview/glossary` ## Related Linear Issues None ## Summary by Sourcery Introduce automatic link transformation and validation for aggregated documentation content and update content sourcing configuration. New Features: - Add a remark plugin to normalize internal documentation links based on their content section. - Add a link validation script to check internal links across MDX documentation files at build/CI time. - Add a link migration utility to bulk-update legacy internal links to the new base path format. Enhancements: - Wire the new remark link transformation plugin into the MDX configuration so all docs content uses normalized URLs. - Adjust Docker content sourcing for the asset tokenization kit to use the updated content image and export additional docs assets. Build: - Add package scripts and dependencies required for link checking and migration, and update the Bun lockfile accordingly. CI: - Extend the QA GitHub Actions workflow to run internal link validation on pushes and pull requests. Chores: - Update docker-compose service configuration for asset-tokenization-kit content handling and adjust .gitignore entries. <!-- This is an auto-generated description by cubic. --> --- ## Summary by cubic Adds a remark plugin that rewrites internal links per content section and a CI link checker to catch broken URLs across aggregated docs. - **New Features** - Remark plugin transforms internal links (/docs, /api) to section paths (/asset-tokenization-kit, /sdk, /blockchain-platform, /asset-tokenization-kit-legacy) for both Markdown links and MDX hrefs. - Link validation script (bun run check-links) scans MDX files and runs in the QA workflow. - Integrated plugin in MDX config so all sourced docs use normalized URLs. - **Migration** - Preview changes: bun scripts/migrate-links.ts --dry-run - Apply changes: bun scripts/migrate-links.ts <sup>Written for commit 768afcc. Summary will update automatically on new commits.</sup> <!-- End of auto-generated description by cubic. --> --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent c9dd6b4 commit 7e9434d

File tree

8 files changed

+492
-96
lines changed

8 files changed

+492
-96
lines changed

.github/workflows/qa.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,11 @@ jobs:
162162
NEXT_PUBLIC_POSTHOG_KEY: ${{ secrets.NEXT_PUBLIC_POSTHOG_KEY }}
163163
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
164164

165+
- name: Check links
166+
if: github.event_name == 'pull_request' || github.event_name == 'push'
167+
run: |
168+
bun run check-links
169+
165170
- name: Build Docker container
166171
if: github.event_name == 'pull_request' || github.event_name == 'push'
167172
run: |

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,3 +34,7 @@ content/docs/sdk/*
3434

3535
public/images/screenshots/*
3636
public/atk
37+
public/docs
38+
39+
# AI planning docs
40+
plans/

bun.lock

Lines changed: 70 additions & 92 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
"lint": "eslint",
1212
"check-types": "tsc --noEmit",
1313
"format": "prettier --write .",
14+
"check-links": "bun scripts/check-links.ts",
1415
"generate-sdk-docs": "bun scripts/generate-sdk-docs.ts",
1516
"docker": "bash -c \"docker buildx build . --provenance true --sbom true --platform=linux/amd64,linux/arm64 -t ghcr.io/settlemint/btp-docs:${VERSION:-7.0.0-dev.$(date +%s)} --push --progress=plain\""
1617
},
@@ -41,6 +42,7 @@
4142
"lucide-react": "0.559.0",
4243
"mermaid": "11.12.2",
4344
"next": "16.0.8",
45+
"next-validate-link": "1.6.3",
4446
"posthog-js": "1.304.0",
4547
"react": "19.2.1",
4648
"react-dom": "19.2.1",
@@ -53,6 +55,7 @@
5355
"zod": "4.1.13"
5456
},
5557
"devDependencies": {
58+
"glob": "13.0.0",
5659
"@tailwindcss/postcss": "4.1.17",
5760
"@types/mdx": "2.0.13",
5861
"@types/node": "25.0.0",
@@ -66,4 +69,4 @@
6669
"@eslint/eslintrc": "3.3.3",
6770
"prettier": "3.7.4"
6871
}
69-
}
72+
}

scripts/check-links.ts

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import { scanURLs, validateFiles, printErrors } from "next-validate-link";
2+
import { source } from "../src/lib/source";
3+
import { glob } from "glob";
4+
5+
const BASE_PATH = "/documentation";
6+
7+
async function getFiles(): Promise<string[]> {
8+
return glob("content/docs/**/*.mdx", { cwd: process.cwd() });
9+
}
10+
11+
async function checkLinks() {
12+
console.log("[Check Links] Starting link validation...");
13+
14+
const pages = source.getPages();
15+
console.log(`[Check Links] Found ${pages.length} pages in source`);
16+
17+
// Build the URL map for validation
18+
// For catch-all routes [[...slug]], value should be string[] directly
19+
const populate: Record<
20+
string,
21+
Array<{ value: string[]; hashes: string[] }>
22+
> = {
23+
"[[...slug]]": pages.map((page) => ({
24+
value: page.slugs,
25+
hashes: [], // Skip heading validation for now
26+
})),
27+
};
28+
29+
const scanned = await scanURLs({
30+
preset: "next",
31+
populate,
32+
});
33+
34+
const files = await getFiles();
35+
console.log(`[Check Links] Validating ${files.length} MDX files`);
36+
37+
const errors = await validateFiles(files, {
38+
scanned,
39+
pathToUrl: (path: string) => {
40+
// Transform file path to URL
41+
const slug = path
42+
.replace("content/docs/", "")
43+
.replace(/\.mdx?$/, "")
44+
.replace(/\/index$/, "");
45+
return `${BASE_PATH}/${slug}`;
46+
},
47+
markdown: {
48+
components: {
49+
Card: { attributes: ["href"] },
50+
Cards: { attributes: ["href"] },
51+
},
52+
},
53+
checkExternal: false, // External links checked separately
54+
checkRelativePaths: "as-url",
55+
});
56+
57+
if (errors.length > 0) {
58+
console.log(`\n[Check Links] Found ${errors.length} link errors:\n`);
59+
printErrors(errors, true);
60+
process.exit(1);
61+
}
62+
63+
console.log("[Check Links] All links validated successfully!");
64+
}
65+
66+
checkLinks().catch((error) => {
67+
console.error("[Check Links] Error:", error);
68+
process.exit(1);
69+
});

scripts/migrate-links.ts

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
import { glob } from "glob";
2+
import { readFile, writeFile } from "node:fs/promises";
3+
4+
const BASE_PATH = "/documentation";
5+
const DRY_RUN = process.argv.includes("--dry-run");
6+
7+
/**
8+
* Migrate internal links in MDX files to use the basePath prefix.
9+
*
10+
* This script finds all root-relative links that don't already have
11+
* the /documentation/ prefix and adds it.
12+
*
13+
* IMPORTANT: This script is designed for one-time migration of legacy content.
14+
* The remark-transform-links plugin handles link transformation at build time,
15+
* so this script should NOT be run on content that will be processed by that plugin.
16+
* Running both would result in double-prefixed links.
17+
*
18+
* Usage:
19+
* bun scripts/migrate-links.ts # Apply changes
20+
* bun scripts/migrate-links.ts --dry-run # Preview changes without writing
21+
*/
22+
23+
// Type for replacement functions that match String.replace callback signature
24+
type ReplaceFn = (substring: string, ...args: string[]) => string;
25+
26+
// Patterns to transform (root-relative without basePath)
27+
// These patterns match links that start with / but NOT /documentation/, /images/, /api/, etc.
28+
const LINK_PATTERNS: Array<{ regex: RegExp; replace: ReplaceFn }> = [
29+
// Markdown links: [text](/path) - but not [text](/documentation/...) or [text](/images/...) etc.
30+
{
31+
regex:
32+
/(\[[^\]]+\]\()\/(?!documentation\/|images\/|api\/|ingest\/|_next\/)([^)]+\))/g,
33+
replace: (_match: string, prefix: string, path: string) =>
34+
`${prefix}${BASE_PATH}/${path}`,
35+
},
36+
// JSX href in Card/other components: href="/path" - but not href="/documentation/..." etc.
37+
{
38+
regex:
39+
/href="\/(?!documentation\/|images\/|api\/|ingest\/|_next\/)([^"]+)"/g,
40+
replace: (_match: string, path: string) => `href="${BASE_PATH}/${path}"`,
41+
},
42+
];
43+
44+
async function migrateLinks() {
45+
console.log("[Migrate Links] Starting link migration...");
46+
if (DRY_RUN) {
47+
console.log("[Migrate Links] DRY RUN MODE - no files will be modified\n");
48+
}
49+
50+
const files = await glob("content/docs/**/*.mdx", { cwd: process.cwd() });
51+
console.log(`[Migrate Links] Found ${files.length} MDX files to scan\n`);
52+
53+
let totalChanges = 0;
54+
let filesChanged = 0;
55+
56+
for (const file of files) {
57+
const content = await readFile(file, "utf-8");
58+
let newContent = content;
59+
let fileChanges = 0;
60+
61+
for (const pattern of LINK_PATTERNS) {
62+
const matches = [...newContent.matchAll(pattern.regex)];
63+
fileChanges += matches.length;
64+
65+
newContent = newContent.replace(pattern.regex, pattern.replace);
66+
}
67+
68+
if (fileChanges > 0) {
69+
filesChanged++;
70+
totalChanges += fileChanges;
71+
72+
if (DRY_RUN) {
73+
console.log(`[DRY RUN] ${file}: ${fileChanges} links would be updated`);
74+
75+
// Show the actual changes for this file
76+
const originalLines = content.split("\n");
77+
const newLines = newContent.split("\n");
78+
79+
for (let i = 0; i < originalLines.length; i++) {
80+
if (originalLines[i] !== newLines[i]) {
81+
console.log(` Line ${i + 1}:`);
82+
console.log(` - ${originalLines[i].trim()}`);
83+
console.log(` + ${newLines[i].trim()}`);
84+
}
85+
}
86+
console.log("");
87+
} else {
88+
await writeFile(file, newContent);
89+
console.log(`[Migrate Links] ${file}: ${fileChanges} links updated`);
90+
}
91+
}
92+
}
93+
94+
console.log(
95+
`\n[Migrate Links] Summary: ${totalChanges} links ${DRY_RUN ? "would be" : ""} updated across ${filesChanged} files`
96+
);
97+
98+
if (DRY_RUN && totalChanges > 0) {
99+
console.log(
100+
"\n[Migrate Links] Run without --dry-run to apply these changes"
101+
);
102+
}
103+
}
104+
105+
migrateLinks().catch((error) => {
106+
console.error("[Migrate Links] Error:", error);
107+
process.exit(1);
108+
});

source.config.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,14 @@ import {
33
defineDocs,
44
frontmatterSchema,
55
metaSchema,
6-
} from 'fumadocs-mdx/config';
6+
} from "fumadocs-mdx/config";
77
import { z } from "zod";
8+
import { remarkTransformLinks } from "@/lib/remark-transform-links";
89

910
// You can customise Zod schemas for frontmatter and `meta.json` here
1011
// see https://fumadocs.dev/docs/mdx/collections
1112
export const docs = defineDocs({
12-
dir: 'content/docs',
13+
dir: "content/docs",
1314
docs: {
1415
schema: frontmatterSchema.extend({
1516
/**
@@ -40,6 +41,6 @@ export const docs = defineDocs({
4041

4142
export default defineConfig({
4243
mdxOptions: {
43-
// MDX options
44+
remarkPlugins: [remarkTransformLinks],
4445
},
4546
});

0 commit comments

Comments
 (0)