docs-i18n 0.6.2 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/{src/admin/ui → admin/app}/components/JobDialog.tsx +21 -2
- package/{src/admin/ui → admin/app}/components/JobPanel.tsx +1 -1
- package/{src/admin/ui → admin/app}/components/Preview.tsx +2 -5
- package/{src/admin/ui → admin/app}/lib/api.ts +18 -39
- package/admin/app/routeTree.gen.ts +68 -0
- package/admin/app/router.tsx +23 -0
- package/admin/app/routes/__root.tsx +55 -0
- package/admin/app/routes/index.tsx +416 -0
- package/{src/admin/ui → admin/app}/styles.css +36 -3
- package/admin/package.json +27 -0
- package/admin/server/functions/jobs.ts +53 -0
- package/admin/server/functions/misc.ts +84 -0
- package/{src/admin/server/routes → admin/server/functions}/models.ts +16 -29
- package/admin/server/functions/status.ts +61 -0
- package/admin/server/index.ts +35 -0
- package/admin/server/init.ts +46 -0
- package/{src/admin → admin}/server/services/job-manager.ts +39 -10
- package/{src/admin → admin}/server/services/status.ts +6 -6
- package/admin/tsconfig.json +19 -0
- package/{src/admin → admin}/vite.config.ts +8 -2
- package/dist/{assemble-7H4QCW35.js → assemble-CP2BRYQJ.js} +6 -4
- package/dist/{chunk-A3YQNPKZ.js → chunk-CLYUAWZE.js} +1 -1
- package/dist/{chunk-YN4VJHCQ.js → chunk-JHBSHTXC.js} +1 -1
- package/dist/chunk-L64GJ4OB.js +32 -0
- package/dist/{chunk-SKKZIV3L.js → chunk-PNKVD2UK.js} +1 -29
- package/dist/{chunk-XEOYZUHS.js → chunk-QKIR7RKQ.js} +4 -31
- package/dist/chunk-TRURQFP4.js +31 -0
- package/dist/cli.js +108 -23
- package/dist/index.d.ts +41 -1
- package/dist/index.js +92 -3
- package/dist/{rescan-O5D3CYC2.js → rescan-HXMWFAOC.js} +5 -3
- package/dist/{status-F4MYIAAY.js → status-AGZDXOTZ.js} +4 -2
- package/dist/{translate-ZIVKNAC4.js → translate-A5X6MX4Y.js} +14 -7
- package/dist/upload-XL6KG6S2.js +132 -0
- package/package.json +17 -15
- package/template/app/components/BlogArticle.tsx +159 -0
- package/template/app/components/BlogList.tsx +88 -0
- package/template/app/components/Breadcrumbs.tsx +81 -0
- package/template/app/components/Card.tsx +31 -0
- package/template/app/components/Doc.tsx +191 -0
- package/template/app/components/DocBreadcrumb.tsx +60 -0
- package/template/app/components/DocContainer.tsx +13 -0
- package/template/app/components/DocTitle.tsx +11 -0
- package/template/app/components/DocsLayout.tsx +715 -0
- package/template/app/components/Dropdown.tsx +116 -0
- package/template/app/components/FallbackBanner.tsx +36 -0
- package/template/app/components/Footer.tsx +29 -0
- package/template/app/components/FrameworkSelect.tsx +150 -0
- package/template/app/components/LibraryCard.tsx +178 -0
- package/template/app/components/LocaleSwitcher.tsx +43 -0
- package/template/app/components/Navbar.tsx +430 -0
- package/template/app/components/PostNotFound.tsx +20 -0
- package/template/app/components/SearchButton.tsx +32 -0
- package/template/app/components/Select.tsx +103 -0
- package/template/app/components/Spinner.tsx +18 -0
- package/template/app/components/ThemeProvider.tsx +141 -0
- package/template/app/components/ThemeToggle.tsx +31 -0
- package/template/app/components/Toc.tsx +86 -0
- package/template/app/components/VersionSelect.tsx +118 -0
- package/template/app/components/icons/BSkyIcon.tsx +27 -0
- package/template/app/components/icons/BaseballCapIcon.tsx +25 -0
- package/template/app/components/icons/BrandXIcon.tsx +28 -0
- package/template/app/components/icons/CheckCircleIcon.tsx +28 -0
- package/template/app/components/icons/CogsIcon.tsx +25 -0
- package/template/app/components/icons/DiscordIcon.tsx +24 -0
- package/template/app/components/icons/GithubIcon.tsx +24 -0
- package/template/app/components/icons/GoogleIcon.tsx +24 -0
- package/template/app/components/icons/InstagramIcon.tsx +24 -0
- package/template/app/components/icons/NpmIcon.tsx +26 -0
- package/template/app/components/icons/YinYangIcon.tsx +26 -0
- package/template/app/components/icons/YouTubeIcon.tsx +24 -0
- package/template/app/components/markdown/CodeBlock.tsx +254 -0
- package/template/app/components/markdown/FileTabs.tsx +58 -0
- package/template/app/components/markdown/FrameworkContent.tsx +76 -0
- package/template/app/components/markdown/Markdown.tsx +216 -0
- package/template/app/components/markdown/MarkdownContent.tsx +89 -0
- package/template/app/components/markdown/MarkdownFrameworkHandler.tsx +66 -0
- package/template/app/components/markdown/MarkdownHeadingContext.tsx +35 -0
- package/template/app/components/markdown/MarkdownLink.tsx +46 -0
- package/template/app/components/markdown/MarkdownTabsHandler.tsx +109 -0
- package/template/app/components/markdown/PackageManagerTabs.tsx +95 -0
- package/template/app/components/markdown/Tabs.tsx +139 -0
- package/template/app/components/markdown/index.ts +15 -0
- package/template/app/components/ui/Button.tsx +141 -0
- package/template/app/components/ui/InlineCode.tsx +16 -0
- package/template/app/components/ui/MarkdownImg.tsx +21 -0
- package/template/app/config/frameworks.ts +93 -0
- package/template/app/contexts/SearchContext.tsx +36 -0
- package/template/app/db/index.ts +17 -0
- package/template/app/db/schema.ts +74 -0
- package/template/app/hooks/useClickOutside.ts +106 -0
- package/template/app/routeTree.gen.ts +584 -0
- package/template/app/router.tsx +29 -0
- package/template/app/routes/$lang.$project.$version.docs.$.tsx +128 -0
- package/template/app/routes/$lang.$project.$version.docs.framework.$framework.$.tsx +106 -0
- package/template/app/routes/$lang.$project.$version.docs.framework.$framework.index.tsx +27 -0
- package/template/app/routes/$lang.$project.$version.docs.framework.index.tsx +44 -0
- package/template/app/routes/$lang.$project.$version.docs.index.tsx +27 -0
- package/template/app/routes/$lang.$project.$version.docs.tsx +70 -0
- package/template/app/routes/$lang.$project.$version.tsx +69 -0
- package/template/app/routes/$lang.$project.docs.$.tsx +104 -0
- package/template/app/routes/$lang.$project.docs.index.tsx +20 -0
- package/template/app/routes/$lang.$project.docs.tsx +79 -0
- package/template/app/routes/$lang.$project.tsx +89 -0
- package/template/app/routes/$lang.blog.$.tsx +82 -0
- package/template/app/routes/$lang.blog.index.tsx +56 -0
- package/template/app/routes/$lang.blog.tsx +26 -0
- package/template/app/routes/$lang.docs.$.tsx +100 -0
- package/template/app/routes/$lang.docs.framework.$framework.$.tsx +104 -0
- package/template/app/routes/$lang.docs.framework.$framework.index.tsx +32 -0
- package/template/app/routes/$lang.docs.framework.index.tsx +47 -0
- package/template/app/routes/$lang.docs.index.tsx +20 -0
- package/template/app/routes/$lang.docs.tsx +90 -0
- package/template/app/routes/$lang.tsx +16 -0
- package/template/app/routes/__root.tsx +180 -0
- package/template/app/routes/index.tsx +89 -0
- package/template/app/site.config.ts +182 -0
- package/template/app/styles/app.css +1029 -0
- package/template/app/types/index.ts +77 -0
- package/template/app/utils/blog.server.ts +193 -0
- package/template/app/utils/blog.ts +42 -0
- package/template/app/utils/config.ts +120 -0
- package/template/app/utils/content-loader.ts +400 -0
- package/template/app/utils/dates.ts +29 -0
- package/template/app/utils/docs.server.ts +150 -0
- package/template/app/utils/markdown/filterFrameworkContent.ts +233 -0
- package/template/app/utils/markdown/index.ts +2 -0
- package/template/app/utils/markdown/installCommand.ts +143 -0
- package/template/app/utils/markdown/plugins/collectHeadings.ts +104 -0
- package/template/app/utils/markdown/plugins/extractCodeMeta.ts +57 -0
- package/template/app/utils/markdown/plugins/helpers.ts +33 -0
- package/template/app/utils/markdown/plugins/index.ts +8 -0
- package/template/app/utils/markdown/plugins/parseCommentComponents.ts +103 -0
- package/template/app/utils/markdown/plugins/transformCommentComponents.ts +23 -0
- package/template/app/utils/markdown/plugins/transformFrameworkComponent.ts +217 -0
- package/template/app/utils/markdown/plugins/transformTabsComponent.ts +359 -0
- package/template/app/utils/markdown/processor.ts +75 -0
- package/template/app/utils/site-config.tsx +11 -0
- package/template/app/utils/upload.ts +232 -0
- package/template/app/utils/useLocalStorage.ts +65 -0
- package/template/app/utils/utils.ts +23 -0
- package/template/package.json +54 -0
- package/template/public/favicon.svg +1 -0
- package/template/public/fonts/Inter-latin-ext.woff2 +0 -0
- package/template/public/fonts/Inter-latin.woff2 +0 -0
- package/template/public/images/frameworks/angular-logo.svg +1 -0
- package/template/public/images/frameworks/js-logo.svg +1 -0
- package/template/public/images/frameworks/lit-logo.svg +1 -0
- package/template/public/images/frameworks/preact-logo.svg +6 -0
- package/template/public/images/frameworks/qwik-logo.svg +1 -0
- package/template/public/images/frameworks/react-logo.svg +1 -0
- package/template/public/images/frameworks/solid-logo.svg +1 -0
- package/template/public/images/frameworks/svelte-logo.svg +1 -0
- package/template/public/images/frameworks/vue-logo.svg +4 -0
- package/template/tsconfig.json +24 -0
- package/template/vite.config.ts +43 -0
- package/template/wrangler.jsonc +16 -0
- package/README.md +0 -161
- package/dist/server-73AVSOL5.js +0 -598
- package/src/admin/index.html +0 -13
- package/src/admin/server/index.ts +0 -138
- package/src/admin/server/routes/jobs.ts +0 -113
- package/src/admin/server/routes/status.ts +0 -57
- package/src/admin/ui/App.tsx +0 -332
- package/src/admin/ui/main.tsx +0 -19
- /package/{src/admin/ui → admin/app}/components/FileList.tsx +0 -0
- /package/{src/admin/ui → admin/app}/components/LangGrid.tsx +0 -0
- /package/{src/admin/ui → admin/app}/components/ProgressBar.tsx +0 -0
- /package/{src/admin/ui → admin/app}/lib/flags.ts +0 -0
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
export interface SiteConfig {
|
|
2
|
+
name: string
|
|
3
|
+
description: string
|
|
4
|
+
repo: string
|
|
5
|
+
defaultLocale: string
|
|
6
|
+
supportedLocales: Record<string, string>
|
|
7
|
+
defaultProject?: string
|
|
8
|
+
projects: ProjectConfig[]
|
|
9
|
+
features: {
|
|
10
|
+
frameworkSelector: boolean
|
|
11
|
+
versionSelector: boolean
|
|
12
|
+
editOnGithub: boolean
|
|
13
|
+
}
|
|
14
|
+
/** When true and viewing the latest version, omit the version segment from URLs */
|
|
15
|
+
hideLatestVersion?: boolean
|
|
16
|
+
components?: Record<string, () => Promise<{ default: React.ComponentType<any> }>>
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface ProjectConfig {
|
|
20
|
+
id: string
|
|
21
|
+
name: string
|
|
22
|
+
type: 'doc' | 'blog'
|
|
23
|
+
repo: string
|
|
24
|
+
latestVersion: string
|
|
25
|
+
latestBranch: string
|
|
26
|
+
availableVersions: string[]
|
|
27
|
+
docsRoot?: string
|
|
28
|
+
defaultDocs?: string
|
|
29
|
+
frameworks?: string[]
|
|
30
|
+
colorFrom: string
|
|
31
|
+
colorTo: string
|
|
32
|
+
textColor?: string
|
|
33
|
+
bgStyle?: string
|
|
34
|
+
borderStyle?: string
|
|
35
|
+
textStyle?: string
|
|
36
|
+
badge?: string
|
|
37
|
+
tagline?: string
|
|
38
|
+
description?: string
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface MarkdownHeading {
|
|
42
|
+
id: string
|
|
43
|
+
level: number
|
|
44
|
+
text: string
|
|
45
|
+
framework?: string
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface DocsConfig {
|
|
49
|
+
sections: DocsConfigSection[]
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface DocsConfigSection {
|
|
53
|
+
label: string
|
|
54
|
+
children: DocsConfigItem[]
|
|
55
|
+
collapsible?: boolean
|
|
56
|
+
defaultCollapsed?: boolean
|
|
57
|
+
frameworks?: DocsConfigFramework[]
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export interface DocsConfigItem {
|
|
61
|
+
label: string
|
|
62
|
+
to: string
|
|
63
|
+
badge?: string
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export interface DocsConfigFramework {
|
|
67
|
+
label: string
|
|
68
|
+
children: DocsConfigItem[]
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export interface LoadedDoc {
|
|
72
|
+
content: string
|
|
73
|
+
meta: Record<string, any>
|
|
74
|
+
locale: string
|
|
75
|
+
isFallback: boolean
|
|
76
|
+
filePath: string
|
|
77
|
+
}
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Server-side blog content loading functions.
|
|
3
|
+
*
|
|
4
|
+
* Blog posts are stored as markdown files in content/blog/{lang}/{slug}.md
|
|
5
|
+
* with frontmatter: title, published, excerpt, authors, draft
|
|
6
|
+
*
|
|
7
|
+
* Uses the same filesystem loading pattern as docs.server.ts,
|
|
8
|
+
* with i18n fallback to English when a translation is missing.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { readFileSync, existsSync, readdirSync } from 'node:fs'
|
|
12
|
+
import { resolve, join } from 'node:path'
|
|
13
|
+
import matter from 'gray-matter'
|
|
14
|
+
|
|
15
|
+
// Re-export shared types and utilities from the non-server module
|
|
16
|
+
export type { BlogPostMeta, LoadedBlogPost } from './blog'
|
|
17
|
+
export { formatAuthors } from './blog'
|
|
18
|
+
|
|
19
|
+
import type { BlogPostMeta, LoadedBlogPost } from './blog'
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Get the project root directory.
|
|
23
|
+
* Same resolution as docs.server.ts.
|
|
24
|
+
*/
|
|
25
|
+
function getProjectRoot(): string {
|
|
26
|
+
if (process.env.DOCS_I18N_PROJECT_ROOT) {
|
|
27
|
+
return process.env.DOCS_I18N_PROJECT_ROOT
|
|
28
|
+
}
|
|
29
|
+
// Prefer cwd if it has a content directory (avoids stale temp file from another project)
|
|
30
|
+
const cwd = process.cwd()
|
|
31
|
+
if (existsSync(resolve(cwd, 'content'))) {
|
|
32
|
+
return cwd
|
|
33
|
+
}
|
|
34
|
+
try {
|
|
35
|
+
const tmpDir = process.env.TMPDIR || process.env.TEMP || '/tmp'
|
|
36
|
+
return readFileSync(resolve(tmpDir, 'docs-i18n-project-root'), 'utf-8').trim()
|
|
37
|
+
} catch {
|
|
38
|
+
// ignore
|
|
39
|
+
}
|
|
40
|
+
return cwd
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// formatAuthors is re-exported from ./blog above
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Load all published blog posts for a given locale, sorted newest first.
|
|
47
|
+
*
|
|
48
|
+
* Resolution:
|
|
49
|
+
* 1. Scan content/blog/en/ for all .md files (source of truth for available posts)
|
|
50
|
+
* 2. For each post, try content/blog/{lang}/{slug}.md first, fallback to en
|
|
51
|
+
* 3. Filter out drafts and future-dated posts
|
|
52
|
+
* 4. Sort by published date descending
|
|
53
|
+
*/
|
|
54
|
+
export function loadBlogPosts(lang: string): BlogPostMeta[] {
|
|
55
|
+
const root = getProjectRoot()
|
|
56
|
+
const enDir = resolve(root, 'content', 'blog', 'en')
|
|
57
|
+
|
|
58
|
+
if (!existsSync(enDir)) {
|
|
59
|
+
return []
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const now = new Date()
|
|
63
|
+
const posts: BlogPostMeta[] = []
|
|
64
|
+
|
|
65
|
+
try {
|
|
66
|
+
const files = readdirSync(enDir).filter((f) => f.endsWith('.md'))
|
|
67
|
+
|
|
68
|
+
for (const file of files) {
|
|
69
|
+
const slug = file.replace(/\.md$/, '')
|
|
70
|
+
const meta = loadPostMeta(root, lang, slug)
|
|
71
|
+
|
|
72
|
+
if (!meta) continue
|
|
73
|
+
// Filter drafts and future posts
|
|
74
|
+
if (meta.draft) continue
|
|
75
|
+
if (new Date(meta.published) > now) continue
|
|
76
|
+
|
|
77
|
+
posts.push(meta)
|
|
78
|
+
}
|
|
79
|
+
} catch {
|
|
80
|
+
// ignore read errors
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Sort newest first
|
|
84
|
+
posts.sort(
|
|
85
|
+
(a, b) =>
|
|
86
|
+
new Date(b.published).getTime() - new Date(a.published).getTime(),
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
return posts
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Load a single blog post by slug with i18n fallback.
|
|
94
|
+
*
|
|
95
|
+
* Resolution:
|
|
96
|
+
* 1. content/blog/{lang}/{slug}.md
|
|
97
|
+
* 2. content/blog/en/{slug}.md (fallback, isFallback = true)
|
|
98
|
+
*/
|
|
99
|
+
export function loadBlogPost(lang: string, slug: string): LoadedBlogPost | null {
|
|
100
|
+
const root = getProjectRoot()
|
|
101
|
+
|
|
102
|
+
// Try requested language
|
|
103
|
+
const langPath = resolve(root, 'content', 'blog', lang, `${slug}.md`)
|
|
104
|
+
if (existsSync(langPath)) {
|
|
105
|
+
return parsePostFile(langPath, slug, lang, false)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Fallback to English
|
|
109
|
+
if (lang !== 'en') {
|
|
110
|
+
const enPath = resolve(root, 'content', 'blog', 'en', `${slug}.md`)
|
|
111
|
+
if (existsSync(enPath)) {
|
|
112
|
+
return parsePostFile(enPath, slug, 'en', true)
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return null
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Load just the metadata for a blog post (used for listing).
|
|
121
|
+
*/
|
|
122
|
+
function loadPostMeta(
|
|
123
|
+
root: string,
|
|
124
|
+
lang: string,
|
|
125
|
+
slug: string,
|
|
126
|
+
): BlogPostMeta | null {
|
|
127
|
+
// Try requested language
|
|
128
|
+
const langPath = resolve(root, 'content', 'blog', lang, `${slug}.md`)
|
|
129
|
+
if (existsSync(langPath)) {
|
|
130
|
+
return parsePostMeta(langPath, slug)
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Fallback to English
|
|
134
|
+
if (lang !== 'en') {
|
|
135
|
+
const enPath = resolve(root, 'content', 'blog', 'en', `${slug}.md`)
|
|
136
|
+
if (existsSync(enPath)) {
|
|
137
|
+
return parsePostMeta(enPath, slug)
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return null
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function parsePostMeta(filePath: string, slug: string): BlogPostMeta | null {
|
|
145
|
+
try {
|
|
146
|
+
const raw = readFileSync(filePath, 'utf-8')
|
|
147
|
+
const { data, content } = matter(raw)
|
|
148
|
+
|
|
149
|
+
// Extract header image from first markdown image
|
|
150
|
+
const headerImageMatch = content.match(/!\[([^\]]*)\]\(([^)]+)\)/)
|
|
151
|
+
const headerImage = headerImageMatch ? headerImageMatch[2] : undefined
|
|
152
|
+
|
|
153
|
+
return {
|
|
154
|
+
slug,
|
|
155
|
+
title: (data.title as string) || slug,
|
|
156
|
+
published: (data.published as string) || '',
|
|
157
|
+
excerpt: (data.excerpt as string) || '',
|
|
158
|
+
authors: Array.isArray(data.authors) ? data.authors : [],
|
|
159
|
+
draft: data.draft === true,
|
|
160
|
+
headerImage,
|
|
161
|
+
}
|
|
162
|
+
} catch {
|
|
163
|
+
return null
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function parsePostFile(
|
|
168
|
+
filePath: string,
|
|
169
|
+
slug: string,
|
|
170
|
+
locale: string,
|
|
171
|
+
isFallback: boolean,
|
|
172
|
+
): LoadedBlogPost {
|
|
173
|
+
const raw = readFileSync(filePath, 'utf-8')
|
|
174
|
+
const { data, content } = matter(raw)
|
|
175
|
+
|
|
176
|
+
// Extract header image from first markdown image
|
|
177
|
+
const headerImageMatch = content.match(/!\[([^\]]*)\]\(([^)]+)\)/)
|
|
178
|
+
const headerImage = headerImageMatch ? headerImageMatch[2] : undefined
|
|
179
|
+
|
|
180
|
+
return {
|
|
181
|
+
slug,
|
|
182
|
+
title: (data.title as string) || slug,
|
|
183
|
+
published: (data.published as string) || '',
|
|
184
|
+
excerpt: (data.excerpt as string) || '',
|
|
185
|
+
authors: Array.isArray(data.authors) ? data.authors : [],
|
|
186
|
+
draft: data.draft === true,
|
|
187
|
+
headerImage,
|
|
188
|
+
content,
|
|
189
|
+
locale,
|
|
190
|
+
isFallback,
|
|
191
|
+
filePath,
|
|
192
|
+
}
|
|
193
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared blog utilities — safe for both client and server.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Intl-based author list formatter.
|
|
7
|
+
*/
|
|
8
|
+
const listJoiner = new Intl.ListFormat('en-US', {
|
|
9
|
+
style: 'long',
|
|
10
|
+
type: 'conjunction',
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
export function formatAuthors(authors: string[]): string {
|
|
14
|
+
if (!authors.length) return 'Unknown'
|
|
15
|
+
return listJoiner.format(authors)
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** Metadata parsed from blog post frontmatter */
|
|
19
|
+
export interface BlogPostMeta {
|
|
20
|
+
slug: string
|
|
21
|
+
title: string
|
|
22
|
+
published: string
|
|
23
|
+
excerpt: string
|
|
24
|
+
authors: string[]
|
|
25
|
+
draft?: boolean
|
|
26
|
+
headerImage?: string
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** A fully loaded blog post with content */
|
|
30
|
+
export interface LoadedBlogPost {
|
|
31
|
+
slug: string
|
|
32
|
+
title: string
|
|
33
|
+
published: string
|
|
34
|
+
excerpt: string
|
|
35
|
+
authors: string[]
|
|
36
|
+
draft?: boolean
|
|
37
|
+
headerImage?: string
|
|
38
|
+
content: string
|
|
39
|
+
locale: string
|
|
40
|
+
isFallback: boolean
|
|
41
|
+
filePath: string
|
|
42
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Config utilities for parsing and validating docs config.json files.
|
|
3
|
+
*
|
|
4
|
+
* Follows the same schema as tanstack.com's config.json:
|
|
5
|
+
* - sections: array of sidebar sections
|
|
6
|
+
* - Each section has label, children, optional frameworks, collapsible, defaultCollapsed
|
|
7
|
+
* - Children have label, to, optional badge
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { DocsConfig, DocsConfigSection, DocsConfigItem, DocsConfigFramework } from '~/types'
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Validate and normalize a raw config.json object into a typed DocsConfig.
|
|
14
|
+
* Returns null if the input is invalid.
|
|
15
|
+
*/
|
|
16
|
+
export function parseDocsConfig(raw: unknown): DocsConfig | null {
|
|
17
|
+
if (!raw || typeof raw !== 'object') return null
|
|
18
|
+
|
|
19
|
+
const obj = raw as Record<string, unknown>
|
|
20
|
+
if (!Array.isArray(obj.sections)) return null
|
|
21
|
+
|
|
22
|
+
const sections: DocsConfigSection[] = []
|
|
23
|
+
|
|
24
|
+
for (const section of obj.sections) {
|
|
25
|
+
if (!section || typeof section !== 'object') continue
|
|
26
|
+
const s = section as Record<string, unknown>
|
|
27
|
+
|
|
28
|
+
if (typeof s.label !== 'string') continue
|
|
29
|
+
if (!Array.isArray(s.children)) continue
|
|
30
|
+
|
|
31
|
+
const children = parseConfigItems(s.children)
|
|
32
|
+
const frameworks = s.frameworks ? parseFrameworks(s.frameworks) : undefined
|
|
33
|
+
|
|
34
|
+
sections.push({
|
|
35
|
+
label: s.label,
|
|
36
|
+
children,
|
|
37
|
+
collapsible: typeof s.collapsible === 'boolean' ? s.collapsible : undefined,
|
|
38
|
+
defaultCollapsed: typeof s.defaultCollapsed === 'boolean' ? s.defaultCollapsed : undefined,
|
|
39
|
+
frameworks,
|
|
40
|
+
})
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return { sections }
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function parseConfigItems(items: unknown[]): DocsConfigItem[] {
|
|
47
|
+
const result: DocsConfigItem[] = []
|
|
48
|
+
|
|
49
|
+
for (const item of items) {
|
|
50
|
+
if (!item || typeof item !== 'object') continue
|
|
51
|
+
const i = item as Record<string, unknown>
|
|
52
|
+
|
|
53
|
+
if (typeof i.label !== 'string' || typeof i.to !== 'string') continue
|
|
54
|
+
|
|
55
|
+
result.push({
|
|
56
|
+
label: i.label,
|
|
57
|
+
to: i.to,
|
|
58
|
+
badge: typeof i.badge === 'string' ? i.badge : undefined,
|
|
59
|
+
})
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return result
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function parseFrameworks(frameworks: unknown): DocsConfigFramework[] | undefined {
|
|
66
|
+
if (!Array.isArray(frameworks)) return undefined
|
|
67
|
+
|
|
68
|
+
const result: DocsConfigFramework[] = []
|
|
69
|
+
|
|
70
|
+
for (const fw of frameworks) {
|
|
71
|
+
if (!fw || typeof fw !== 'object') continue
|
|
72
|
+
const f = fw as Record<string, unknown>
|
|
73
|
+
|
|
74
|
+
if (typeof f.label !== 'string') continue
|
|
75
|
+
if (!Array.isArray(f.children)) continue
|
|
76
|
+
|
|
77
|
+
result.push({
|
|
78
|
+
label: f.label,
|
|
79
|
+
children: parseConfigItems(f.children),
|
|
80
|
+
})
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return result.length > 0 ? result : undefined
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Build sidebar menu items from a DocsConfig, resolving paths
|
|
88
|
+
* relative to the current route.
|
|
89
|
+
*/
|
|
90
|
+
export function buildSidebarItems(
|
|
91
|
+
config: DocsConfig,
|
|
92
|
+
basePath: string,
|
|
93
|
+
framework?: string,
|
|
94
|
+
) {
|
|
95
|
+
return config.sections.map((section) => {
|
|
96
|
+
const children = section.children.map((item) => ({
|
|
97
|
+
...item,
|
|
98
|
+
to: `${basePath}/${item.to}`,
|
|
99
|
+
}))
|
|
100
|
+
|
|
101
|
+
// If framework is specified, also include framework-specific items
|
|
102
|
+
let frameworkChildren: DocsConfigItem[] = []
|
|
103
|
+
if (framework && section.frameworks) {
|
|
104
|
+
const fwSection = section.frameworks.find(
|
|
105
|
+
(fw) => fw.label.toLowerCase() === framework.toLowerCase(),
|
|
106
|
+
)
|
|
107
|
+
if (fwSection) {
|
|
108
|
+
frameworkChildren = fwSection.children.map((item) => ({
|
|
109
|
+
...item,
|
|
110
|
+
to: `${basePath}/framework/${framework}/${item.to}`,
|
|
111
|
+
}))
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return {
|
|
116
|
+
...section,
|
|
117
|
+
children: [...children, ...frameworkChildren],
|
|
118
|
+
}
|
|
119
|
+
})
|
|
120
|
+
}
|