mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-05-26 00:55:16 -07:00
[codex] add versioned Pages deployment (#73)
This commit is contained in:
+210
-66
@@ -1,3 +1,7 @@
|
||||
import { existsSync } from 'node:fs';
|
||||
import { join } from 'node:path';
|
||||
import type { DefaultTheme, HeadConfig, TransformContext, UserConfig } from 'vitepress';
|
||||
|
||||
const DOCS_HOSTNAME = 'https://docs.subminer.moe';
|
||||
const PLAUSIBLE_PROXY_HOSTNAME = 'https://worker.sudacode.com';
|
||||
const PLAUSIBLE_SITE_SCRIPT_PATH = '/js/pa-h28Pn9ppgTJRmiSJlyPT6.js';
|
||||
@@ -7,20 +11,212 @@ const PLAUSIBLE_INIT_SCRIPT = [
|
||||
`plausible.init({ endpoint: '${PLAUSIBLE_ENDPOINT}' });`,
|
||||
].join('\n');
|
||||
|
||||
function pageToCanonicalHref(page: string): string | null {
|
||||
type DocsChannel = 'stable-root' | 'stable-archive' | 'main';
|
||||
|
||||
type VersionManifest = {
|
||||
latestStable: string;
|
||||
channels: Array<{ label: string; path: string }>;
|
||||
versions: Array<{ version: string; path: string }>;
|
||||
};
|
||||
|
||||
const base = normalizeBase(process.env.SUBMINER_DOCS_BASE ?? '/');
|
||||
const outDir = process.env.SUBMINER_DOCS_OUT_DIR;
|
||||
const channel = normalizeChannel(process.env.SUBMINER_DOCS_CHANNEL);
|
||||
const docsVersion = process.env.SUBMINER_DOCS_VERSION;
|
||||
const latestStable = process.env.SUBMINER_DOCS_LATEST_STABLE ?? 'v0.14.0';
|
||||
const versionManifest = parseVersionManifest(process.env.SUBMINER_DOCS_VERSION_MANIFEST);
|
||||
|
||||
function normalizeBase(value: string): string {
|
||||
if (!value || value === '/') return '/';
|
||||
return `/${value.replace(/^\/+|\/+$/g, '')}/`;
|
||||
}
|
||||
|
||||
function normalizeChannel(value: string | undefined): DocsChannel {
|
||||
if (value === 'main' || value === 'stable-archive') return value;
|
||||
return 'stable-root';
|
||||
}
|
||||
|
||||
function parseVersionManifest(value: string | undefined): VersionManifest {
|
||||
if (!value) {
|
||||
return {
|
||||
latestStable,
|
||||
channels: [
|
||||
{ label: 'Latest stable', path: '/' },
|
||||
{ label: 'main', path: '/main/' },
|
||||
],
|
||||
versions: [{ version: latestStable, path: `/v/${latestStable.replace(/^v/, '')}/` }],
|
||||
};
|
||||
}
|
||||
|
||||
return JSON.parse(value) as VersionManifest;
|
||||
}
|
||||
|
||||
function withDocsBase(path: string): string {
|
||||
if (/^[a-z]+:\/\//i.test(path)) return path;
|
||||
const normalizedPath = path.startsWith('/') ? path : `/${path}`;
|
||||
if (base === '/') return normalizedPath;
|
||||
return `${base.replace(/\/$/, '')}${normalizedPath}`;
|
||||
}
|
||||
|
||||
function pageToRoute(page: string): string | null {
|
||||
if (page === '404.md') return null;
|
||||
|
||||
const route = page
|
||||
.replace(/(^|\/)index\.md$/, '')
|
||||
.replace(/\.md$/, '')
|
||||
.replace(/\/$/, '');
|
||||
return route ? `${DOCS_HOSTNAME}/${route}` : `${DOCS_HOSTNAME}/`;
|
||||
return route ? `/${route}` : '/';
|
||||
}
|
||||
|
||||
export default {
|
||||
function pageToCanonicalHref(page: string): string | null {
|
||||
const route = pageToRoute(page);
|
||||
if (!route) return null;
|
||||
|
||||
if (channel === 'main') {
|
||||
return `${DOCS_HOSTNAME}${canonicalRouteWithBase(route)}`;
|
||||
}
|
||||
|
||||
if (channel === 'stable-archive' && docsVersion !== latestStable) {
|
||||
return `${DOCS_HOSTNAME}${canonicalRouteWithBase(route)}`;
|
||||
}
|
||||
|
||||
return route === '/' ? `${DOCS_HOSTNAME}/` : `${DOCS_HOSTNAME}${route}`;
|
||||
}
|
||||
|
||||
function canonicalRouteWithBase(route: string): string {
|
||||
const routeWithBase = withDocsBase(route);
|
||||
return route === '/' ? routeWithBase : routeWithBase.replace(/\/$/, '');
|
||||
}
|
||||
|
||||
function transformPageHead({ page }: TransformContext): HeadConfig[] {
|
||||
const href = pageToCanonicalHref(page);
|
||||
const head: HeadConfig[] = href ? [['link', { rel: 'canonical', href }]] : [];
|
||||
|
||||
if (channel === 'main') {
|
||||
head.push(['meta', { name: 'robots', content: 'noindex,follow' }]);
|
||||
}
|
||||
|
||||
return head;
|
||||
}
|
||||
|
||||
function linkToPagePath(link: string): string | null {
|
||||
if (!link.startsWith('/') || link.startsWith('/v/') || link.startsWith('/main/')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const withoutHash = link.split('#')[0] ?? '/';
|
||||
const withoutQuery = withoutHash.split('?')[0] ?? '/';
|
||||
const route = withoutQuery.replace(/^\/+|\/+$/g, '');
|
||||
return route ? `${route}.md` : 'index.md';
|
||||
}
|
||||
|
||||
function hasPageForLink(link: string): boolean {
|
||||
const pagePath = linkToPagePath(link);
|
||||
if (!pagePath) return true;
|
||||
return existsSync(join(process.cwd(), pagePath));
|
||||
}
|
||||
|
||||
function filterNav(items: DefaultTheme.NavItem[]): DefaultTheme.NavItem[] {
|
||||
return items
|
||||
.map((item) => {
|
||||
if ('items' in item && item.items) {
|
||||
return { ...item, items: filterNav(item.items as DefaultTheme.NavItem[]) };
|
||||
}
|
||||
if ('link' in item && item.link && !hasPageForLink(item.link)) {
|
||||
return null;
|
||||
}
|
||||
return item;
|
||||
})
|
||||
.filter((item): item is DefaultTheme.NavItem => Boolean(item));
|
||||
}
|
||||
|
||||
function filterSidebar(items: DefaultTheme.SidebarItem[]): DefaultTheme.SidebarItem[] {
|
||||
return items
|
||||
.map((item) => {
|
||||
const filteredChildren = item.items ? filterSidebar(item.items) : undefined;
|
||||
if (item.link && !hasPageForLink(item.link)) return null;
|
||||
if (item.items && filteredChildren?.length === 0 && !item.link) return null;
|
||||
return { ...item, items: filteredChildren };
|
||||
})
|
||||
.filter((item): item is DefaultTheme.SidebarItem => Boolean(item));
|
||||
}
|
||||
|
||||
const versionItems = [
|
||||
{
|
||||
text: `Latest stable (${versionManifest.latestStable})`,
|
||||
link: '/',
|
||||
},
|
||||
...versionManifest.channels
|
||||
.filter((entry) => entry.label !== 'Latest stable')
|
||||
.map((entry) => ({ text: entry.label, link: entry.path })),
|
||||
...versionManifest.versions.map((entry) => ({
|
||||
text: entry.version,
|
||||
link: entry.path,
|
||||
})),
|
||||
];
|
||||
|
||||
const nav: DefaultTheme.NavItem[] = [
|
||||
{ text: 'Home', link: '/' },
|
||||
{ text: 'Get Started', link: '/installation' },
|
||||
{ text: 'Mining', link: '/mining-workflow' },
|
||||
{ text: 'Configuration', link: '/configuration' },
|
||||
{ text: 'Changelog', link: '/changelog' },
|
||||
{ text: 'Troubleshooting', link: '/troubleshooting' },
|
||||
{ text: docsVersion ?? (channel === 'main' ? 'main' : latestStable), items: versionItems },
|
||||
];
|
||||
|
||||
const sidebar: DefaultTheme.SidebarItem[] = [
|
||||
{
|
||||
text: 'Getting Started',
|
||||
items: [
|
||||
{ text: 'Overview', link: '/' },
|
||||
{ text: 'Installation', link: '/installation' },
|
||||
{ text: 'Usage', link: '/usage' },
|
||||
{ text: 'Mining Workflow', link: '/mining-workflow' },
|
||||
{ text: 'Launcher Script', link: '/launcher-script' },
|
||||
],
|
||||
},
|
||||
{
|
||||
text: 'Reference',
|
||||
items: [
|
||||
{ text: 'Configuration', link: '/configuration' },
|
||||
{ text: 'Keyboard Shortcuts', link: '/shortcuts' },
|
||||
{ text: 'Subtitle Annotations', link: '/subtitle-annotations' },
|
||||
{ text: 'Subtitle Sidebar', link: '/subtitle-sidebar' },
|
||||
{ text: 'Immersion Tracking', link: '/immersion-tracking' },
|
||||
{ text: 'Troubleshooting', link: '/troubleshooting' },
|
||||
],
|
||||
},
|
||||
{
|
||||
text: 'Integrations',
|
||||
items: [
|
||||
{ text: 'MPV Plugin', link: '/mpv-plugin' },
|
||||
{ text: 'Anki', link: '/anki-integration' },
|
||||
{ text: 'Jellyfin', link: '/jellyfin-integration' },
|
||||
{ text: 'YouTube', link: '/youtube-integration' },
|
||||
{ text: 'Jimaku', link: '/jimaku-integration' },
|
||||
{ text: 'AniList', link: '/anilist-integration' },
|
||||
{ text: 'Character Dictionary', link: '/character-dictionary' },
|
||||
],
|
||||
},
|
||||
{
|
||||
text: 'Development',
|
||||
items: [
|
||||
{ text: 'Building & Testing', link: '/development' },
|
||||
{ text: 'Architecture', link: '/architecture' },
|
||||
{ text: 'IPC + Runtime Contracts', link: '/ipc-contracts' },
|
||||
{ text: 'WebSocket + Texthooker API', link: '/websocket-texthooker-api' },
|
||||
{ text: 'Changelog', link: '/changelog' },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const config: UserConfig = {
|
||||
title: 'SubMiner Docs',
|
||||
description:
|
||||
'SubMiner: an MPV immersion-mining overlay with Yomitan and AnkiConnect integration.',
|
||||
base,
|
||||
...(outDir ? { outDir } : {}),
|
||||
head: [
|
||||
['link', { rel: 'preconnect', href: PLAUSIBLE_PROXY_HOSTNAME }],
|
||||
[
|
||||
@@ -31,13 +227,13 @@ export default {
|
||||
},
|
||||
],
|
||||
['script', {}, PLAUSIBLE_INIT_SCRIPT],
|
||||
['link', { rel: 'icon', href: '/favicon.ico', sizes: 'any' }],
|
||||
['link', { rel: 'icon', href: withDocsBase('/favicon.ico'), sizes: 'any' }],
|
||||
[
|
||||
'link',
|
||||
{
|
||||
rel: 'icon',
|
||||
type: 'image/png',
|
||||
href: '/favicon-32x32.png',
|
||||
href: withDocsBase('/favicon-32x32.png'),
|
||||
sizes: '32x32',
|
||||
},
|
||||
],
|
||||
@@ -46,7 +242,7 @@ export default {
|
||||
{
|
||||
rel: 'icon',
|
||||
type: 'image/png',
|
||||
href: '/favicon-16x16.png',
|
||||
href: withDocsBase('/favicon-16x16.png'),
|
||||
sizes: '16x16',
|
||||
},
|
||||
],
|
||||
@@ -54,7 +250,7 @@ export default {
|
||||
'link',
|
||||
{
|
||||
rel: 'apple-touch-icon',
|
||||
href: '/apple-touch-icon.png',
|
||||
href: withDocsBase('/apple-touch-icon.png'),
|
||||
sizes: '180x180',
|
||||
},
|
||||
],
|
||||
@@ -70,10 +266,7 @@ export default {
|
||||
);
|
||||
},
|
||||
},
|
||||
transformHead({ page }) {
|
||||
const href = pageToCanonicalHref(page);
|
||||
return href ? [['link', { rel: 'canonical', href }]] : [];
|
||||
},
|
||||
transformHead: transformPageHead,
|
||||
lastUpdated: true,
|
||||
srcExclude: ['subagents/**'],
|
||||
markdown: {
|
||||
@@ -84,63 +277,12 @@ export default {
|
||||
},
|
||||
themeConfig: {
|
||||
logo: {
|
||||
light: '/assets/SubMiner.png',
|
||||
dark: '/assets/SubMiner.png',
|
||||
light: withDocsBase('/assets/SubMiner.png'),
|
||||
dark: withDocsBase('/assets/SubMiner.png'),
|
||||
},
|
||||
siteTitle: 'SubMiner Docs',
|
||||
nav: [
|
||||
{ text: 'Home', link: '/' },
|
||||
{ text: 'Get Started', link: '/installation' },
|
||||
{ text: 'Mining', link: '/mining-workflow' },
|
||||
{ text: 'Configuration', link: '/configuration' },
|
||||
{ text: 'Changelog', link: '/changelog' },
|
||||
{ text: 'Troubleshooting', link: '/troubleshooting' },
|
||||
],
|
||||
sidebar: [
|
||||
{
|
||||
text: 'Getting Started',
|
||||
items: [
|
||||
{ text: 'Overview', link: '/' },
|
||||
{ text: 'Installation', link: '/installation' },
|
||||
{ text: 'Usage', link: '/usage' },
|
||||
{ text: 'Mining Workflow', link: '/mining-workflow' },
|
||||
{ text: 'Launcher Script', link: '/launcher-script' },
|
||||
],
|
||||
},
|
||||
{
|
||||
text: 'Reference',
|
||||
items: [
|
||||
{ text: 'Configuration', link: '/configuration' },
|
||||
{ text: 'Keyboard Shortcuts', link: '/shortcuts' },
|
||||
{ text: 'Subtitle Annotations', link: '/subtitle-annotations' },
|
||||
{ text: 'Subtitle Sidebar', link: '/subtitle-sidebar' },
|
||||
{ text: 'Immersion Tracking', link: '/immersion-tracking' },
|
||||
{ text: 'Troubleshooting', link: '/troubleshooting' },
|
||||
],
|
||||
},
|
||||
{
|
||||
text: 'Integrations',
|
||||
items: [
|
||||
{ text: 'MPV Plugin', link: '/mpv-plugin' },
|
||||
{ text: 'Anki', link: '/anki-integration' },
|
||||
{ text: 'Jellyfin', link: '/jellyfin-integration' },
|
||||
{ text: 'YouTube', link: '/youtube-integration' },
|
||||
{ text: 'Jimaku', link: '/jimaku-integration' },
|
||||
{ text: 'AniList', link: '/anilist-integration' },
|
||||
{ text: 'Character Dictionary', link: '/character-dictionary' },
|
||||
],
|
||||
},
|
||||
{
|
||||
text: 'Development',
|
||||
items: [
|
||||
{ text: 'Building & Testing', link: '/development' },
|
||||
{ text: 'Architecture', link: '/architecture' },
|
||||
{ text: 'IPC + Runtime Contracts', link: '/ipc-contracts' },
|
||||
{ text: 'WebSocket + Texthooker API', link: '/websocket-texthooker-api' },
|
||||
{ text: 'Changelog', link: '/changelog' },
|
||||
],
|
||||
},
|
||||
],
|
||||
nav: filterNav(nav),
|
||||
sidebar: filterSidebar(sidebar),
|
||||
search: {
|
||||
provider: 'local',
|
||||
},
|
||||
@@ -159,3 +301,5 @@ export default {
|
||||
socialLinks: [{ icon: 'github', link: 'https://github.com/ksyasuda/SubMiner' }],
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
|
||||
Reference in New Issue
Block a user