diff --git a/examples/versioned/content/dev/api.mdx b/examples/versioned/content/dev/api.mdx
index f1afad0b..82668e13 100644
--- a/examples/versioned/content/dev/api.mdx
+++ b/examples/versioned/content/dev/api.mdx
@@ -6,3 +6,8 @@ order: 2
# Dev API notes — latest
Latest `/dev/api`.
+
+- [Dev Home](./index.mdx)
+- [Docs Guide](../docs/guide.mdx)
+- [Docs Home](../docs/index.mdx)
+- [Guide with hash](../docs/guide.mdx#some-section)
diff --git a/examples/versioned/content/dev/index.mdx b/examples/versioned/content/dev/index.mdx
index bff05478..1ff62d55 100644
--- a/examples/versioned/content/dev/index.mdx
+++ b/examples/versioned/content/dev/index.mdx
@@ -6,3 +6,7 @@ order: 1
# Dev — latest
Latest `/dev`.
+
+- [API Notes](./api.mdx)
+- [Docs Home](../docs/index.mdx)
+- [Docs Guide](../docs/guide.mdx)
diff --git a/examples/versioned/content/docs/guide.mdx b/examples/versioned/content/docs/guide.mdx
index 96a4f64a..cf1058c7 100644
--- a/examples/versioned/content/docs/guide.mdx
+++ b/examples/versioned/content/docs/guide.mdx
@@ -6,3 +6,7 @@ order: 2
# Guide — latest docs
Latest `/docs/guide`.
+
+- [Docs Home](./index.mdx)
+- [Dev API](../dev/api.mdx)
+- [Dev Home](../dev/index.mdx)
diff --git a/examples/versioned/content/docs/index.mdx b/examples/versioned/content/docs/index.mdx
index 99b03d74..289077b8 100644
--- a/examples/versioned/content/docs/index.mdx
+++ b/examples/versioned/content/docs/index.mdx
@@ -8,6 +8,10 @@ order: 1
This is `/docs` on latest (3.0).
+## Links
+
+- [Guide](./guide.mdx)
+
## Getting Started
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
diff --git a/packages/chronicle/src/components/ui/search.tsx b/packages/chronicle/src/components/ui/search.tsx
index c2a3e7d6..1a313ac4 100644
--- a/packages/chronicle/src/components/ui/search.tsx
+++ b/packages/chronicle/src/components/ui/search.tsx
@@ -13,10 +13,10 @@ import { usePageContext } from '@/lib/page-context';
import styles from './search.module.css';
interface SearchProps {
- className?: string;
+ classNames?: { trigger?: string };
}
-export function Search({ className }: SearchProps) {
+export function Search({ classNames }: SearchProps) {
const [open, setOpen] = useState(false);
const navigate = useNavigate();
const { version } = usePageContext();
@@ -60,7 +60,7 @@ export function Search({ className }: SearchProps) {
aria-label='Search'
title='Search (Ctrl/⌘K)'
onClick={() => setOpen(true)}
- className={className}
+ className={classNames?.trigger}
>
diff --git a/packages/chronicle/src/lib/remark-resolve-links.ts b/packages/chronicle/src/lib/remark-resolve-links.ts
new file mode 100644
index 00000000..68efe2d5
--- /dev/null
+++ b/packages/chronicle/src/lib/remark-resolve-links.ts
@@ -0,0 +1,32 @@
+import path from 'node:path'
+import { visit } from 'unist-util-visit'
+import type { Plugin } from 'unified'
+import type { Link } from 'mdast'
+
+const remarkResolveLinks: Plugin = () => {
+ return (tree, file) => {
+ const filePath = file.path
+ if (!filePath) return
+
+ const contentIdx = filePath.lastIndexOf('/content/')
+ if (contentIdx === -1) return
+
+ const relative = filePath.slice(contentIdx + '/content/'.length)
+ const dir = path.posix.dirname(relative)
+
+ visit(tree, 'link', (node: Link) => {
+ if (!node.url) return
+ if (/^[a-z][a-z0-9+\-.]*:/i.test(node.url)) return
+ if (node.url.startsWith('#')) return
+ if (node.url.startsWith('/')) return
+
+ const [rawPath, hash] = node.url.split('#')
+ const stripped = rawPath.replace(/\.mdx?$/, '')
+ let resolved = path.posix.normalize(path.posix.join(dir, stripped))
+ resolved = resolved.replace(/\/(index|readme)$/i, '') || '.'
+ node.url = `/${resolved}${hash ? `#${hash}` : ''}`
+ })
+ }
+}
+
+export default remarkResolveLinks
diff --git a/packages/chronicle/src/lib/remark-strip-md-extensions.ts b/packages/chronicle/src/lib/remark-strip-md-extensions.ts
deleted file mode 100644
index 91858eff..00000000
--- a/packages/chronicle/src/lib/remark-strip-md-extensions.ts
+++ /dev/null
@@ -1,14 +0,0 @@
-import { visit } from 'unist-util-visit'
-import type { Plugin } from 'unified'
-
-const remarkStripMdExtensions: Plugin = () => {
- return (tree) => {
- visit(tree, 'link', (node: any) => {
- if (!node.url) return
- if (node.url.startsWith('http://') || node.url.startsWith('https://')) return
- node.url = node.url.replace(/\.mdx?(#|$)/, '$1')
- })
- }
-}
-
-export default remarkStripMdExtensions
diff --git a/packages/chronicle/src/pages/ApiLayout.tsx b/packages/chronicle/src/pages/ApiLayout.tsx
index bfd6631a..66593c92 100644
--- a/packages/chronicle/src/pages/ApiLayout.tsx
+++ b/packages/chronicle/src/pages/ApiLayout.tsx
@@ -1,6 +1,5 @@
import { cx } from 'class-variance-authority';
import type { ReactNode } from 'react';
-import { Search } from '@/components/ui/search';
import { buildApiPageTree } from '@/lib/api-routes';
import { usePageContext } from '@/lib/page-context';
import { getTheme } from '@/themes/registry';
@@ -26,7 +25,6 @@ export function ApiLayout({ children }: ApiLayoutProps) {
content: styles.content
}}
>
-
{children}
);
diff --git a/packages/chronicle/src/server/api/page.ts b/packages/chronicle/src/server/api/page.ts
index 568d6aa4..6d4ac035 100644
--- a/packages/chronicle/src/server/api/page.ts
+++ b/packages/chronicle/src/server/api/page.ts
@@ -1,5 +1,5 @@
import { defineHandler, HTTPError } from 'nitro';
-import { getPage, getPageNav, extractFrontmatter, getRelativePath, getOriginalPath } from '@/lib/source';
+import { getPage, getPageNav, extractFrontmatter, getRelativePath, getOriginalPath, loadPageModule } from '@/lib/source';
export default defineHandler(async event => {
const slugParam = event.url.searchParams.get('slug') ?? '';
@@ -11,11 +11,17 @@ export default defineHandler(async event => {
}
const nav = await getPageNav(slug);
+ const originalPath = getOriginalPath(page);
+ const relativePath = getRelativePath(page);
+ const mdxModule = (originalPath || relativePath) ? await loadPageModule(originalPath || relativePath) : null;
return {
- frontmatter: extractFrontmatter(page, slug[slug.length - 1]),
- relativePath: getRelativePath(page),
- originalPath: getOriginalPath(page),
+ frontmatter: {
+ ...extractFrontmatter(page, slug[slug.length - 1]),
+ _readingTime: mdxModule?._readingTime,
+ },
+ relativePath,
+ originalPath,
prev: nav.prev,
next: nav.next,
};
diff --git a/packages/chronicle/src/server/vite-config.ts b/packages/chronicle/src/server/vite-config.ts
index c0084331..b285fdfa 100644
--- a/packages/chronicle/src/server/vite-config.ts
+++ b/packages/chronicle/src/server/vite-config.ts
@@ -7,7 +7,7 @@ import fs from 'node:fs/promises';
import path from 'node:path';
import remarkDirective from 'remark-directive';
import { type InlineConfig } from 'vite';
-import remarkStripMdExtensions from '../lib/remark-strip-md-extensions';
+import remarkResolveLinks from '../lib/remark-resolve-links';
import remarkReadingTime from 'remark-reading-time';
import remarkUnusedDirectives from '../lib/remark-unused-directives';
@@ -77,7 +77,7 @@ export async function createViteConfig(
},
}],
remarkUnusedDirectives,
- remarkStripMdExtensions,
+ remarkResolveLinks,
remarkMdxMermaid,
remarkReadingTime,
],
diff --git a/packages/chronicle/src/themes/paper/Layout.module.css b/packages/chronicle/src/themes/paper/Layout.module.css
index 4fb5bf08..27d98c40 100644
--- a/packages/chronicle/src/themes/paper/Layout.module.css
+++ b/packages/chronicle/src/themes/paper/Layout.module.css
@@ -79,3 +79,7 @@
flex: 1;
background: var(--rs-color-background-neutral-primary);
}
+
+.hiddenTrigger {
+ display: none;
+}
diff --git a/packages/chronicle/src/themes/paper/Layout.tsx b/packages/chronicle/src/themes/paper/Layout.tsx
index d52c80ab..974fdaa7 100644
--- a/packages/chronicle/src/themes/paper/Layout.tsx
+++ b/packages/chronicle/src/themes/paper/Layout.tsx
@@ -6,6 +6,7 @@ import { useLocation, useNavigate } from 'react-router';
import { getLandingEntries } from '@/lib/config';
import { getActiveContentDir } from '@/lib/navigation';
import { usePageContext } from '@/lib/page-context';
+import { Search } from '@/components/ui/search';
import type { ThemeLayoutProps } from '@/types';
import { ChapterNav } from './ChapterNav';
import styles from './Layout.module.css';
@@ -82,6 +83,7 @@ function LayoutInner({
) : null}
+ {config.search?.enabled && }
{children}