diff --git a/.changeset/base-aware-starter-navigation.md b/.changeset/base-aware-starter-navigation.md
new file mode 100644
index 00000000..6916d98b
--- /dev/null
+++ b/.changeset/base-aware-starter-navigation.md
@@ -0,0 +1,6 @@
+---
+"@cloudflare/nimbus-docs": patch
+"@cloudflare/create-nimbus-docs": patch
+---
+
+Keep framework assets and starter navigation working when an Astro site is deployed under a sub-path.
diff --git a/packages/nimbus-docs/src/components/NimbusHead.astro b/packages/nimbus-docs/src/components/NimbusHead.astro
index c2564c29..c4ddf28c 100644
--- a/packages/nimbus-docs/src/components/NimbusHead.astro
+++ b/packages/nimbus-docs/src/components/NimbusHead.astro
@@ -165,7 +165,7 @@ const ogType = isHomePage ? "website" : "article";
// Favicon precedence: svg > ico > png. The first file that exists wins;
// when none exists we still emit the svg link so users who drop one in
// later don't need to touch the layout.
-const faviconHref = `${import.meta.env.BASE_URL}${headDefaults.favicon.file}`;
+const faviconHref = withBase(headDefaults.favicon.file, baseUrl);
const faviconType = headDefaults.favicon.type;
// Social image precedence: page prop > config > `public/opengraph.png` >
@@ -205,7 +205,7 @@ const structuredData = canonical
// Per-HTML last-element-wins semantics: page extras come after config
// extras so per-page overrides win for any duplicated single-value tag.
const mergedHead = [...(config.head ?? []), ...pageHead];
-const shikiCssHref = `${import.meta.env.BASE_URL}_nimbus/shiki.css${import.meta.env.DEV ? `?v=${Date.now()}` : ""}`;
+const shikiCssHref = `${withBase("/_nimbus/shiki.css", baseUrl)}${import.meta.env.DEV ? `?v=${Date.now()}` : ""}`;
---
{fullTitle}
diff --git a/packages/nimbus-docs/test/subpath-template-contract.test.ts b/packages/nimbus-docs/test/subpath-template-contract.test.ts
new file mode 100644
index 00000000..2860aa86
--- /dev/null
+++ b/packages/nimbus-docs/test/subpath-template-contract.test.ts
@@ -0,0 +1,69 @@
+import assert from "node:assert/strict";
+import { readFileSync } from "node:fs";
+import { test } from "node:test";
+import { fileURLToPath } from "node:url";
+import path from "node:path";
+
+const repoRoot = path.resolve(
+ fileURLToPath(new URL("../../..", import.meta.url)),
+);
+
+function source(relativePath: string): string {
+ return readFileSync(path.join(repoRoot, relativePath), "utf8");
+}
+
+test("framework and starter-owned navigation apply Astro's base path", () => {
+ const head = source("packages/nimbus-docs/src/components/NimbusHead.astro");
+ assert.match(
+ head,
+ /const faviconHref = withBase\(headDefaults\.favicon\.file, baseUrl\);/,
+ );
+ assert.match(head, /withBase\("\/_nimbus\/shiki\.css", baseUrl\)/);
+
+ const starterRoot = "packages/nimbus-starter-source/src";
+ const header = source(`${starterRoot}/components/Header.astro`);
+ assert.match(
+ header,
+ /const homeHref = withBase\("\/", import\.meta\.env\.BASE_URL\);/,
+ );
+ assert.match(header, /href=\{homeHref\}/);
+ assert.match(
+ header,
+ /href=\{withBase\(section\.href, import\.meta\.env\.BASE_URL\)\}/,
+ );
+
+ const sidebarLink = source(
+ `${starterRoot}/components/ui/sidebar/SidebarLink.astro`,
+ );
+ assert.match(
+ sidebarLink,
+ /const hrefWithBase = withBase\(href, import\.meta\.env\.BASE_URL\);/,
+ );
+ assert.match(sidebarLink, /href=\{hrefWithBase\}/);
+
+ const breadcrumbs = source(
+ `${starterRoot}/components/ui/breadcrumbs/Breadcrumbs.astro`,
+ );
+ assert.match(breadcrumbs, /href=\{hrefWithBase\(crumb\.href\)\}/);
+
+ const pagination = source(
+ `${starterRoot}/components/ui/pagination/Pagination.astro`,
+ );
+ assert.match(
+ pagination,
+ /const prevHref = prev && withBase\(prev\.href, import\.meta\.env\.BASE_URL\);/,
+ );
+ assert.match(
+ pagination,
+ /const nextHref = next && withBase\(next\.href, import\.meta\.env\.BASE_URL\);/,
+ );
+
+ assert.match(
+ source(`${starterRoot}/layouts/DocsLayout.astro`),
+ /const homeHref = withBase\("\/", import\.meta\.env\.BASE_URL\);/,
+ );
+ assert.match(
+ source(`${starterRoot}/pages/404.astro`),
+ /const homeHref = withBase\("\/", import\.meta\.env\.BASE_URL\);/,
+ );
+});
diff --git a/packages/nimbus-starter-source/src/components/Header.astro b/packages/nimbus-starter-source/src/components/Header.astro
index 33d2bf15..4b622c3d 100644
--- a/packages/nimbus-starter-source/src/components/Header.astro
+++ b/packages/nimbus-starter-source/src/components/Header.astro
@@ -6,7 +6,7 @@ import { LinkButton } from "./ui/link-button";
import { ThemeToggle } from "./ui/theme-toggle";
import SearchTrigger from "./ui/search/SearchTrigger.astro";
import { config } from "virtual:nimbus/config";
-import { getSidebarSections } from "@cloudflare/nimbus-docs/runtime";
+import { getSidebarSections, withBase } from "@cloudflare/nimbus-docs/runtime";
interface Props {
/** Astro collection id for the current page, forwarded from DocsLayout. */
@@ -32,13 +32,14 @@ const currentSlug = Astro.url.pathname.replace(/\/$/, "") || "/";
const sections =
sectionsProp ?? (await getSidebarSections(currentSlug, { collection }));
const showSections = sections.length >= 2;
+const homeHref = withBase("/", import.meta.env.BASE_URL);
---
-
+
{config.title}
@@ -49,7 +50,7 @@ const showSections = sections.length >= 2;