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.
- package/build/components/Layout.js +8 -2
- package/build/components/SidebarArticles.js +9 -9
- package/build/components/SidebarLinks.js +3 -2
- package/build/components/SidebarSections.js +8 -2
- package/build/components/SiteTitle.js +3 -2
- package/build/content/article.d.ts +10 -10
- package/build/content/article.js +19 -17
- package/build/content-collections.js +3 -2
- package/build/helpers/article.d.ts +1 -15
- package/build/helpers/article.js +1 -34
- package/build/helpers/prefix-url.d.ts +6 -0
- package/build/helpers/prefix-url.js +24 -0
- package/build/helpers/prefix-url.unit.d.ts +1 -0
- package/build/helpers/prefix-url.unit.js +45 -0
- package/build/helpers/resolve-asset-path.d.ts +1 -1
- package/build/helpers/resolve-asset-path.js +9 -5
- package/build/helpers/resolve-asset-path.unit.d.ts +1 -0
- package/build/helpers/resolve-asset-path.unit.js +34 -0
- package/build/helpers/section.js +2 -1
- package/build/models/article.d.ts +0 -9
- package/build/models/article.js +1 -2
- package/build/models/config.d.ts +39 -11
- package/build/models/config.js +11 -1
- package/build/models/patch.d.ts +0 -1
- package/build/models/section.d.ts +27 -6
- package/build/models/section.js +14 -3
- package/build/plugins/remark-image.d.ts +1 -0
- package/build/plugins/remark-image.js +3 -3
- package/build/plugins/vite-livemark.js +6 -1
- package/build/router.js +3 -1
- package/build/routes/__root.js +3 -2
- package/build/vite.config.js +18 -4
- package/package.json +1 -1
- package/source/components/Layout.tsx +17 -1
- package/source/components/SidebarArticles.tsx +17 -24
- package/source/components/SidebarLinks.tsx +2 -1
- package/source/components/SidebarSections.tsx +18 -1
- package/source/components/SiteTitle.tsx +2 -1
- package/source/content/article.ts +20 -26
- package/source/content-collections.ts +2 -1
- package/source/helpers/article.ts +1 -39
- package/source/helpers/prefix-url.ts +22 -0
- package/source/helpers/prefix-url.unit.ts +57 -0
- package/source/helpers/resolve-asset-path.ts +12 -5
- package/source/helpers/resolve-asset-path.unit.ts +59 -0
- package/source/helpers/section.ts +1 -0
- package/source/models/article.ts +0 -10
- package/source/models/config.ts +9 -0
- package/source/models/section.ts +16 -2
- package/source/plugins/remark-image.ts +3 -3
- package/source/plugins/vite-livemark.ts +5 -0
- package/source/router.tsx +2 -0
- package/source/routes/__root.tsx +5 -1
- 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 {
|
|
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
|
|
24
|
-
export const
|
|
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
|
|
30
|
-
export const flatArticles =
|
|
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
|
-
/**
|
|
46
|
-
|
|
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 (
|
|
55
|
-
const sectionByPrefix = new Map(
|
|
56
|
-
const buckets = partitionBySection(sortedArticles,
|
|
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
|
|
68
|
-
|
|
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 (!
|
|
78
|
-
return matchSection(path,
|
|
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 {
|
|
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(
|
|
5
|
-
|
|
6
|
-
|
|
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
|
-
|
|
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
|
+
})
|
package/source/models/article.ts
CHANGED
|
@@ -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
|
-
}
|
package/source/models/config.ts
CHANGED
|
@@ -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,
|
package/source/models/section.ts
CHANGED
|
@@ -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", [
|
|
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",
|
package/source/routes/__root.tsx
CHANGED
|
@@ -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 =
|
|
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" },
|
package/source/vite.config.ts
CHANGED
|
@@ -18,12 +18,26 @@ export default defineConfig({
|
|
|
18
18
|
contentCollections({ configPath: "content-collections.ts" }),
|
|
19
19
|
tanstackStart({
|
|
20
20
|
srcDirectory: ".",
|
|
21
|
-
|
|
22
|
-
|
|
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: {
|
|
37
|
+
router: {
|
|
38
|
+
generatedRouteTree: "routeTree.gen.ts",
|
|
39
|
+
basepath: config.base,
|
|
40
|
+
},
|
|
27
41
|
}),
|
|
28
42
|
react(),
|
|
29
43
|
svgr(),
|