@tansuasici/docsync
v0.1.0
Published
Transform GitHub markdown into docs-framework-compatible output. Write once in GFM, publish everywhere.
Maintainers
Readme
DocSync
DocSync transforms your GitHub-native markdown (README.md, docs/*.md) into framework-ready MDX. No more maintaining docs in two places.
README.md ──┐
docs/*.md ──┤── docsync build ──→ .docsync/*.mdx ──→ Fumadocs / Docusaurus / ...
└── docsync.config.tsThe Problem
You write documentation in markdown and it looks great on GitHub. But when you want a modern docs site (Fumadocs, Docusaurus, Nextra, Starlight), every framework demands its own directory structure, MDX format, and frontmatter conventions. You end up maintaining docs in two places.
DocSync bridges this gap. Keep your markdown as the single source of truth.
Quick Start
npm install -D @tansuasici/docsync
npx docsync initEdit the generated config:
// docsync.config.ts
import { defineConfig } from '@tansuasici/docsync'
export default defineConfig({
sources: [
{ path: 'README.md', slug: 'index', title: 'Introduction' },
{ path: 'docs/**/*.md' },
],
target: 'fumadocs',
github: { repo: 'your-username/your-repo' },
})Build:
npx docsync build
# ✓ index.mdx
# ✓ getting-started.mdx
# ✓ api.mdx
# Done! 3 files written to .docsync/Point your docs framework to the output:
// source.config.ts (Fumadocs)
import { defineDocs } from 'fumadocs-mdx/config'
export const docs = defineDocs({
dir: '.docsync',
})What It Does
| Input (GFM) | Output (MDX) |
|---|---|
| > [!NOTE] alerts | <Callout> components |
| {curly braces} in text | \{escaped\} for MDX |
| <!-- HTML comments --> | {/* JSX comments */} |
| Unclosed tags in prose (<placeholder>, List<T>) | \<placeholder> (otherwise an MDX compile error) |
| ./docs/guide.md links | /docs/guide site URLs |
| ./docs/agents/README.md links | /docs/agents (folder route, not /docs/agents/index) |
| ## [1.2.0](…/compare/…) headings | ## 1.2.0 on Fumadocs (it wraps headings in its own anchor — a nested <a> breaks hydration) |
| ./assets/logo.png | GitHub raw URL |
| First # Heading | title frontmatter |
| First paragraph | description frontmatter |
It also generates navigation config (meta.json for Fumadocs) from your source ordering — or from an explicit nav config.
Configuration
import { defineConfig } from '@tansuasici/docsync'
export default defineConfig({
// Required
sources: [
{ path: 'README.md', slug: 'index', title: 'Introduction' },
{ path: 'docs/**/*.md' },
{ path: 'CHANGELOG.md', slug: 'changelog', order: 99 },
],
target: 'fumadocs', // or 'docusaurus', 'nextra', 'starlight'
// Optional
outDir: '.docsync', // default: '.docsync'
baseUrl: '/docs', // default: '/docs'
clean: true, // default: true
github: {
repo: 'user/repo',
branch: 'main', // default: 'main'
},
})Source Entry Options
| Option | Type | Description |
|--------|------|-------------|
| path | string | File path or glob pattern (required) |
| slug | string | Override URL slug |
| title | string | Override page title |
| description | string | Override page description |
| order | number | Sidebar position (lower = higher) |
| rootDir | string | Prefix stripped from glob matches when deriving slugs (../Repo/docs/core/a.md with rootDir: '../Repo/docs' → core/a) |
Navigation (Fumadocs)
By default every directory's meta.json lists its pages and subfolders in source order. To control the sidebar yourself, add nav, keyed by output directory ('' is the root):
export default defineConfig({
// ...
nav: {
'': {
title: 'Documentation',
pages: [
'index',
'---Guides---', // section separator
'getting-started',
'agents', // folder — has its own entry below
'skill-explorer', // page written by another tool is fine
'[GitHub](https://github.com/user/repo)',
'changelog',
],
},
agents: { title: 'Agents', pages: ['planner', 'reviewer', '...'] },
reference: { title: 'API Reference' }, // title only — keeps generated order
},
})pagesis written verbatim, so all Fumadocs syntax works:---Label---,...(the rest, alphabetically),...folder,!page,[Text](url). Other keys (icon,defaultOpen, …) pass through.An explicit entry is the source of truth — it is never merged with the existing file, even with
clean: false.After writing, DocSync checks each
meta.jsonagainst what's actually in that directory, including pages other tools wrote there, and warns about:- entries that match no page or folder (typos, renames)
- pages missing from
pages. Fumadocs treats the list as exhaustive, so those pages still build but never show up in the sidebar. End the list with"..."to include them.
The check only sees files that exist when DocSync runs, so run other page generators before
docsync build.
Integration with Build Pipeline
{
"scripts": {
"prebuild": "docsync build",
"build": "next build",
"dev": "docsync build && next dev"
}
}Supported Targets
| Target | Status | Alert Syntax | Nav Config |
|--------|--------|-------------|------------|
| Fumadocs | Full support | <Callout> | meta.json |
| Docusaurus | Full support | :::note directive | _category_.json |
| Nextra (3+) | Full support | <Callout> | _meta.js per directory |
| Starlight | Full support | :::note directive | frontmatter sidebar |
How It Works
DocSync uses a unified / remark pipeline to transform markdown at the AST level:
- Parse GFM markdown (tables, task lists, alerts, etc.)
- Transform GitHub-specific syntax to MDX-compatible output
- Rewrite relative links and images using the source map
- Inject frontmatter (title, description) from content analysis
- Generate framework-specific navigation config
This approach is reliable — no regex hacks, no string replacement. The AST handles edge cases like nested code blocks, frontmatter, and mixed content correctly.
Contributing
Contributions are welcome! The codebase is TypeScript with a clean architecture:
src/
cli/ CLI commands (citty)
config/ Config schema (Zod) + loader (c12)
core/ Build pipeline + source resolver
transform/ Remark plugins (alerts, escaping, links, images)
adapters/ Framework-specific output (Fumadocs, ...)pnpm install
pnpm test # 121 tests
pnpm typecheck # TypeScript verification
pnpm lint # ESLint (flat config)
pnpm build # Build with tsupLicense
MIT
