livemark 0.14.0 → 0.16.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 (54) hide show
  1. package/build/components/Layout.js +8 -2
  2. package/build/components/SidebarArticles.js +9 -9
  3. package/build/components/SidebarLinks.js +3 -2
  4. package/build/components/SidebarSections.js +8 -2
  5. package/build/components/SiteTitle.js +3 -2
  6. package/build/content/article.d.ts +10 -10
  7. package/build/content/article.js +19 -17
  8. package/build/content-collections.js +3 -2
  9. package/build/helpers/article.d.ts +1 -15
  10. package/build/helpers/article.js +1 -34
  11. package/build/helpers/prefix-url.d.ts +6 -0
  12. package/build/helpers/prefix-url.js +24 -0
  13. package/build/helpers/prefix-url.unit.d.ts +1 -0
  14. package/build/helpers/prefix-url.unit.js +45 -0
  15. package/build/helpers/resolve-asset-path.d.ts +1 -1
  16. package/build/helpers/resolve-asset-path.js +9 -5
  17. package/build/helpers/resolve-asset-path.unit.d.ts +1 -0
  18. package/build/helpers/resolve-asset-path.unit.js +34 -0
  19. package/build/helpers/section.js +2 -1
  20. package/build/models/article.d.ts +0 -9
  21. package/build/models/article.js +1 -2
  22. package/build/models/config.d.ts +39 -11
  23. package/build/models/config.js +11 -1
  24. package/build/models/patch.d.ts +0 -1
  25. package/build/models/section.d.ts +27 -6
  26. package/build/models/section.js +14 -3
  27. package/build/plugins/remark-image.d.ts +1 -0
  28. package/build/plugins/remark-image.js +3 -3
  29. package/build/plugins/vite-livemark.js +6 -1
  30. package/build/router.js +3 -1
  31. package/build/routes/__root.js +3 -2
  32. package/build/vite.config.js +18 -4
  33. package/package.json +1 -1
  34. package/source/components/Layout.tsx +17 -1
  35. package/source/components/SidebarArticles.tsx +17 -24
  36. package/source/components/SidebarLinks.tsx +2 -1
  37. package/source/components/SidebarSections.tsx +18 -1
  38. package/source/components/SiteTitle.tsx +2 -1
  39. package/source/content/article.ts +20 -26
  40. package/source/content-collections.ts +2 -1
  41. package/source/helpers/article.ts +1 -39
  42. package/source/helpers/prefix-url.ts +22 -0
  43. package/source/helpers/prefix-url.unit.ts +57 -0
  44. package/source/helpers/resolve-asset-path.ts +12 -5
  45. package/source/helpers/resolve-asset-path.unit.ts +59 -0
  46. package/source/helpers/section.ts +1 -0
  47. package/source/models/article.ts +0 -10
  48. package/source/models/config.ts +9 -0
  49. package/source/models/section.ts +16 -2
  50. package/source/plugins/remark-image.ts +3 -3
  51. package/source/plugins/vite-livemark.ts +5 -0
  52. package/source/router.tsx +2 -0
  53. package/source/routes/__root.tsx +5 -1
  54. package/source/vite.config.ts +17 -3
@@ -1,11 +1,7 @@
1
1
  import { config } from "livemark:virtual"
2
2
  import { allArticles } from "content-collections"
3
- import type { ArticleGroup } from "../models/article.ts"
4
- import {
5
- buildArticleTree,
6
- flattenArticleTree,
7
- groupArticleTree,
8
- } from "../helpers/article.ts"
3
+ import type { ArticleNode } from "../models/article.ts"
4
+ import { buildArticleTree, flattenArticleTree } from "../helpers/article.ts"
9
5
  import { matchSection, partitionBySection } from "../helpers/section.ts"
10
6
 
11
7
  /**
@@ -20,16 +16,11 @@ export const sortedArticles = [...allArticles].sort(
20
16
  /** The article whose path is `/`, if any (rendered in place at the root) */
21
17
  export const homeArticle = sortedArticles.find(a => a.path === "/")
22
18
 
23
- /** Sidebar sections derived from the path tree, partitioned by `group` */
24
- export const articleGroups = groupArticleTree(
25
- buildArticleTree(sortedArticles),
26
- sortedArticles,
27
- )
19
+ /** Sidebar tree derived from the article path hierarchy */
20
+ export const articleTree = buildArticleTree(sortedArticles)
28
21
 
29
- /** Articles in depth-first order across all sections (matches sidebar walk) */
30
- export const flatArticles = articleGroups.flatMap(group =>
31
- flattenArticleTree(group.nodes),
32
- )
22
+ /** Articles in depth-first order across the tree (matches sidebar walk) */
23
+ export const flatArticles = flattenArticleTree(articleTree)
33
24
 
34
25
  /** The first article in sidebar order, used as the default landing page */
35
26
  export const firstArticle = sortedArticles.find(a => a.path === flatArticles[0])
@@ -42,8 +33,12 @@ function orderKey(order?: number) {
42
33
 
43
34
  const configSections = config.sections
44
35
 
45
- /** Per-section article groups, keyed by section prefix */
46
- export const sectionArticleGroups = new Map<string, ArticleGroup[]>()
36
+ /** Route-bound sections (everything except `external`). External sections
37
+ * are pure links — they have no `prefix` and don't claim a URL space. */
38
+ const routedSections = configSections?.filter(s => s.type !== "external") ?? []
39
+
40
+ /** Per-section article tree, keyed by section prefix */
41
+ export const sectionArticleTrees = new Map<string, ArticleNode[]>()
47
42
 
48
43
  /** Per-section flat article lists, keyed by section prefix */
49
44
  export const sectionFlatArticles = new Map<string, string[]>()
@@ -51,9 +46,9 @@ export const sectionFlatArticles = new Map<string, string[]>()
51
46
  /** Per-section first article path, keyed by section prefix */
52
47
  export const sectionFirstArticle = new Map<string, string | undefined>()
53
48
 
54
- if (configSections?.length) {
55
- const sectionByPrefix = new Map(configSections.map(s => [s.prefix, s]))
56
- const buckets = partitionBySection(sortedArticles, configSections)
49
+ if (routedSections.length) {
50
+ const sectionByPrefix = new Map(routedSections.map(s => [s.prefix, s]))
51
+ const buckets = partitionBySection(sortedArticles, routedSections)
57
52
  for (const [key, bucket] of buckets) {
58
53
  const section = sectionByPrefix.get(key)
59
54
  const sorted =
@@ -64,16 +59,15 @@ if (configSections?.length) {
64
59
  )
65
60
  : bucket
66
61
  const tree = buildArticleTree(sorted)
67
- const groups = groupArticleTree(tree, sorted)
68
- const flat = groups.flatMap(g => flattenArticleTree(g.nodes))
69
- sectionArticleGroups.set(key, groups)
62
+ const flat = flattenArticleTree(tree)
63
+ sectionArticleTrees.set(key, tree)
70
64
  sectionFlatArticles.set(key, flat)
71
65
  sectionFirstArticle.set(key, flat[0])
72
66
  }
73
67
  }
74
68
 
75
- /** Find which config section an article path belongs to */
69
+ /** Find which routed config section an article path belongs to */
76
70
  export function currentSection(path: string) {
77
- if (!configSections?.length) return undefined
78
- return matchSection(path, configSections)
71
+ if (!routedSections.length) return undefined
72
+ return matchSection(path, routedSections)
79
73
  }
@@ -162,6 +162,7 @@ const articles = defineCollection({
162
162
  {
163
163
  filePath: resolve(config.root, doc._meta.filePath),
164
164
  root: config.root,
165
+ base: config.base,
165
166
  },
166
167
  ],
167
168
  remarkAnsi,
@@ -213,7 +214,7 @@ const articles = defineCollection({
213
214
  const searchText = extractSearchText(content)
214
215
  const lastUpdated = getLastUpdated(doc._meta.filePath)
215
216
  const image = doc.image
216
- ? resolveAssetPath(doc.image, filePath, config.root)
217
+ ? resolveAssetPath(doc.image, filePath, config.root, config.base)
217
218
  : undefined
218
219
  return {
219
220
  ...doc,
@@ -1,4 +1,4 @@
1
- import type { ArticleGroup, ArticleNode } from "../models/article.ts"
1
+ import type { ArticleNode } from "../models/article.ts"
2
2
 
3
3
  /** Build a tree from a flat sorted list of articles based on path hierarchy */
4
4
  export function buildArticleTree(
@@ -41,44 +41,6 @@ export function buildArticleTree(
41
41
  return roots
42
42
  }
43
43
 
44
- /**
45
- * Partition root nodes into sidebar sections.
46
- *
47
- * Walks root nodes in sort order. A root whose article has a `group` field
48
- * opens a new section; subsequent roots without `group` append to the current
49
- * section. Roots before the first `group` field land in a single leading
50
- * section with `name: undefined` (rendered without a label).
51
- *
52
- * `group` on non-root articles is ignored — only roots gate section boundaries.
53
- */
54
- export function groupArticleTree(
55
- tree: ArticleNode[],
56
- articles: { path: string; group?: string }[],
57
- ): ArticleGroup[] {
58
- const groupByPathname = new Map<string, string | undefined>()
59
- for (const article of articles) {
60
- groupByPathname.set(article.path, article.group)
61
- }
62
-
63
- const sections: ArticleGroup[] = []
64
- let current: ArticleGroup | undefined
65
-
66
- for (const node of tree) {
67
- const group = groupByPathname.get(node.path)
68
- if (group) {
69
- current = { name: group, nodes: [node] }
70
- sections.push(current)
71
- } else if (current) {
72
- current.nodes.push(node)
73
- } else {
74
- current = { name: undefined, nodes: [node] }
75
- sections.push(current)
76
- }
77
- }
78
-
79
- return sections
80
- }
81
-
82
44
  /** Depth-first flatten of an article tree into a list of paths */
83
45
  export function flattenArticleTree(nodes: ArticleNode[]) {
84
46
  const result: string[] = []
@@ -0,0 +1,22 @@
1
+ /** Prefix a root-relative URL with `base` when the site is mounted at
2
+ * a sub-path. Leaves protocol-absolute (`http(s)://`, `//`), scheme
3
+ * (`mailto:`, `tel:`, `data:`), hash, and relative URLs untouched, and
4
+ * is idempotent — already-prefixed URLs are returned unchanged so
5
+ * config validation or hot reload can't double-prefix. */
6
+ export function prefixUrl(url: string, base: string | undefined) {
7
+ if (!base) return url
8
+ if (
9
+ url.startsWith("http://") ||
10
+ url.startsWith("https://") ||
11
+ url.startsWith("//") ||
12
+ url.startsWith("mailto:") ||
13
+ url.startsWith("tel:") ||
14
+ url.startsWith("data:") ||
15
+ url.startsWith("#")
16
+ ) {
17
+ return url
18
+ }
19
+ if (!url.startsWith("/")) return url
20
+ if (url === base || url.startsWith(`${base}/`)) return url
21
+ return `${base}${url}`
22
+ }
@@ -0,0 +1,57 @@
1
+ import { describe, expect, it } from "vite-plus/test"
2
+ import { prefixUrl } from "./prefix-url.ts"
3
+
4
+ describe("prefixUrl", () => {
5
+ it("should return url unchanged when base is undefined", () => {
6
+ expect(prefixUrl("/foo", undefined)).toBe("/foo")
7
+ })
8
+
9
+ it("should prefix a root-relative url", () => {
10
+ expect(prefixUrl("/foo", "/repo")).toBe("/repo/foo")
11
+ })
12
+
13
+ it("should prefix a deep root-relative url", () => {
14
+ expect(prefixUrl("/a/b/c.png", "/repo")).toBe("/repo/a/b/c.png")
15
+ })
16
+
17
+ it("should leave protocol-absolute http urls untouched", () => {
18
+ expect(prefixUrl("http://x.com/y", "/repo")).toBe("http://x.com/y")
19
+ expect(prefixUrl("https://x.com/y", "/repo")).toBe("https://x.com/y")
20
+ })
21
+
22
+ it("should leave protocol-relative urls untouched", () => {
23
+ expect(prefixUrl("//cdn.x.com/y", "/repo")).toBe("//cdn.x.com/y")
24
+ })
25
+
26
+ it("should leave scheme urls untouched", () => {
27
+ expect(prefixUrl("mailto:a@b.com", "/repo")).toBe("mailto:a@b.com")
28
+ expect(prefixUrl("tel:+123", "/repo")).toBe("tel:+123")
29
+ expect(prefixUrl("data:image/png;base64,xyz", "/repo")).toBe(
30
+ "data:image/png;base64,xyz",
31
+ )
32
+ })
33
+
34
+ it("should leave hash urls untouched", () => {
35
+ expect(prefixUrl("#section", "/repo")).toBe("#section")
36
+ })
37
+
38
+ it("should leave document-relative urls untouched", () => {
39
+ expect(prefixUrl("foo.png", "/repo")).toBe("foo.png")
40
+ expect(prefixUrl("./foo.png", "/repo")).toBe("./foo.png")
41
+ expect(prefixUrl("../foo.png", "/repo")).toBe("../foo.png")
42
+ })
43
+
44
+ it("should be idempotent on already-prefixed urls", () => {
45
+ expect(prefixUrl("/repo/foo", "/repo")).toBe("/repo/foo")
46
+ expect(prefixUrl("/repo", "/repo")).toBe("/repo")
47
+ })
48
+
49
+ it("should not match prefix-as-substring", () => {
50
+ expect(prefixUrl("/repository/foo", "/repo")).toBe("/repo/repository/foo")
51
+ })
52
+
53
+ it("should support multi-segment base", () => {
54
+ expect(prefixUrl("/foo", "/a/b")).toBe("/a/b/foo")
55
+ expect(prefixUrl("/a/b/foo", "/a/b")).toBe("/a/b/foo")
56
+ })
57
+ })
@@ -1,9 +1,16 @@
1
- import { dirname, relative, resolve } from "node:path"
1
+ import { dirname, relative, resolve, sep } from "node:path"
2
+ import { prefixUrl } from "./prefix-url.ts"
2
3
 
3
4
  /** Resolve a relative asset path to an absolute URL serveable by Vite */
4
- export function resolveAssetPath(url: string, filePath: string, root: string) {
5
- if (url.startsWith("http") || url.startsWith("/") || url.startsWith("data:"))
6
- return url
5
+ export function resolveAssetPath(
6
+ url: string,
7
+ filePath: string,
8
+ root: string,
9
+ base?: string,
10
+ ) {
11
+ if (url.startsWith("http") || url.startsWith("data:")) return url
12
+ if (url.startsWith("/")) return prefixUrl(url, base)
7
13
  const absolute = resolve(dirname(filePath), url)
8
- return "/" + relative(root, absolute)
14
+ const rel = relative(root, absolute).split(sep).join("/")
15
+ return prefixUrl("/" + rel, base)
9
16
  }
@@ -0,0 +1,59 @@
1
+ import { describe, expect, it } from "vite-plus/test"
2
+ import { resolveAssetPath } from "./resolve-asset-path.ts"
3
+
4
+ describe("resolveAssetPath", () => {
5
+ const root = "/site"
6
+ const filePath = "/site/docs/page.md"
7
+
8
+ it("should resolve a sibling relative path", () => {
9
+ expect(resolveAssetPath("./logo.png", filePath, root)).toBe(
10
+ "/docs/logo.png",
11
+ )
12
+ })
13
+
14
+ it("should resolve a parent relative path", () => {
15
+ expect(resolveAssetPath("../assets/x.png", filePath, root)).toBe(
16
+ "/assets/x.png",
17
+ )
18
+ })
19
+
20
+ it("should pass through a root-relative url", () => {
21
+ expect(resolveAssetPath("/foo.png", filePath, root)).toBe("/foo.png")
22
+ })
23
+
24
+ it("should pass through an http url", () => {
25
+ expect(resolveAssetPath("https://x.com/y.png", filePath, root)).toBe(
26
+ "https://x.com/y.png",
27
+ )
28
+ })
29
+
30
+ it("should pass through a data url", () => {
31
+ expect(resolveAssetPath("data:image/png;base64,xyz", filePath, root)).toBe(
32
+ "data:image/png;base64,xyz",
33
+ )
34
+ })
35
+
36
+ it("should prefix a resolved relative path with base", () => {
37
+ expect(resolveAssetPath("./logo.png", filePath, root, "/repo")).toBe(
38
+ "/repo/docs/logo.png",
39
+ )
40
+ })
41
+
42
+ it("should prefix a root-relative url with base", () => {
43
+ expect(resolveAssetPath("/foo.png", filePath, root, "/repo")).toBe(
44
+ "/repo/foo.png",
45
+ )
46
+ })
47
+
48
+ it("should not double-prefix an already-prefixed url", () => {
49
+ expect(resolveAssetPath("/repo/foo.png", filePath, root, "/repo")).toBe(
50
+ "/repo/foo.png",
51
+ )
52
+ })
53
+
54
+ it("should not prefix external urls when base is set", () => {
55
+ expect(
56
+ resolveAssetPath("https://x.com/y.png", filePath, root, "/repo"),
57
+ ).toBe("https://x.com/y.png")
58
+ })
59
+ })
@@ -4,6 +4,7 @@ const DEFAULT_ICONS: Record<Section["type"], string> = {
4
4
  article: "book-open",
5
5
  blog: "rss",
6
6
  changelog: "history",
7
+ external: "external-link",
7
8
  }
8
9
 
9
10
  /** Return the section's icon, falling back to a type-based default */
@@ -12,7 +12,6 @@ export const Article = z.object({
12
12
  icon: nonEmptyString.optional(),
13
13
  order: z.number().optional(),
14
14
  path: nonEmptyString.optional(),
15
- group: nonEmptyString.optional(),
16
15
  sidebar: z.boolean().default(true),
17
16
  toc: z.boolean().default(true),
18
17
  image: nonEmptyString.optional(),
@@ -49,12 +48,3 @@ export interface ArticleNode {
49
48
  icon: string
50
49
  children: ArticleNode[]
51
50
  }
52
-
53
- /**
54
- * A contiguous section of root articles sharing the same `group` label.
55
- * `name: undefined` represents the leading ungrouped section (at most one).
56
- */
57
- export interface ArticleGroup {
58
- name?: string
59
- nodes: ArticleNode[]
60
- }
@@ -23,6 +23,14 @@ export type UserConfig = Omit<z.infer<typeof UserConfig>, "vite"> & {
23
23
  }
24
24
  export const UserConfig = z.object({
25
25
  site: z.string().optional(),
26
+ base: z
27
+ .string()
28
+ .optional()
29
+ .transform(v => {
30
+ if (!v) return undefined
31
+ const trimmed = v.replace(/^\/|\/$/g, "")
32
+ return trimmed ? `/${trimmed}` : undefined
33
+ }),
26
34
  favicon: z.string().optional(),
27
35
  logo: z.string().optional(),
28
36
  title: z.string().default("Livemark"),
@@ -46,6 +54,7 @@ export const WebsiteConfig = UserConfig.pick({
46
54
  title: true,
47
55
  description: true,
48
56
  site: true,
57
+ base: true,
49
58
  favicon: true,
50
59
  logo: true,
51
60
  links: true,
@@ -3,7 +3,6 @@ import { z } from "zod"
3
3
  const BaseSection = z.object({
4
4
  icon: z.string().optional(),
5
5
  title: z.string(),
6
- prefix: z.string(),
7
6
  position: z.enum(["header", "sidebar"]).default("header"),
8
7
  })
9
8
 
@@ -11,22 +10,32 @@ const BaseSection = z.object({
11
10
  export type ArticleSection = z.infer<typeof ArticleSection>
12
11
  export const ArticleSection = BaseSection.extend({
13
12
  type: z.literal("article"),
13
+ prefix: z.string(),
14
14
  })
15
15
 
16
16
  /** Blog section: date-sorted posts with tags and index. */
17
17
  export type BlogSection = z.infer<typeof BlogSection>
18
18
  export const BlogSection = BaseSection.extend({
19
19
  type: z.literal("blog"),
20
+ prefix: z.string(),
20
21
  })
21
22
 
22
23
  /** Changelog section: single file or GitHub releases, split per version. */
23
24
  export type ChangelogSection = z.infer<typeof ChangelogSection>
24
25
  export const ChangelogSection = BaseSection.extend({
25
26
  type: z.literal("changelog"),
27
+ prefix: z.string(),
26
28
  source: z.string(),
27
29
  version: z.boolean().optional(),
28
30
  })
29
31
 
32
+ /** External section: renders as a link to an arbitrary URL. */
33
+ export type ExternalSection = z.infer<typeof ExternalSection>
34
+ export const ExternalSection = BaseSection.extend({
35
+ type: z.literal("external"),
36
+ url: z.string(),
37
+ })
38
+
30
39
  /**
31
40
  * A site section. Discriminated on `type`.
32
41
  * When `type` is omitted, it defaults to "article" so plain
@@ -41,5 +50,10 @@ export const Section = z.preprocess(
41
50
  const record = input as Record<string, unknown>
42
51
  return "type" in record ? record : { ...record, type: "article" }
43
52
  },
44
- z.discriminatedUnion("type", [ArticleSection, BlogSection, ChangelogSection]),
53
+ z.discriminatedUnion("type", [
54
+ ArticleSection,
55
+ BlogSection,
56
+ ChangelogSection,
57
+ ExternalSection,
58
+ ]),
45
59
  )
@@ -5,12 +5,12 @@ import { resolveAssetPath } from "../helpers/resolve-asset-path.ts"
5
5
 
6
6
  /** Remark plugin that resolves relative image paths and unwraps image-only paragraphs */
7
7
  export const remarkImage: Plugin<
8
- [{ filePath: string; root: string }],
8
+ [{ filePath: string; root: string; base?: string }],
9
9
  Root
10
- > = ({ filePath, root }) => {
10
+ > = ({ filePath, root, base }) => {
11
11
  return tree => {
12
12
  visit(tree, "image", node => {
13
- node.url = resolveAssetPath(node.url, filePath, root)
13
+ node.url = resolveAssetPath(node.url, filePath, root, base)
14
14
  })
15
15
 
16
16
  visit(tree, "paragraph", (node, index, parent) => {
@@ -50,6 +50,11 @@ export function livemark(opts: LivemarkOptions): Plugin {
50
50
  targetDir = buildTarget(opts.config.configPath)
51
51
  const base: ViteUserConfig = {
52
52
  root: targetDir,
53
+ // Mount everything (asset URLs, modulepreloads, dev server) under
54
+ // `config.base` so the same build works at root or at a sub-path
55
+ // (e.g. GitHub Pages `https://user.github.io/repo/`). The router
56
+ // and asset-path resolver mirror this at runtime.
57
+ base: opts.config.base ? `${opts.config.base}/` : "/",
53
58
  // outDir lives outside the target root (in the consumer's
54
59
  // .livemark/) so the consumer can find their deployable output
55
60
  // without digging into node_modules. Vite's default refusal to
package/source/router.tsx CHANGED
@@ -1,3 +1,4 @@
1
+ import { config } from "livemark:virtual"
1
2
  import { QueryClient } from "@tanstack/react-query"
2
3
  import { createRouter } from "@tanstack/react-router"
3
4
  import { setupRouterSsrQueryIntegration } from "@tanstack/react-router-ssr-query"
@@ -10,6 +11,7 @@ export function getRouter() {
10
11
 
11
12
  const router = createRouter({
12
13
  routeTree,
14
+ basepath: config.base,
13
15
  context: { queryClient },
14
16
  trailingSlash: "always",
15
17
  defaultPreload: "intent",
@@ -15,13 +15,17 @@ import { DefaultCatchBoundary } from "../components/DefaultCatchBoundary.tsx"
15
15
  import { NotFound } from "../components/NotFound.tsx"
16
16
  import { Toaster } from "../elements/sonner.tsx"
17
17
  import { faviconType } from "../helpers/favicon.ts"
18
+ import { prefixUrl } from "../helpers/prefix-url.ts"
18
19
  import defaultFavicon from "../assets/logo.svg"
19
20
  import generalCss from "../styles/general.css?url"
20
21
  import markdownCss from "../styles/markdown.css?url"
21
22
 
22
23
  export const Route = createRootRoute({
23
24
  head: () => {
24
- const favicon = config.favicon ?? config.logo ?? defaultFavicon
25
+ const favicon = prefixUrl(
26
+ config.favicon ?? config.logo ?? defaultFavicon,
27
+ config.base,
28
+ )
25
29
  return {
26
30
  meta: [
27
31
  { charSet: "utf-8" },
@@ -18,12 +18,26 @@ export default defineConfig({
18
18
  contentCollections({ configPath: "content-collections.ts" }),
19
19
  tanstackStart({
20
20
  srcDirectory: ".",
21
- pages: [{ path: "/" }],
22
- prerender: { enabled: true, crawlLinks: true },
21
+ // Seed path must match what the crawler produces (router-emitted
22
+ // hrefs include the basepath), otherwise prerender records both
23
+ // shapes and the sitemap ends up with duplicate entries.
24
+ pages: [{ path: config.base ? `${config.base}/` : "/" }],
25
+ // Disable route-tree auto-discovery: it produces basepath-naive
26
+ // paths (e.g. `/`) that duplicate the crawl-derived prefixed paths,
27
+ // bloating pages.json and sitemap. Crawling from the seed already
28
+ // reaches every linked page.
29
+ prerender: {
30
+ enabled: true,
31
+ crawlLinks: true,
32
+ autoStaticPathsDiscovery: false,
33
+ },
23
34
  sitemap: config.site
24
35
  ? { enabled: true, host: config.site }
25
36
  : { enabled: false },
26
- router: { generatedRouteTree: "routeTree.gen.ts" },
37
+ router: {
38
+ generatedRouteTree: "routeTree.gen.ts",
39
+ basepath: config.base,
40
+ },
27
41
  }),
28
42
  react(),
29
43
  svgr(),