aboutsummaryrefslogtreecommitdiff
path: root/website/src
diff options
context:
space:
mode:
authorOgulcan Celik <ogulcancelik@gmail.com>2026-07-22 14:58:26 +0300
committerOgulcan Celik <ogulcancelik@gmail.com>2026-07-22 14:59:09 +0300
commitc234f221f9be698bc0501c60459e1ae8e67df6c3 (patch)
tree944b4dec10fada8d93456dc5a85e6bfdb36a8b8b /website/src
parentef85fa0c7ebb48bb59fe7593af5a67f3e02ff3d4 (diff)
feat(docs): publish versioned release documentation
Diffstat (limited to 'website/src')
-rw-r--r--website/src/components/Banner.astro29
-rw-r--r--website/src/components/ConfigReference.astro13
-rw-r--r--website/src/components/DocsVersionSelect.astro84
-rw-r--r--website/src/components/Head.astro7
-rw-r--r--website/src/components/Header.astro58
-rw-r--r--website/src/components/Search.astro28
-rw-r--r--website/src/components/Sidebar.astro67
-rw-r--r--website/src/components/SiteTitle.astro7
-rw-r--r--website/src/docs-path.test.ts49
-rw-r--r--website/src/docs-path.ts83
10 files changed, 287 insertions, 138 deletions
diff --git a/website/src/components/Banner.astro b/website/src/components/Banner.astro
index 8a4bad22..01098659 100644
--- a/website/src/components/Banner.astro
+++ b/website/src/components/Banner.astro
@@ -1,4 +1,7 @@
---
+import versions from '../data/docs-versions.json';
+import { docsRoute, docsTargetHref } from '../docs-path';
+
const { entry, locale, isFallback } = Astro.locals.starlightRoute;
const { banner } = entry.data;
@@ -8,9 +11,24 @@ const translationNotices = {
'zh-cn': `本页面的翻译由 LLM 生成。如果你发现翻译有误,请<a href="${issuesUrl}">在 GitHub 上提交 issue</a> 告诉我们。`,
};
const translationNotice = locale && !isFallback ? translationNotices[locale] : undefined;
+const docs = docsRoute(Astro.url.pathname);
+const archivedVersion =
+ docs.target !== 'stable' && docs.target !== 'preview' && docs.target !== versions.current
+ ? docs.target
+ : undefined;
+const latestHref = docsTargetHref(docs, 'stable', versions.scopes);
+const archivedNotices = {
+ root: `This page documents Herdr ${archivedVersion}. <a href="${latestHref}">Switch to the latest release.</a>`,
+ ja: `このページは Herdr ${archivedVersion} のドキュメントです。<a href="${latestHref}">最新版に切り替えてください。</a>`,
+ 'zh-cn': `本页面是 Herdr ${archivedVersion} 的文档。<a href="${latestHref}">切换到最新版本。</a>`,
+};
+const archivedNotice = archivedVersion ? archivedNotices[locale ?? 'root'] : undefined;
---
{banner && <div class="sl-banner" data-pagefind-ignore set:html={banner.content} />}
+{archivedNotice && (
+ <div class="version-notice" data-pagefind-ignore set:html={archivedNotice} />
+)}
{translationNotice && (
<div class="translation-notice" data-pagefind-ignore set:html={translationNotice} />
)}
@@ -31,7 +49,8 @@ const translationNotice = locale && !isFallback ? translationNotices[locale] : u
color: var(--__sl-banner-text);
}
- .translation-notice {
+ .translation-notice,
+ .version-notice {
padding: 0.5rem var(--sl-nav-pad-x);
background-color: var(--sl-color-bg-nav);
border-bottom: 1px solid var(--sl-color-hairline);
@@ -41,8 +60,16 @@ const translationNotice = locale && !isFallback ? translationNotices[locale] : u
text-align: center;
text-wrap: balance;
}
+ .version-notice {
+ background-color: var(--sl-color-orange-low);
+ border-bottom-color: var(--sl-color-orange);
+ color: var(--sl-color-orange-high);
+ }
.translation-notice :global(a) {
color: var(--sl-color-text-accent);
}
+ .version-notice :global(a) {
+ color: var(--sl-color-orange-high);
+ }
}
</style>
diff --git a/website/src/components/ConfigReference.astro b/website/src/components/ConfigReference.astro
index d951f1cc..33894993 100644
--- a/website/src/components/ConfigReference.astro
+++ b/website/src/components/ConfigReference.astro
@@ -1,6 +1,8 @@
---
import stableReference from '../data/config-reference.json';
import previewReference from '../data/config-reference-preview.json';
+import versionReferences from '../data/config-reference-versions.json';
+import { docsVersion } from '../docs-path';
interface Props {
placeholder?: string;
@@ -14,9 +16,16 @@ const {
keyCountLabel = 'keys',
} = Astro.props;
-const reference = Astro.url.pathname.includes('/docs/preview/')
+const version = docsVersion(Astro.url.pathname);
+const archivedReferences = versionReferences as Record<string, typeof stableReference>;
+const reference = version === 'preview'
? previewReference
- : stableReference;
+ : version === 'stable'
+ ? stableReference
+ : archivedReferences[version];
+if (!reference) {
+ throw new Error(`documentation version ${version} has no archived configuration reference`);
+}
const totalKeys = reference.sections.reduce((sum, section) => sum + section.keys.length, 0);
---
diff --git a/website/src/components/DocsVersionSelect.astro b/website/src/components/DocsVersionSelect.astro
new file mode 100644
index 00000000..cc6ea67f
--- /dev/null
+++ b/website/src/components/DocsVersionSelect.astro
@@ -0,0 +1,84 @@
+---
+import versions from '../data/docs-versions.json';
+import { docsRoute, docsTargetHref } from '../docs-path';
+
+const route = docsRoute(Astro.url.pathname);
+const locale = route.locale ?? 'root';
+const labels = {
+ root: { accessible: 'Documentation version', latest: 'Latest', next: 'Unreleased' },
+ ja: { accessible: 'ドキュメントのバージョン', latest: '最新', next: '未リリース' },
+ 'zh-cn': { accessible: '文档版本', latest: '最新', next: '未发布' },
+}[locale];
+const scopes = versions.scopes;
+---
+
+{route.isDocs && (
+ <label class="docs-version-select">
+ <span class="sr-only">{labels.accessible}</span>
+ <select autocomplete="off" aria-label={labels.accessible}>
+ <option
+ value={docsTargetHref(route, 'stable', scopes)}
+ selected={route.target === 'stable'}
+ >
+ {labels.latest}{' '}{versions.current}
+ </option>
+ <option
+ value={docsTargetHref(route, 'preview', scopes)}
+ selected={route.target === 'preview'}
+ >
+ {labels.next}
+ </option>
+ {versions.versions
+ .filter((entry) => entry.version !== versions.current || route.target === versions.current)
+ .map((entry) => (
+ <option
+ value={docsTargetHref(route, entry.version, scopes)}
+ selected={route.target === entry.version}
+ >
+ {entry.version}
+ </option>
+ ))}
+ </select>
+ </label>
+)}
+
+<script>
+ for (const select of document.querySelectorAll<HTMLSelectElement>('.docs-version-select select')) {
+ select.addEventListener('change', () => {
+ globalThis.location.href = select.value;
+ });
+ }
+</script>
+
+<style>
+ .docs-version-select {
+ display: flex;
+ min-width: 0;
+ }
+
+ select {
+ width: 9.5rem;
+ min-width: 0;
+ border: 1px solid var(--sl-color-hairline);
+ border-radius: 4px;
+ padding: 0.4rem 1.6rem 0.4rem 0.55rem;
+ background: var(--sl-color-bg-nav);
+ color: var(--sl-color-gray-2);
+ font: inherit;
+ font-size: var(--sl-text-xs);
+ font-weight: 700;
+ text-overflow: ellipsis;
+ }
+
+ select:hover,
+ select:focus {
+ border-color: var(--sl-color-accent);
+ color: var(--sl-color-accent-high);
+ }
+
+ @media (max-width: 38rem) {
+ select {
+ width: 7.5rem;
+ }
+ }
+</style>
diff --git a/website/src/components/Head.astro b/website/src/components/Head.astro
index b1214cb1..9987173b 100644
--- a/website/src/components/Head.astro
+++ b/website/src/components/Head.astro
@@ -1,9 +1,10 @@
---
-import { docsChannel } from '../docs-path';
+import { docsVersion } from '../docs-path';
const { head } = Astro.locals.starlightRoute;
-const channel = docsChannel(Astro.url.pathname);
+const version = docsVersion(Astro.url.pathname);
---
{head.map(({ tag: Tag, attrs, content }) => <Tag {...attrs} set:html={content} />)}
-<meta data-pagefind-filter="channel[content]" content={channel} />
+<meta data-pagefind-filter="version[content]" content={version} />
+{version !== 'stable' && <meta name="robots" content="noindex, nofollow" />}
diff --git a/website/src/components/Header.astro b/website/src/components/Header.astro
index 982efd2e..cfa3d759 100644
--- a/website/src/components/Header.astro
+++ b/website/src/components/Header.astro
@@ -6,19 +6,12 @@ import Search from 'virtual:starlight/components/Search';
import SiteTitle from 'virtual:starlight/components/SiteTitle';
import SocialIcons from 'virtual:starlight/components/SocialIcons';
import ThemeSelect from 'virtual:starlight/components/ThemeSelect';
+import DocsVersionSelect from './DocsVersionSelect.astro';
+import { docsRoute } from '../docs-path';
const shouldRenderSearch =
config.pagefind || config.components.Search !== '@astrojs/starlight/components/Search.astro';
-
-const pathname = Astro.url.pathname;
-const isDocs = pathname === '/docs/' || pathname.startsWith('/docs/');
-const isPreview = pathname === '/docs/preview/' || pathname.startsWith('/docs/preview/');
-const stableHref = isPreview ? pathname.replace(/^\/docs\/preview\/?/, '/docs/') : pathname;
-const previewHref = isPreview
- ? pathname
- : pathname === '/docs/'
- ? '/docs/preview/'
- : pathname.replace(/^\/docs\/?/, '/docs/preview/');
+const isDocs = docsRoute(Astro.url.pathname).isDocs;
---
<div class="header">
@@ -26,12 +19,7 @@ const previewHref = isPreview
<SiteTitle />
</div>
<div class="sl-flex print:hidden search-wrapper">
- {isDocs && (
- <nav class="docs-channel-switcher" aria-label="Documentation channel">
- <a href={stableHref} aria-current={!isPreview ? 'page' : undefined}>Stable</a>
- <a href={previewHref} aria-current={isPreview ? 'page' : undefined}>Preview</a>
- </nav>
- )}
+ {isDocs && <DocsVersionSelect />}
{shouldRenderSearch && <Search />}
</div>
<div class="sl-hidden md:sl-flex print:hidden right-group">
@@ -66,38 +54,6 @@ const previewHref = isPreview
min-width: 0;
}
- .docs-channel-switcher {
- display: flex;
- align-items: center;
- border: 1px solid var(--sl-color-hairline);
- border-radius: 4px;
- overflow: hidden;
- background: var(--sl-color-bg-nav);
- font-size: var(--sl-text-xs);
- font-weight: 700;
- line-height: 1;
- white-space: nowrap;
- }
-
- .docs-channel-switcher a {
- color: var(--sl-color-gray-2);
- text-decoration: none;
- padding: 0.45rem 0.6rem;
- }
-
- .docs-channel-switcher a + a {
- border-inline-start: 1px solid var(--sl-color-hairline);
- }
-
- .docs-channel-switcher a[aria-current="page"] {
- color: var(--sl-color-accent-high);
- background: color-mix(in srgb, var(--sl-color-accent) 14%, transparent);
- }
-
- .docs-channel-switcher a:hover {
- color: var(--sl-color-accent-high);
- }
-
.right-group,
.social-icons {
gap: 1rem;
@@ -110,12 +66,6 @@ const previewHref = isPreview
border-inline-end: 1px solid var(--sl-color-gray-5);
}
- @media (max-width: 38rem) {
- .docs-channel-switcher {
- display: none;
- }
- }
-
@media (min-width: 50rem) {
:global(:root[data-has-sidebar]) {
--__sidebar-pad: calc(2 * var(--sl-nav-pad-x));
diff --git a/website/src/components/Search.astro b/website/src/components/Search.astro
index 2a31ea24..cceeed34 100644
--- a/website/src/components/Search.astro
+++ b/website/src/components/Search.astro
@@ -1,25 +1,25 @@
---
import StarlightSearch from '@astrojs/starlight/components/Search.astro';
-import { docsChannel } from '../docs-path';
+import { docsVersion } from '../docs-path';
-const channel = docsChannel(Astro.url.pathname);
+const version = docsVersion(Astro.url.pathname);
---
-<channel-search data-channel={channel}>
+<version-search data-version={version}>
<StarlightSearch />
-</channel-search>
+</version-search>
<script>
- class ChannelSearch extends HTMLElement {
+ class VersionSearch extends HTMLElement {
observer?: MutationObserver;
connectedCallback() {
- const channel = this.dataset.channel;
- if (!channel) return;
+ const version = this.dataset.version;
+ if (!version) return;
- const selectChannel = () => {
+ const selectVersion = () => {
const checkbox = this.querySelector<HTMLInputElement>(
- `.pagefind-ui__filter-checkbox[name="channel"][value="${channel}"]`,
+ `.pagefind-ui__filter-checkbox[name="version"][value="${version}"]`,
);
if (!checkbox) return false;
if (!checkbox.checked) checkbox.click();
@@ -27,9 +27,9 @@ const channel = docsChannel(Astro.url.pathname);
return true;
};
- if (selectChannel()) return;
+ if (selectVersion()) return;
// Starlight does not expose its PagefindUI instance, so wait for Pagefind's filter control.
- this.observer = new MutationObserver(selectChannel);
+ this.observer = new MutationObserver(selectVersion);
this.observer.observe(this, { childList: true, subtree: true });
}
@@ -38,15 +38,15 @@ const channel = docsChannel(Astro.url.pathname);
}
}
- customElements.define('channel-search', ChannelSearch);
+ customElements.define('version-search', VersionSearch);
</script>
<style>
- channel-search {
+ version-search {
display: contents;
}
- channel-search :global(.pagefind-ui__filter-panel) {
+ version-search :global(.pagefind-ui__filter-panel) {
display: none;
}
</style>
diff --git a/website/src/components/Sidebar.astro b/website/src/components/Sidebar.astro
index 8ded83bb..08d31f79 100644
--- a/website/src/components/Sidebar.astro
+++ b/website/src/components/Sidebar.astro
@@ -2,62 +2,39 @@
import MobileMenuFooter from 'virtual:starlight/components/MobileMenuFooter';
import SidebarPersister from '@astrojs/starlight/components/SidebarPersister.astro';
import SidebarSublist from '@astrojs/starlight/components/SidebarSublist.astro';
+import versions from '../data/docs-versions.json';
+import { docsRoute, docsTargetHref } from '../docs-path';
const { sidebar } = Astro.locals.starlightRoute;
const pathname = Astro.url.pathname;
-const isPreview = pathname === '/docs/preview/' || pathname.startsWith('/docs/preview/');
-const channelSidebar = isPreview ? addPreviewOnlyItems(rewriteSidebar(sidebar)) : sidebar;
+const route = docsRoute(pathname);
+const scopedSidebar = route.target === 'stable' ? sidebar : rewriteSidebar(sidebar);
-function addPreviewOnlyItems(entries) {
- return entries.map((entry) => {
- if (entry.type !== 'group' || entry.label !== 'Start here') return entry;
-
- const windowsBeta = {
- type: 'link',
- label: 'Windows beta',
- href: '/docs/preview/windows-beta/',
- badge: undefined,
- isCurrent: samePath('/docs/preview/windows-beta/', pathname),
- attrs: {},
- };
- const nextEntries = [];
- for (const child of entry.entries) {
- nextEntries.push(child);
- if (child.type === 'link' && child.label === 'Install') {
- nextEntries.push(windowsBeta);
+function rewriteSidebar(entries) {
+ return entries.flatMap((entry) => {
+ if (entry.type === 'link') {
+ const linkRoute = docsRoute(entry.href);
+ if (!linkRoute.isDocs) return [entry];
+ if (linkRoute.target !== 'stable') {
+ return linkRoute.target === route.target
+ ? [{ ...entry, isCurrent: samePath(entry.href, pathname) }]
+ : [];
}
- }
- return {
- ...entry,
- entries: nextEntries,
- };
- });
-}
+ const localeKey = route.locale ?? 'root';
+ const availablePages = versions.scopes[route.target]?.locales[localeKey] ?? [];
+ if (!availablePages.includes(linkRoute.page)) return [];
-function rewriteSidebar(entries) {
- return entries.map((entry) => {
- if (entry.type === 'link') {
- const href = previewHref(entry.href);
- return {
- ...entry,
- href,
- isCurrent: samePath(href, pathname),
- };
+ const href = docsTargetHref({ ...linkRoute, locale: route.locale }, route.target, versions.scopes);
+ return [{ ...entry, href, isCurrent: samePath(href, pathname) }];
}
+
const rewrittenEntries = rewriteSidebar(entry.entries);
- return {
- ...entry,
- entries: rewrittenEntries,
- };
+ if (rewrittenEntries.length === 0) return [];
+ return [{ ...entry, entries: rewrittenEntries }];
});
}
-function previewHref(href) {
- if (!href.startsWith('/docs/') || href.startsWith('/docs/preview/')) return href;
- return href === '/docs/' ? '/docs/preview/' : href.replace(/^\/docs\//, '/docs/preview/');
-}
-
function samePath(left, right) {
return stripTrailingSlash(left) === stripTrailingSlash(right);
}
@@ -68,7 +45,7 @@ function stripTrailingSlash(path) {
---
<SidebarPersister>
- <SidebarSublist sublist={channelSidebar} />
+ <SidebarSublist sublist={scopedSidebar} />
</SidebarPersister>
<div class="md:sl-hidden">
diff --git a/website/src/components/SiteTitle.astro b/website/src/components/SiteTitle.astro
index 576e7635..3e5af0a3 100644
--- a/website/src/components/SiteTitle.astro
+++ b/website/src/components/SiteTitle.astro
@@ -1,8 +1,11 @@
---
+import { docsRoute } from '../docs-path';
+
const { siteTitle, siteTitleHref, locale } = Astro.locals.starlightRoute;
-const isPreview = Astro.url.pathname === '/docs/preview/' || Astro.url.pathname.startsWith('/docs/preview/');
+const route = docsRoute(Astro.url.pathname);
const localePrefix = locale ? `/${locale}` : '';
-const docsHref = isPreview ? '/docs/preview/' : `${localePrefix}/docs/`;
+const targetSegment = route.target === 'stable' ? '' : `/${route.target}`;
+const docsHref = `${localePrefix}/docs${targetSegment}/`;
---
<div class="title-row sl-flex">
diff --git a/website/src/docs-path.test.ts b/website/src/docs-path.test.ts
index 26f9a701..0fadb26e 100644
--- a/website/src/docs-path.test.ts
+++ b/website/src/docs-path.test.ts
@@ -1,16 +1,44 @@
import { describe, expect, test } from 'bun:test';
-import { docsChannel, docsPath } from './docs-path';
+import { docsPath, docsRoute, docsTargetHref, docsVersion } from './docs-path';
-describe('docsChannel', () => {
+const scopes = {
+ stable: { locales: { root: ['', 'install'], ja: ['', 'install'] } },
+ preview: { locales: { root: ['', 'install', 'new-page'], ja: ['', 'install'] } },
+ '0.7.4': { locales: { root: ['', 'install'], ja: [''] } },
+};
+
+describe('docsRoute', () => {
+ test.each([
+ ['/docs/', { isDocs: true, locale: undefined, target: 'stable', page: '' }],
+ ['/ja/docs/install/', { isDocs: true, locale: 'ja', target: 'stable', page: 'install' }],
+ ['/docs/preview/new-page/', { isDocs: true, locale: undefined, target: 'preview', page: 'new-page' }],
+ ['/zh-cn/docs/preview/', { isDocs: true, locale: 'zh-cn', target: 'preview', page: '' }],
+ ['/docs/0.7.4/install/', { isDocs: true, locale: undefined, target: '0.7.4', page: 'install' }],
+ ['/ja/docs/0.7.4/', { isDocs: true, locale: 'ja', target: '0.7.4', page: '' }],
+ ['/blog/', { isDocs: false, locale: undefined, target: 'stable', page: '' }],
+ ])('parses %s', (pathname, expected) => {
+ expect(docsRoute(pathname)).toEqual(expected);
+ });
+});
+
+describe('docsVersion', () => {
test.each([
['/docs/', 'stable'],
- ['/ja/docs/install/', 'stable'],
- ['/zh-cn/docs/', 'stable'],
- ['/docs/preview/', 'preview'],
['/ja/docs/preview/install/', 'preview'],
- ['/zh-cn/docs/preview/', 'preview'],
- ])('maps %s to %s', (pathname, expected) => {
- expect(docsChannel(pathname)).toBe(expected);
+ ['/zh-cn/docs/0.7.4/', '0.7.4'],
+ ])('maps %s', (pathname, version) => {
+ expect(docsVersion(pathname)).toBe(version);
+ });
+});
+
+describe('docsTargetHref', () => {
+ test.each([
+ ['/docs/install/', 'preview', '/docs/preview/install/'],
+ ['/ja/docs/install/', '0.7.4', '/ja/docs/0.7.4/'],
+ ['/docs/0.7.4/install/', 'stable', '/docs/install/'],
+ ['/docs/preview/new-page/', '0.7.4', '/docs/0.7.4/'],
+ ])('maps %s to %s', (pathname, target, expected) => {
+ expect(docsTargetHref(docsRoute(pathname), target, scopes)).toBe(expected);
});
});
@@ -25,7 +53,10 @@ describe('docsPath', () => {
['preview/install.mdx', 'docs/preview/install'],
['preview/ja/index.mdx', 'ja/docs/preview'],
['preview/ja/install.mdx', 'ja/docs/preview/install'],
- ['preview/zh-cn/install.mdx', 'zh-cn/docs/preview/install'],
+ ['_versions/0.7.4/index.mdx', 'docs/0.7.4'],
+ ['_versions/0.7.4/install.mdx', 'docs/0.7.4/install'],
+ ['_versions/0.7.4/ja/index.mdx', 'ja/docs/0.7.4'],
+ ['_versions/0.7.4/zh-cn/install.mdx', 'zh-cn/docs/0.7.4/install'],
])('maps %s to %s', (entry, expected) => {
expect(docsPath({ entry })).toBe(expected);
});
diff --git a/website/src/docs-path.ts b/website/src/docs-path.ts
index be022a4b..53ace41d 100644
--- a/website/src/docs-path.ts
+++ b/website/src/docs-path.ts
@@ -1,20 +1,87 @@
const docsLocales = new Set(['ja', 'zh-cn']);
+const docsVersionPattern = /^\d+\.\d+\.\d+$/;
-export function docsChannel(pathname: string) {
- return /^\/(?:ja\/|zh-cn\/)?docs\/preview(?:\/|$)/.test(pathname) ? 'preview' : 'stable';
+export type DocsTarget = 'stable' | 'preview' | string;
+
+export interface DocsRoute {
+ isDocs: boolean;
+ locale: string | undefined;
+ target: DocsTarget;
+ page: string;
+}
+
+export interface DocsScope {
+ locales: Record<string, string[]>;
+}
+
+export function docsRoute(pathname: string): DocsRoute {
+ const segments = pathname.split('/').filter(Boolean);
+ let locale: string | undefined;
+
+ if (segments[0] && docsLocales.has(segments[0])) {
+ locale = segments.shift();
+ }
+
+ if (segments.shift() !== 'docs') {
+ return { isDocs: false, locale, target: 'stable', page: '' };
+ }
+
+ let target: DocsTarget = 'stable';
+ if (segments[0] === 'preview') {
+ target = 'preview';
+ segments.shift();
+ } else if (segments[0] && docsVersionPattern.test(segments[0])) {
+ target = segments.shift()!;
+ }
+
+ return {
+ isDocs: true,
+ locale,
+ target,
+ page: segments.join('/'),
+ };
+}
+
+export function docsVersion(pathname: string) {
+ return docsRoute(pathname).target;
+}
+
+export function docsTargetHref(
+ route: DocsRoute,
+ target: DocsTarget,
+ scopes?: Record<string, DocsScope>,
+) {
+ if (!route.isDocs) throw new Error('cannot build a documentation target from a non-docs route');
+
+ const localeKey = route.locale ?? 'root';
+ const availablePages = scopes?.[target]?.locales[localeKey];
+ const page = availablePages?.includes(route.page) === false ? '' : route.page;
+ const prefix = route.locale ? `/${route.locale}` : '';
+ const targetSegment = target === 'stable' ? '' : `/${target}`;
+ return `${prefix}/docs${targetSegment}${page ? `/${page}` : ''}/`;
}
export function docsPath({ entry }: { entry: string }) {
const slug = entry.replace(/\.(md|mdx|markdown|mdown|mkdn|mkd|mdwn)$/i, '');
const normalized = slug.replace(/\/index$/, '');
const segments = normalized.split('/');
- const localeIndex = segments[0] === 'preview' ? 1 : 0;
- const locale = segments[localeIndex];
- if (locale && docsLocales.has(locale)) {
- segments.splice(localeIndex, 1);
- return `${locale}/docs${segments.length > 0 ? `/${segments.join('/')}` : ''}`;
+ let targetSegment = '';
+ if (segments[0] === 'preview') {
+ targetSegment = 'preview';
+ segments.shift();
+ } else if (segments[0] === '_versions' && segments[1] && docsVersionPattern.test(segments[1])) {
+ segments.shift();
+ targetSegment = segments.shift()!;
+ }
+
+ let locale: string | undefined;
+ if (segments[0] && docsLocales.has(segments[0])) {
+ locale = segments.shift();
}
- return normalized === 'index' ? 'docs' : `docs/${normalized}`;
+ const page = segments.join('/');
+ const prefix = locale ? `${locale}/` : '';
+ const target = targetSegment ? `/${targetSegment}` : '';
+ return `${prefix}docs${target}${page && page !== 'index' ? `/${page}` : ''}`;
}