feat: add getBasePath and resolveBasePath for subdirectory deployments

Add utility functions to resolve links correctly when Quartz is deployed
to a subdirectory (e.g., user.github.io/repository). getBasePath() reads
the base path from a data attribute on <body>, and resolveBasePath()
prepends it to slugs for client-side navigation.
This commit is contained in:
saberzero1 2026-03-18 11:50:30 +01:00
parent 81189f697c
commit d7d93758b2
No known key found for this signature in database
9 changed files with 78 additions and 6 deletions

2
dist/index.d.ts vendored
View file

@ -1,4 +1,4 @@
export { RelativeURL, SimpleSlug, TransformOptions, endsWith, getAllSegmentPrefixes, getFileExtension, getFullSlug, getFullSlugFromUrl, isAbsoluteURL, isFilePath, isFolderPath, isFullSlug, isRelativeURL, isSimpleSlug, joinSegments, pathToRoot, resolvePath, resolveRelative, simplifySlug, slugTag, slugifyFilePath, splitAnchor, stripSlashes, transformInternalLink, transformLink, trimSuffix } from './path.js';
export { RelativeURL, SimpleSlug, TransformOptions, endsWith, getAllSegmentPrefixes, getBasePath, getFileExtension, getFullSlug, getFullSlugFromUrl, isAbsoluteURL, isFilePath, isFolderPath, isFullSlug, isRelativeURL, isSimpleSlug, joinSegments, pathToRoot, resolveBasePath, resolvePath, resolveRelative, simplifySlug, slugTag, slugifyFilePath, splitAnchor, stripSlashes, transformInternalLink, transformLink, trimSuffix } from './path.js';
export { normalizeRelativeURLs, registerEscapeHandler, removeAllChildren } from './dom.js';
export { capitalize, classNames } from './lang.js';
export { escapeHTML, unescapeHTML } from './escape.js';

11
dist/index.js vendored
View file

@ -75,6 +75,15 @@ function resolvePath(to) {
if (to.startsWith("/")) return to;
return "/" + to;
}
function getBasePath() {
if (typeof document === "undefined") return "";
return document.body?.dataset?.basepath ?? "";
}
function resolveBasePath(to, basePath) {
const base = basePath ?? getBasePath();
const slug = to.startsWith("/") ? to : "/" + to;
return base + slug;
}
function endsWith(s, suffix) {
return s === suffix || s.endsWith("/" + suffix);
}
@ -275,6 +284,6 @@ function htmlToJsx(tree, components) {
});
}
export { capitalize, classNames, endsWith, escapeHTML, getAllSegmentPrefixes, getFileExtension, getFullSlug, getFullSlugFromUrl, htmlToJsx, isAbsoluteURL, isFilePath, isFolderPath, isFullSlug, isRelativeURL, isSimpleSlug, joinSegments, normalizeRelativeURLs, pathToRoot, registerEscapeHandler, removeAllChildren, resolvePath, resolveRelative, simplifySlug, slugTag, slugifyFilePath, splitAnchor, stripSlashes, transformInternalLink, transformLink, trimSuffix, unescapeHTML };
export { capitalize, classNames, endsWith, escapeHTML, getAllSegmentPrefixes, getBasePath, getFileExtension, getFullSlug, getFullSlugFromUrl, htmlToJsx, isAbsoluteURL, isFilePath, isFolderPath, isFullSlug, isRelativeURL, isSimpleSlug, joinSegments, normalizeRelativeURLs, pathToRoot, registerEscapeHandler, removeAllChildren, resolveBasePath, resolvePath, resolveRelative, simplifySlug, slugTag, slugifyFilePath, splitAnchor, stripSlashes, transformInternalLink, transformLink, trimSuffix, unescapeHTML };
//# sourceMappingURL=index.js.map
//# sourceMappingURL=index.js.map

2
dist/index.js.map vendored

File diff suppressed because one or more lines are too long

8
dist/path.d.ts vendored
View file

@ -24,6 +24,12 @@ declare function getFullSlugFromUrl(): FullSlug;
declare function slugifyFilePath(fp: FilePath, excludeExt?: boolean): FullSlug;
declare function joinSegments(...args: string[]): string;
declare function resolvePath(to: string): string;
/** Read the base path injected at build time via `data-basepath` on `<body>`.
* Returns `""` for root deployments, e.g. `"/repository"` for subdirectory. */
declare function getBasePath(): string;
/** Resolve a slug to an absolute URL path, prepending the site's base path.
* e.g. `resolveBasePath("features/Callouts")` `"/repository/features/Callouts"` */
declare function resolveBasePath(to: string, basePath?: string): string;
declare function endsWith(s: string, suffix: string): boolean;
declare function trimSuffix(s: string, suffix: string): string;
declare function stripSlashes(s: string, onlyStripPrefix?: boolean): string;
@ -37,4 +43,4 @@ declare function slugTag(tag: string): string;
declare function transformInternalLink(link: string): RelativeURL;
declare function transformLink(src: FullSlug, target: string, opts: TransformOptions): RelativeURL;
export { type RelativeURL, type SimpleSlug, type TransformOptions, endsWith, getAllSegmentPrefixes, getFileExtension, getFullSlug, getFullSlugFromUrl, isAbsoluteURL, isFilePath, isFolderPath, isFullSlug, isRelativeURL, isSimpleSlug, joinSegments, pathToRoot, resolvePath, resolveRelative, simplifySlug, slugTag, slugifyFilePath, splitAnchor, stripSlashes, transformInternalLink, transformLink, trimSuffix };
export { type RelativeURL, type SimpleSlug, type TransformOptions, endsWith, getAllSegmentPrefixes, getBasePath, getFileExtension, getFullSlug, getFullSlugFromUrl, isAbsoluteURL, isFilePath, isFolderPath, isFullSlug, isRelativeURL, isSimpleSlug, joinSegments, pathToRoot, resolveBasePath, resolvePath, resolveRelative, simplifySlug, slugTag, slugifyFilePath, splitAnchor, stripSlashes, transformInternalLink, transformLink, trimSuffix };

11
dist/path.js vendored
View file

@ -72,6 +72,15 @@ function resolvePath(to) {
if (to.startsWith("/")) return to;
return "/" + to;
}
function getBasePath() {
if (typeof document === "undefined") return "";
return document.body?.dataset?.basepath ?? "";
}
function resolveBasePath(to, basePath) {
const base = basePath ?? getBasePath();
const slug = to.startsWith("/") ? to : "/" + to;
return base + slug;
}
function endsWith(s, suffix) {
return s === suffix || s.endsWith("/" + suffix);
}
@ -184,6 +193,6 @@ function _addRelativeToStart(s) {
return s;
}
export { endsWith, getAllSegmentPrefixes, getFileExtension, getFullSlug, getFullSlugFromUrl, isAbsoluteURL, isFilePath, isFolderPath, isFullSlug, isRelativeURL, isSimpleSlug, joinSegments, pathToRoot, resolvePath, resolveRelative, simplifySlug, slugTag, slugifyFilePath, splitAnchor, stripSlashes, transformInternalLink, transformLink, trimSuffix };
export { endsWith, getAllSegmentPrefixes, getBasePath, getFileExtension, getFullSlug, getFullSlugFromUrl, isAbsoluteURL, isFilePath, isFolderPath, isFullSlug, isRelativeURL, isSimpleSlug, joinSegments, pathToRoot, resolveBasePath, resolvePath, resolveRelative, simplifySlug, slugTag, slugifyFilePath, splitAnchor, stripSlashes, transformInternalLink, transformLink, trimSuffix };
//# sourceMappingURL=path.js.map
//# sourceMappingURL=path.js.map

2
dist/path.js.map vendored

File diff suppressed because one or more lines are too long

View file

@ -4,6 +4,8 @@ export {
getFullSlugFromUrl,
joinSegments,
resolvePath,
getBasePath,
resolveBasePath,
endsWith,
trimSuffix,
stripSlashes,

View file

@ -107,6 +107,21 @@ export function resolvePath(to: string): string {
return "/" + to;
}
/** Read the base path injected at build time via `data-basepath` on `<body>`.
* Returns `""` for root deployments, e.g. `"/repository"` for subdirectory. */
export function getBasePath(): string {
if (typeof document === "undefined") return "";
return document.body?.dataset?.basepath ?? "";
}
/** Resolve a slug to an absolute URL path, prepending the site's base path.
* e.g. `resolveBasePath("features/Callouts")` `"/repository/features/Callouts"` */
export function resolveBasePath(to: string, basePath?: string): string {
const base = basePath ?? getBasePath();
const slug = to.startsWith("/") ? to : "/" + to;
return base + slug;
}
export function endsWith(s: string, suffix: string): boolean {
return s === suffix || s.endsWith("/" + suffix);
}

View file

@ -3,6 +3,7 @@ import {
simplifySlug,
joinSegments,
resolvePath,
resolveBasePath,
endsWith,
trimSuffix,
stripSlashes,
@ -105,3 +106,33 @@ describe("getAllSegmentPrefixes", () => {
expect(getAllSegmentPrefixes("a/b/c")).toEqual(["a", "a/b", "a/b/c"]);
});
});
describe("resolveBasePath", () => {
it("prepends explicit basePath to slug", () => {
expect(resolveBasePath("features/Callouts", "/repository")).toBe(
"/repository/features/Callouts",
);
});
it("prepends explicit basePath to slug with leading slash", () => {
expect(resolveBasePath("/features/Callouts", "/repository")).toBe(
"/repository/features/Callouts",
);
});
it("returns just /slug when basePath is empty (root deployment)", () => {
expect(resolveBasePath("features/Callouts", "")).toBe("/features/Callouts");
});
it("handles empty slug with basePath", () => {
expect(resolveBasePath("", "/repository")).toBe("/repository/");
});
it("handles empty slug with empty basePath", () => {
expect(resolveBasePath("", "")).toBe("/");
});
it("falls back to getBasePath() when no basePath argument (server-side returns empty)", () => {
expect(resolveBasePath("page")).toBe("/page");
});
});