livemark 0.13.1 → 0.15.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 (37) hide show
  1. package/build/components/Layout.js +3 -2
  2. package/build/components/SidebarArticles.js +10 -12
  3. package/build/components/SidebarLinks.js +3 -2
  4. package/build/components/SiteTitle.js +3 -2
  5. package/build/content-collections.js +3 -2
  6. package/build/helpers/prefix-url.d.ts +6 -0
  7. package/build/helpers/prefix-url.js +24 -0
  8. package/build/helpers/prefix-url.unit.d.ts +1 -0
  9. package/build/helpers/prefix-url.unit.js +45 -0
  10. package/build/helpers/resolve-asset-path.d.ts +1 -1
  11. package/build/helpers/resolve-asset-path.js +9 -5
  12. package/build/helpers/resolve-asset-path.unit.d.ts +1 -0
  13. package/build/helpers/resolve-asset-path.unit.js +34 -0
  14. package/build/models/config.d.ts +3 -0
  15. package/build/models/config.js +11 -1
  16. package/build/plugins/remark-image.d.ts +1 -0
  17. package/build/plugins/remark-image.js +3 -3
  18. package/build/plugins/vite-livemark.js +6 -1
  19. package/build/router.js +3 -1
  20. package/build/routes/__root.js +3 -2
  21. package/build/vite.config.js +18 -4
  22. package/package.json +1 -1
  23. package/source/components/Layout.tsx +2 -1
  24. package/source/components/SidebarArticles.tsx +34 -30
  25. package/source/components/SidebarLinks.tsx +2 -1
  26. package/source/components/SiteTitle.tsx +2 -1
  27. package/source/content-collections.ts +2 -1
  28. package/source/helpers/prefix-url.ts +22 -0
  29. package/source/helpers/prefix-url.unit.ts +57 -0
  30. package/source/helpers/resolve-asset-path.ts +12 -5
  31. package/source/helpers/resolve-asset-path.unit.ts +59 -0
  32. package/source/models/config.ts +9 -0
  33. package/source/plugins/remark-image.ts +3 -3
  34. package/source/plugins/vite-livemark.ts +5 -0
  35. package/source/router.tsx +2 -0
  36. package/source/routes/__root.tsx +5 -1
  37. package/source/vite.config.ts +17 -3
@@ -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
+ })
@@ -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,
@@ -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(),