docs-i18n 0.6.3 → 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.
Files changed (169) hide show
  1. package/{src/admin/ui → admin/app}/components/JobDialog.tsx +21 -2
  2. package/{src/admin/ui → admin/app}/components/JobPanel.tsx +1 -1
  3. package/{src/admin/ui → admin/app}/components/Preview.tsx +2 -5
  4. package/{src/admin/ui → admin/app}/lib/api.ts +18 -39
  5. package/admin/app/routeTree.gen.ts +68 -0
  6. package/admin/app/router.tsx +23 -0
  7. package/admin/app/routes/__root.tsx +55 -0
  8. package/admin/app/routes/index.tsx +416 -0
  9. package/{src/admin/ui → admin/app}/styles.css +36 -3
  10. package/admin/package.json +27 -0
  11. package/admin/server/functions/jobs.ts +53 -0
  12. package/admin/server/functions/misc.ts +84 -0
  13. package/{src/admin/server/routes → admin/server/functions}/models.ts +16 -29
  14. package/admin/server/functions/status.ts +61 -0
  15. package/admin/server/index.ts +35 -0
  16. package/admin/server/init.ts +46 -0
  17. package/{src/admin → admin}/server/services/job-manager.ts +39 -10
  18. package/{src/admin → admin}/server/services/status.ts +6 -6
  19. package/admin/tsconfig.json +19 -0
  20. package/{src/admin → admin}/vite.config.ts +8 -2
  21. package/dist/{assemble-7H4QCW35.js → assemble-CP2BRYQJ.js} +6 -4
  22. package/dist/{chunk-A3YQNPKZ.js → chunk-CLYUAWZE.js} +1 -1
  23. package/dist/{chunk-YN4VJHCQ.js → chunk-JHBSHTXC.js} +1 -1
  24. package/dist/chunk-L64GJ4OB.js +32 -0
  25. package/dist/{chunk-SKKZIV3L.js → chunk-PNKVD2UK.js} +1 -29
  26. package/dist/{chunk-XEOYZUHS.js → chunk-QKIR7RKQ.js} +4 -31
  27. package/dist/chunk-TRURQFP4.js +31 -0
  28. package/dist/cli.js +108 -7
  29. package/dist/index.d.ts +41 -1
  30. package/dist/index.js +92 -3
  31. package/dist/{rescan-O5D3CYC2.js → rescan-HXMWFAOC.js} +5 -3
  32. package/dist/{status-F4MYIAAY.js → status-AGZDXOTZ.js} +4 -2
  33. package/dist/{translate-ZIVKNAC4.js → translate-A5X6MX4Y.js} +14 -7
  34. package/dist/upload-XL6KG6S2.js +132 -0
  35. package/package.json +17 -15
  36. package/template/app/components/BlogArticle.tsx +159 -0
  37. package/template/app/components/BlogList.tsx +88 -0
  38. package/template/app/components/Breadcrumbs.tsx +81 -0
  39. package/template/app/components/Card.tsx +31 -0
  40. package/template/app/components/Doc.tsx +191 -0
  41. package/template/app/components/DocBreadcrumb.tsx +60 -0
  42. package/template/app/components/DocContainer.tsx +13 -0
  43. package/template/app/components/DocTitle.tsx +11 -0
  44. package/template/app/components/DocsLayout.tsx +715 -0
  45. package/template/app/components/Dropdown.tsx +116 -0
  46. package/template/app/components/FallbackBanner.tsx +36 -0
  47. package/template/app/components/Footer.tsx +29 -0
  48. package/template/app/components/FrameworkSelect.tsx +150 -0
  49. package/template/app/components/LibraryCard.tsx +178 -0
  50. package/template/app/components/LocaleSwitcher.tsx +43 -0
  51. package/template/app/components/Navbar.tsx +430 -0
  52. package/template/app/components/PostNotFound.tsx +20 -0
  53. package/template/app/components/SearchButton.tsx +32 -0
  54. package/template/app/components/Select.tsx +103 -0
  55. package/template/app/components/Spinner.tsx +18 -0
  56. package/template/app/components/ThemeProvider.tsx +141 -0
  57. package/template/app/components/ThemeToggle.tsx +31 -0
  58. package/template/app/components/Toc.tsx +86 -0
  59. package/template/app/components/VersionSelect.tsx +118 -0
  60. package/template/app/components/icons/BSkyIcon.tsx +27 -0
  61. package/template/app/components/icons/BaseballCapIcon.tsx +25 -0
  62. package/template/app/components/icons/BrandXIcon.tsx +28 -0
  63. package/template/app/components/icons/CheckCircleIcon.tsx +28 -0
  64. package/template/app/components/icons/CogsIcon.tsx +25 -0
  65. package/template/app/components/icons/DiscordIcon.tsx +24 -0
  66. package/template/app/components/icons/GithubIcon.tsx +24 -0
  67. package/template/app/components/icons/GoogleIcon.tsx +24 -0
  68. package/template/app/components/icons/InstagramIcon.tsx +24 -0
  69. package/template/app/components/icons/NpmIcon.tsx +26 -0
  70. package/template/app/components/icons/YinYangIcon.tsx +26 -0
  71. package/template/app/components/icons/YouTubeIcon.tsx +24 -0
  72. package/template/app/components/markdown/CodeBlock.tsx +254 -0
  73. package/template/app/components/markdown/FileTabs.tsx +58 -0
  74. package/template/app/components/markdown/FrameworkContent.tsx +76 -0
  75. package/template/app/components/markdown/Markdown.tsx +216 -0
  76. package/template/app/components/markdown/MarkdownContent.tsx +89 -0
  77. package/template/app/components/markdown/MarkdownFrameworkHandler.tsx +66 -0
  78. package/template/app/components/markdown/MarkdownHeadingContext.tsx +35 -0
  79. package/template/app/components/markdown/MarkdownLink.tsx +46 -0
  80. package/template/app/components/markdown/MarkdownTabsHandler.tsx +109 -0
  81. package/template/app/components/markdown/PackageManagerTabs.tsx +95 -0
  82. package/template/app/components/markdown/Tabs.tsx +139 -0
  83. package/template/app/components/markdown/index.ts +15 -0
  84. package/template/app/components/ui/Button.tsx +141 -0
  85. package/template/app/components/ui/InlineCode.tsx +16 -0
  86. package/template/app/components/ui/MarkdownImg.tsx +21 -0
  87. package/template/app/config/frameworks.ts +93 -0
  88. package/template/app/contexts/SearchContext.tsx +36 -0
  89. package/template/app/db/index.ts +17 -0
  90. package/template/app/db/schema.ts +74 -0
  91. package/template/app/hooks/useClickOutside.ts +106 -0
  92. package/template/app/routeTree.gen.ts +584 -0
  93. package/template/app/router.tsx +29 -0
  94. package/template/app/routes/$lang.$project.$version.docs.$.tsx +128 -0
  95. package/template/app/routes/$lang.$project.$version.docs.framework.$framework.$.tsx +106 -0
  96. package/template/app/routes/$lang.$project.$version.docs.framework.$framework.index.tsx +27 -0
  97. package/template/app/routes/$lang.$project.$version.docs.framework.index.tsx +44 -0
  98. package/template/app/routes/$lang.$project.$version.docs.index.tsx +27 -0
  99. package/template/app/routes/$lang.$project.$version.docs.tsx +70 -0
  100. package/template/app/routes/$lang.$project.$version.tsx +69 -0
  101. package/template/app/routes/$lang.$project.docs.$.tsx +104 -0
  102. package/template/app/routes/$lang.$project.docs.index.tsx +20 -0
  103. package/template/app/routes/$lang.$project.docs.tsx +79 -0
  104. package/template/app/routes/$lang.$project.tsx +89 -0
  105. package/template/app/routes/$lang.blog.$.tsx +82 -0
  106. package/template/app/routes/$lang.blog.index.tsx +56 -0
  107. package/template/app/routes/$lang.blog.tsx +26 -0
  108. package/template/app/routes/$lang.docs.$.tsx +100 -0
  109. package/template/app/routes/$lang.docs.framework.$framework.$.tsx +104 -0
  110. package/template/app/routes/$lang.docs.framework.$framework.index.tsx +32 -0
  111. package/template/app/routes/$lang.docs.framework.index.tsx +47 -0
  112. package/template/app/routes/$lang.docs.index.tsx +20 -0
  113. package/template/app/routes/$lang.docs.tsx +90 -0
  114. package/template/app/routes/$lang.tsx +16 -0
  115. package/template/app/routes/__root.tsx +180 -0
  116. package/template/app/routes/index.tsx +89 -0
  117. package/template/app/site.config.ts +182 -0
  118. package/template/app/styles/app.css +1029 -0
  119. package/template/app/types/index.ts +77 -0
  120. package/template/app/utils/blog.server.ts +193 -0
  121. package/template/app/utils/blog.ts +42 -0
  122. package/template/app/utils/config.ts +120 -0
  123. package/template/app/utils/content-loader.ts +400 -0
  124. package/template/app/utils/dates.ts +29 -0
  125. package/template/app/utils/docs.server.ts +150 -0
  126. package/template/app/utils/markdown/filterFrameworkContent.ts +233 -0
  127. package/template/app/utils/markdown/index.ts +2 -0
  128. package/template/app/utils/markdown/installCommand.ts +143 -0
  129. package/template/app/utils/markdown/plugins/collectHeadings.ts +104 -0
  130. package/template/app/utils/markdown/plugins/extractCodeMeta.ts +57 -0
  131. package/template/app/utils/markdown/plugins/helpers.ts +33 -0
  132. package/template/app/utils/markdown/plugins/index.ts +8 -0
  133. package/template/app/utils/markdown/plugins/parseCommentComponents.ts +103 -0
  134. package/template/app/utils/markdown/plugins/transformCommentComponents.ts +23 -0
  135. package/template/app/utils/markdown/plugins/transformFrameworkComponent.ts +217 -0
  136. package/template/app/utils/markdown/plugins/transformTabsComponent.ts +359 -0
  137. package/template/app/utils/markdown/processor.ts +75 -0
  138. package/template/app/utils/site-config.tsx +11 -0
  139. package/template/app/utils/upload.ts +232 -0
  140. package/template/app/utils/useLocalStorage.ts +65 -0
  141. package/template/app/utils/utils.ts +23 -0
  142. package/template/package.json +54 -0
  143. package/template/public/favicon.svg +1 -0
  144. package/template/public/fonts/Inter-latin-ext.woff2 +0 -0
  145. package/template/public/fonts/Inter-latin.woff2 +0 -0
  146. package/template/public/images/frameworks/angular-logo.svg +1 -0
  147. package/template/public/images/frameworks/js-logo.svg +1 -0
  148. package/template/public/images/frameworks/lit-logo.svg +1 -0
  149. package/template/public/images/frameworks/preact-logo.svg +6 -0
  150. package/template/public/images/frameworks/qwik-logo.svg +1 -0
  151. package/template/public/images/frameworks/react-logo.svg +1 -0
  152. package/template/public/images/frameworks/solid-logo.svg +1 -0
  153. package/template/public/images/frameworks/svelte-logo.svg +1 -0
  154. package/template/public/images/frameworks/vue-logo.svg +4 -0
  155. package/template/tsconfig.json +24 -0
  156. package/template/vite.config.ts +43 -0
  157. package/template/wrangler.jsonc +16 -0
  158. package/README.md +0 -161
  159. package/dist/server-73AVSOL5.js +0 -598
  160. package/src/admin/index.html +0 -13
  161. package/src/admin/server/index.ts +0 -138
  162. package/src/admin/server/routes/jobs.ts +0 -113
  163. package/src/admin/server/routes/status.ts +0 -57
  164. package/src/admin/ui/App.tsx +0 -332
  165. package/src/admin/ui/main.tsx +0 -19
  166. /package/{src/admin/ui → admin/app}/components/FileList.tsx +0 -0
  167. /package/{src/admin/ui → admin/app}/components/LangGrid.tsx +0 -0
  168. /package/{src/admin/ui → admin/app}/components/ProgressBar.tsx +0 -0
  169. /package/{src/admin/ui → admin/app}/lib/flags.ts +0 -0
@@ -0,0 +1,89 @@
1
+ import { Link, createFileRoute } from '@tanstack/react-router'
2
+ import { notFound, Outlet, useParams } from '@tanstack/react-router'
3
+ import { findProject, siteConfig } from '~/site.config'
4
+
5
+ export const Route = createFileRoute('/$lang/$project')({
6
+ beforeLoad: (ctx) => {
7
+ const project = findProject(ctx.params.project)
8
+
9
+ if (!project) {
10
+ throw notFound()
11
+ }
12
+ },
13
+ loader: (ctx) => {
14
+ const project = findProject(ctx.params.project)
15
+
16
+ if (!project) {
17
+ throw notFound()
18
+ }
19
+ },
20
+ head: (ctx) => {
21
+ const project = findProject(ctx.params.project)
22
+
23
+ if (!project) {
24
+ return {
25
+ meta: [{ title: '404 Not Found' }],
26
+ }
27
+ }
28
+
29
+ const frameworkTitle = project.frameworks?.length
30
+ ? ` | ${project.frameworks
31
+ .map(
32
+ (framework) =>
33
+ `${framework.charAt(0).toUpperCase()}${framework.slice(1)} ${project.name}`,
34
+ )
35
+ .join(', ')}`
36
+ : ''
37
+
38
+ return {
39
+ meta: [
40
+ {
41
+ title: `${project.name}${frameworkTitle}`,
42
+ },
43
+ ...(project.description
44
+ ? [{ name: 'description', content: project.description }]
45
+ : []),
46
+ ],
47
+ }
48
+ },
49
+ component: ProjectLayout,
50
+ staticData: {
51
+ Title: () => {
52
+ const { project: projectId } = Route.useParams()
53
+ const { version } = useParams({ strict: false })
54
+ const project = findProject(projectId)
55
+
56
+ if (!project) {
57
+ return null
58
+ }
59
+
60
+ const projectName = project.name
61
+ const resolvedVersion =
62
+ version === 'latest' ? project.latestVersion : version!
63
+ const gradientText = `inline-block text-transparent bg-clip-text bg-linear-to-r ${project.colorFrom} ${project.colorTo}`
64
+ return (
65
+ <Link
66
+ to={`/$lang/$project`}
67
+ params={{ lang: siteConfig.defaultLocale, project: projectId }}
68
+ className="relative whitespace-nowrap"
69
+ >
70
+ <span className={`${gradientText}`}>{projectName}</span>{' '}
71
+ {resolvedVersion && resolvedVersion !== 'latest' && (
72
+ <>
73
+ <span className="text-sm absolute right-0 top-0 font-normal normal-case">
74
+ {resolvedVersion}
75
+ </span>
76
+ <span className="text-sm opacity-0 normal-case">
77
+ {resolvedVersion}
78
+ </span>
79
+ </>
80
+ )}
81
+ </Link>
82
+ )
83
+ },
84
+ },
85
+ })
86
+
87
+ function ProjectLayout() {
88
+ return <Outlet />
89
+ }
@@ -0,0 +1,82 @@
1
+ /**
2
+ * Blog article route — renders a single blog post.
3
+ *
4
+ * Loads post content via server function with i18n fallback.
5
+ * Uses splat route to support any slug depth (e.g., /en/blog/my-post).
6
+ */
7
+
8
+ import { notFound, createFileRoute } from '@tanstack/react-router'
9
+ import { createServerFn } from '@tanstack/react-start'
10
+ import { siteConfig } from '~/site.config'
11
+ import { formatAuthors } from '~/utils/blog'
12
+ import { BlogArticle } from '~/components/BlogArticle'
13
+ import { PostNotFound } from '~/components/PostNotFound'
14
+
15
+ const fetchBlogPost = createServerFn({ method: 'GET' })
16
+ .inputValidator((data: { lang: string; slug: string }) => data)
17
+ .handler(async ({ data }) => {
18
+ const { loadBlogPost } = await import('~/utils/blog.server')
19
+ const post = loadBlogPost(data.lang, data.slug)
20
+
21
+ if (!post) {
22
+ throw notFound()
23
+ }
24
+
25
+ // Hide drafts and future-dated posts
26
+ const now = new Date()
27
+ const publishDate = new Date(post.published)
28
+ const isUnpublished = post.draft || publishDate > now
29
+
30
+ return {
31
+ ...post,
32
+ isUnpublished,
33
+ }
34
+ },
35
+ )
36
+
37
+ export const Route = createFileRoute('/$lang/blog/$')({
38
+ staleTime: Infinity,
39
+ loader: ({ params }) =>
40
+ fetchBlogPost({
41
+ data: {
42
+ lang: params.lang,
43
+ slug: params._splat || '',
44
+ },
45
+ }),
46
+ head: ({ loaderData }) => ({
47
+ meta: loaderData
48
+ ? [
49
+ {
50
+ title: `${loaderData.title} | ${siteConfig.name} Blog`,
51
+ },
52
+ {
53
+ name: 'description',
54
+ content: loaderData.excerpt,
55
+ },
56
+ {
57
+ name: 'author',
58
+ content: `${
59
+ loaderData.authors.length > 1 ? 'co-authored by ' : ''
60
+ }${formatAuthors(loaderData.authors)}`,
61
+ },
62
+ ]
63
+ : [],
64
+ }),
65
+ notFoundComponent: () => <PostNotFound />,
66
+ component: BlogPostPage,
67
+ })
68
+
69
+ function BlogPostPage() {
70
+ const post = Route.useLoaderData()
71
+ const { lang } = Route.useParams()
72
+ const localeName =
73
+ siteConfig.supportedLocales[lang] || lang
74
+
75
+ return (
76
+ <BlogArticle
77
+ post={post}
78
+ lang={lang}
79
+ locale={localeName}
80
+ />
81
+ )
82
+ }
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Blog index route — lists all published blog posts.
3
+ *
4
+ * Loads blog post metadata via server function, renders using BlogList component.
5
+ * Posts are sorted by published date (newest first), drafts are excluded.
6
+ */
7
+
8
+ import { createFileRoute } from '@tanstack/react-router'
9
+ import { createServerFn } from '@tanstack/react-start'
10
+ import { siteConfig } from '~/site.config'
11
+ import { BlogList } from '~/components/BlogList'
12
+ import { PostNotFound } from '~/components/PostNotFound'
13
+
14
+ const fetchBlogPosts = createServerFn({ method: 'GET' })
15
+ .inputValidator((data: { lang: string }) => data)
16
+ .handler(async ({ data }) => {
17
+ const { loadBlogPosts } = await import('~/utils/blog.server')
18
+ return loadBlogPosts(data.lang)
19
+ })
20
+
21
+ export const Route = createFileRoute('/$lang/blog/')({
22
+ staleTime: Infinity,
23
+ loader: ({ params }) => fetchBlogPosts({ data: { lang: params.lang } }),
24
+ notFoundComponent: () => <PostNotFound />,
25
+ head: () => ({
26
+ meta: [
27
+ {
28
+ title: `Blog | ${siteConfig.name}`,
29
+ },
30
+ ],
31
+ }),
32
+ component: BlogIndex,
33
+ })
34
+
35
+ function BlogIndex() {
36
+ const posts = Route.useLoaderData()
37
+ const { lang } = Route.useParams()
38
+
39
+ return (
40
+ <div className="flex flex-col max-w-full min-h-screen gap-12 p-4 md:p-8 pb-0">
41
+ <div className="flex-1 w-full max-w-[1400px] mx-auto">
42
+ <div className="flex gap-8 items-start">
43
+ <div className="flex-1 space-y-12 min-w-0">
44
+ <header>
45
+ <h1 className="text-3xl font-black">Blog</h1>
46
+ <p className="text-lg mt-4 text-gray-600 dark:text-gray-400">
47
+ The latest news and blog posts
48
+ </p>
49
+ </header>
50
+ <BlogList posts={posts} lang={lang} />
51
+ </div>
52
+ </div>
53
+ </div>
54
+ </div>
55
+ )
56
+ }
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Blog layout route.
3
+ *
4
+ * Simple wrapper — no sidebar, just navbar + content.
5
+ * The navbar is already rendered by __root.tsx.
6
+ */
7
+
8
+ import { createFileRoute, Outlet } from '@tanstack/react-router'
9
+ import { siteConfig } from '~/site.config'
10
+ import { PostNotFound } from '~/components/PostNotFound'
11
+
12
+ export const Route = createFileRoute('/$lang/blog')({
13
+ head: () => ({
14
+ meta: [
15
+ {
16
+ title: `Blog | ${siteConfig.name}`,
17
+ },
18
+ ],
19
+ }),
20
+ component: BlogLayout,
21
+ notFoundComponent: PostNotFound,
22
+ })
23
+
24
+ function BlogLayout() {
25
+ return <Outlet />
26
+ }
@@ -0,0 +1,100 @@
1
+ import {
2
+ createFileRoute,
3
+ notFound,
4
+ } from '@tanstack/react-router'
5
+ import { createServerFn } from '@tanstack/react-start'
6
+ import { siteConfig, isSingleProject, getSingleProject } from '~/site.config'
7
+ import { Doc } from '~/components/Doc'
8
+
9
+ const fetchDoc = createServerFn({ method: 'GET' })
10
+ .inputValidator(
11
+ (data: { project: string; version: string; lang: string; slug: string }) =>
12
+ data,
13
+ )
14
+ .handler(async ({ data }) => {
15
+ const { loadDoc } = await import('~/utils/docs.server')
16
+ return loadDoc(data.project, data.version, data.lang, data.slug)
17
+ })
18
+
19
+ export const Route = createFileRoute('/$lang/docs/$')({
20
+ staleTime: 1000 * 60 * 5,
21
+ beforeLoad: () => {
22
+ if (!isSingleProject()) {
23
+ throw notFound()
24
+ }
25
+ },
26
+ loader: async ({ params }) => {
27
+ const { lang, _splat: slug } = params
28
+ if (!slug) throw notFound()
29
+
30
+ const project = getSingleProject()
31
+
32
+ try {
33
+ return await fetchDoc({
34
+ data: {
35
+ project: project.id,
36
+ version: project.latestVersion,
37
+ lang,
38
+ slug,
39
+ },
40
+ })
41
+ } catch (error) {
42
+ throw notFound()
43
+ }
44
+ },
45
+ head: ({ loaderData }) => {
46
+ const project = getSingleProject()
47
+ const title = loaderData?.meta?.title
48
+ const projectName = project?.name || siteConfig.name
49
+
50
+ return {
51
+ meta: [
52
+ {
53
+ title: title ? `${title} | ${projectName} Docs` : `${projectName} Docs`,
54
+ },
55
+ ...(loaderData?.meta?.description
56
+ ? [{ name: 'description', content: loaderData.meta.description }]
57
+ : []),
58
+ ],
59
+ }
60
+ },
61
+ headers: () => {
62
+ return {
63
+ 'cache-control': 'public, max-age=60, must-revalidate',
64
+ 'cdn-cache-control':
65
+ 'max-age=600, stale-while-revalidate=3600, durable',
66
+ vary: 'Accept-Encoding',
67
+ }
68
+ },
69
+ component: SingleProjectDocsPage,
70
+ errorComponent: DocNotFound,
71
+ })
72
+
73
+ function SingleProjectDocsPage() {
74
+ const loaderData = Route.useLoaderData()
75
+ const project = getSingleProject()
76
+
77
+ return (
78
+ <Doc
79
+ title={loaderData.meta?.title || ''}
80
+ content={loaderData.content}
81
+ isFallback={loaderData.isFallback}
82
+ filePath={loaderData.filePath}
83
+ repo={project.repo}
84
+ branch={project.latestBranch}
85
+ />
86
+ )
87
+ }
88
+
89
+ function DocNotFound() {
90
+ return (
91
+ <div className="flex-1 flex items-center justify-center py-20">
92
+ <div className="text-center">
93
+ <h1 className="text-2xl font-bold mb-2">Page Not Found</h1>
94
+ <p className="text-gray-500 dark:text-gray-400">
95
+ The document you are looking for does not exist.
96
+ </p>
97
+ </div>
98
+ </div>
99
+ )
100
+ }
@@ -0,0 +1,104 @@
1
+ import {
2
+ createFileRoute,
3
+ notFound,
4
+ redirect,
5
+ } from '@tanstack/react-router'
6
+ import { createServerFn } from '@tanstack/react-start'
7
+ import { siteConfig, isSingleProject, getSingleProject } from '~/site.config'
8
+ import { Doc } from '~/components/Doc'
9
+
10
+ const fetchFrameworkDoc = createServerFn({ method: 'GET' })
11
+ .inputValidator(
12
+ (data: {
13
+ project: string
14
+ version: string
15
+ lang: string
16
+ framework: string
17
+ slug: string
18
+ }) => data,
19
+ )
20
+ .handler(async ({ data }) => {
21
+ const { loadDoc } = await import('~/utils/docs.server')
22
+ return loadDoc(
23
+ data.project,
24
+ data.version,
25
+ data.lang,
26
+ `framework/${data.framework}/${data.slug}`,
27
+ )
28
+ })
29
+
30
+ export const Route = createFileRoute(
31
+ '/$lang/docs/framework/$framework/$',
32
+ )({
33
+ staleTime: 1000 * 60 * 5,
34
+ beforeLoad: () => {
35
+ if (!isSingleProject()) {
36
+ throw notFound()
37
+ }
38
+ },
39
+ loader: async ({ params }) => {
40
+ const { lang, framework, _splat: slug } = params
41
+ const project = getSingleProject()
42
+
43
+ try {
44
+ return await fetchFrameworkDoc({
45
+ data: {
46
+ project: project.id,
47
+ version: project.latestVersion,
48
+ lang,
49
+ framework,
50
+ slug: slug || '',
51
+ },
52
+ })
53
+ } catch (error) {
54
+ throw redirect({
55
+ to: '/$lang/docs/framework/$framework',
56
+ params: { lang, framework },
57
+ })
58
+ }
59
+ },
60
+ head: ({ loaderData, params }) => {
61
+ const project = getSingleProject()
62
+ const projectName = project?.name || siteConfig.name
63
+ const framework =
64
+ params.framework.charAt(0).toUpperCase() + params.framework.slice(1)
65
+ const title = loaderData?.meta?.title
66
+
67
+ return {
68
+ meta: [
69
+ {
70
+ title: title
71
+ ? `${title} | ${projectName} ${framework} Docs`
72
+ : `${projectName} ${framework} Docs`,
73
+ },
74
+ ],
75
+ }
76
+ },
77
+ headers: () => {
78
+ return {
79
+ 'cache-control': 'public, max-age=60, must-revalidate',
80
+ 'cdn-cache-control':
81
+ 'max-age=600, stale-while-revalidate=3600, durable',
82
+ vary: 'Accept-Encoding',
83
+ }
84
+ },
85
+ component: FrameworkDocsPage,
86
+ })
87
+
88
+ function FrameworkDocsPage() {
89
+ const loaderData = Route.useLoaderData()
90
+ const { framework } = Route.useParams()
91
+ const project = getSingleProject()
92
+
93
+ return (
94
+ <Doc
95
+ title={loaderData.meta?.title || ''}
96
+ content={loaderData.content}
97
+ isFallback={loaderData.isFallback}
98
+ filePath={loaderData.filePath}
99
+ repo={project.repo}
100
+ branch={project.latestBranch}
101
+ framework={framework}
102
+ />
103
+ )
104
+ }
@@ -0,0 +1,32 @@
1
+ import { createFileRoute, notFound } from '@tanstack/react-router'
2
+ import { isSingleProject, getSingleProject } from '~/site.config'
3
+
4
+ export const Route = createFileRoute(
5
+ '/$lang/docs/framework/$framework/',
6
+ )({
7
+ beforeLoad: () => {
8
+ if (!isSingleProject()) {
9
+ throw notFound()
10
+ }
11
+ },
12
+ component: FrameworkIndex,
13
+ })
14
+
15
+ function FrameworkIndex() {
16
+ const { framework } = Route.useParams()
17
+ const project = getSingleProject()
18
+ const frameworkLabel =
19
+ framework.charAt(0).toUpperCase() + framework.slice(1)
20
+
21
+ return (
22
+ <div className="py-8 max-w-3xl mx-auto">
23
+ <h1 className="text-3xl font-bold mb-4">
24
+ {project?.name} {frameworkLabel} Documentation
25
+ </h1>
26
+ <div className="h-px bg-gray-200 dark:bg-gray-700 mb-6" />
27
+ <p className="text-gray-600 dark:text-gray-400">
28
+ Use the sidebar to select a documentation page.
29
+ </p>
30
+ </div>
31
+ )
32
+ }
@@ -0,0 +1,47 @@
1
+ import { createFileRoute, Link, notFound } from '@tanstack/react-router'
2
+ import { isSingleProject, getSingleProject } from '~/site.config'
3
+
4
+ export const Route = createFileRoute('/$lang/docs/framework/')({
5
+ beforeLoad: () => {
6
+ if (!isSingleProject()) {
7
+ throw notFound()
8
+ }
9
+ },
10
+ component: FrameworkSelectionPage,
11
+ })
12
+
13
+ function FrameworkSelectionPage() {
14
+ const { lang } = Route.useParams()
15
+ const project = getSingleProject()
16
+ const frameworks = project?.frameworks || []
17
+
18
+ return (
19
+ <div className="py-8 max-w-3xl mx-auto">
20
+ <h1 className="text-3xl font-bold mb-4">
21
+ {project?.name} Frameworks
22
+ </h1>
23
+ <div className="h-px bg-gray-200 dark:bg-gray-700 mb-6" />
24
+
25
+ {frameworks.length > 0 ? (
26
+ <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
27
+ {frameworks.map((fw) => (
28
+ <Link
29
+ key={fw}
30
+ to="/$lang/docs/framework/$framework"
31
+ params={{ lang, framework: fw }}
32
+ className="p-4 rounded-lg border border-gray-200 dark:border-gray-700 hover:bg-gray-50 dark:hover:bg-gray-800 transition-colors"
33
+ >
34
+ <span className="text-lg font-medium">
35
+ {fw.charAt(0).toUpperCase() + fw.slice(1)}
36
+ </span>
37
+ </Link>
38
+ ))}
39
+ </div>
40
+ ) : (
41
+ <p className="text-gray-600 dark:text-gray-400">
42
+ No frameworks are configured for this project.
43
+ </p>
44
+ )}
45
+ </div>
46
+ )
47
+ }
@@ -0,0 +1,20 @@
1
+ import { createFileRoute, notFound, redirect } from '@tanstack/react-router'
2
+ import { isSingleProject, getSingleProject } from '~/site.config'
3
+
4
+ export const Route = createFileRoute('/$lang/docs/')({
5
+ beforeLoad: () => {
6
+ if (!isSingleProject()) {
7
+ throw notFound()
8
+ }
9
+
10
+ const project = getSingleProject()
11
+
12
+ throw redirect({
13
+ from: '/$lang/docs',
14
+ to: './$',
15
+ params: {
16
+ _splat: project.defaultDocs || 'overview',
17
+ },
18
+ })
19
+ },
20
+ })
@@ -0,0 +1,90 @@
1
+ import * as React from 'react'
2
+ import {
3
+ createFileRoute,
4
+ notFound,
5
+ Outlet,
6
+ Link,
7
+ } from '@tanstack/react-router'
8
+ import { createServerFn } from '@tanstack/react-start'
9
+ import { siteConfig, findProject, isSingleProject, getSingleProject } from '~/site.config'
10
+ import { DocsLayout } from '~/components/DocsLayout'
11
+ import { frameworkOptions } from '~/config/frameworks'
12
+ import type { Framework } from '~/config/frameworks'
13
+
14
+ const fetchDocsConfig = createServerFn({ method: 'GET' })
15
+ .inputValidator((data: { project: string; version: string }) => data)
16
+ .handler(async ({ data }) => {
17
+ const { loadDocsConfig } = await import('~/utils/docs.server')
18
+ return loadDocsConfig(data.project, data.version)
19
+ })
20
+
21
+ export const Route = createFileRoute('/$lang/docs')({
22
+ staleTime: 1000 * 60 * 5,
23
+ beforeLoad: () => {
24
+ // This route only works for single-project sites
25
+ if (!isSingleProject()) {
26
+ throw notFound()
27
+ }
28
+ },
29
+ loader: async () => {
30
+ const project = getSingleProject()
31
+ const config = await fetchDocsConfig({
32
+ data: { project: project.id, version: project.latestVersion },
33
+ })
34
+ return { config }
35
+ },
36
+ component: SingleProjectDocsLayout,
37
+ head: () => {
38
+ const project = getSingleProject()
39
+ return {
40
+ meta: [{ title: project ? `${project.name} Docs` : siteConfig.name }],
41
+ }
42
+ },
43
+ headers: () => ({
44
+ 'cache-control': 'public, max-age=0, must-revalidate',
45
+ 'cdn-cache-control': 'max-age=300, stale-while-revalidate=300, durable',
46
+ }),
47
+ staticData: {
48
+ Title: () => {
49
+ const project = getSingleProject()
50
+ const gradientText = `inline-block text-transparent bg-clip-text bg-linear-to-r ${project.colorFrom} ${project.colorTo}`
51
+ return (
52
+ <Link
53
+ to="/$lang/docs"
54
+ params={{ lang: siteConfig.defaultLocale }}
55
+ className="relative whitespace-nowrap"
56
+ >
57
+ <span className={gradientText}>{project.name}</span>
58
+ </Link>
59
+ )
60
+ },
61
+ },
62
+ })
63
+
64
+ function SingleProjectDocsLayout() {
65
+ const { config } = Route.useLoaderData()
66
+ const project = getSingleProject()
67
+
68
+ // Map string framework IDs to Framework type
69
+ const frameworks: Framework[] = (project.frameworks ?? []).filter(
70
+ (f): f is Framework =>
71
+ frameworkOptions.some((opt) => opt.value === f),
72
+ )
73
+
74
+ return (
75
+ <DocsLayout
76
+ name={project.name}
77
+ version={project.latestVersion}
78
+ colorFrom={project.colorFrom}
79
+ colorTo={project.colorTo}
80
+ textColor={project.textColor ?? ''}
81
+ config={config}
82
+ frameworks={frameworks}
83
+ versions={project.availableVersions}
84
+ repo={project.repo}
85
+ docsBasePath="/$lang/docs"
86
+ >
87
+ <Outlet />
88
+ </DocsLayout>
89
+ )
90
+ }
@@ -0,0 +1,16 @@
1
+ import { createFileRoute, notFound, Outlet } from '@tanstack/react-router'
2
+ import { siteConfig } from '~/site.config'
3
+
4
+ export const Route = createFileRoute('/$lang')({
5
+ beforeLoad: ({ params }) => {
6
+ // Validate locale against supported locales
7
+ if (!(params.lang in siteConfig.supportedLocales)) {
8
+ throw notFound()
9
+ }
10
+ },
11
+ component: LangLayout,
12
+ })
13
+
14
+ function LangLayout() {
15
+ return <Outlet />
16
+ }