diff --git a/apps/landing/astro.config.ts b/apps/landing/astro.config.ts
index 6671b981..20c6e51a 100644
--- a/apps/landing/astro.config.ts
+++ b/apps/landing/astro.config.ts
@@ -59,8 +59,7 @@ function createBundleReportPlugin() {
export default defineConfig({
adapter: cloudflare({
imageService: { build: "compile", runtime: "cloudflare-binding" },
- // Keep Shiki-backed Astro code rendering out of workerd's prerender path.
- prerenderEnvironment: "node",
+ prerenderEnvironment: "workerd",
}),
build: {
// Keep page CSS out of a separate render-blocking request for first-load LCP.
diff --git a/apps/landing/wrangler.toml b/apps/landing/wrangler.toml
index 62f4d573..d1990571 100644
--- a/apps/landing/wrangler.toml
+++ b/apps/landing/wrangler.toml
@@ -5,6 +5,7 @@
name = "onequery-landing"
main = "./src/worker.ts"
compatibility_date = "2026-04-14"
+compatibility_flags = ["nodejs_compat"]
logpush = true
preview_urls = true
routes = [{ pattern = "onequery.dev", custom_domain = true }]
diff --git a/packages/astro-agent-markdown/src/content.test.ts b/packages/astro-agent-markdown/src/content.test.ts
index c1a49c6e..b84512c3 100644
--- a/packages/astro-agent-markdown/src/content.test.ts
+++ b/packages/astro-agent-markdown/src/content.test.ts
@@ -75,6 +75,15 @@ title: Debugging production
).toBe("debug-production-agent-runs-with-onequery");
});
+ it("does not treat a collection index route as a content entry", () => {
+ expect(
+ getContentEntryIdForMarkdownPath({
+ markdownPath: "/blog/index.md",
+ routePrefix: "/blog",
+ })
+ ).toBeUndefined();
+ });
+
it("finds a content entry by negotiated page pathname", async () => {
await expect(
getContentMarkdownForPath({
diff --git a/packages/astro-agent-markdown/src/content.ts b/packages/astro-agent-markdown/src/content.ts
index 7321c40c..48b43599 100644
--- a/packages/astro-agent-markdown/src/content.ts
+++ b/packages/astro-agent-markdown/src/content.ts
@@ -97,7 +97,12 @@ export function getContentEntryIdForMarkdownPath(input: {
return undefined;
}
- return input.markdownPath.slice(routeBase.length, -markdownSuffix.length);
+ const entryId = input.markdownPath.slice(
+ routeBase.length,
+ -markdownSuffix.length
+ );
+
+ return entryId.length > 0 ? entryId : undefined;
}
export async function getContentMarkdownForPath(input: {
diff --git a/packages/astro-agent-markdown/src/dev-middleware.test.ts b/packages/astro-agent-markdown/src/dev-middleware.test.ts
index 684ba6bc..727dbf02 100644
--- a/packages/astro-agent-markdown/src/dev-middleware.test.ts
+++ b/packages/astro-agent-markdown/src/dev-middleware.test.ts
@@ -87,6 +87,46 @@ Context, not keys.
`);
});
+ it("falls back to HTML conversion for a content collection index route", async () => {
+ const getMarkdown = vi.fn(async () => "should not read content entry");
+ const onRequest = createDevMarkdownMiddleware({
+ contentRoutes: [
+ {
+ getMarkdown,
+ routePrefix: "/blog",
+ },
+ ],
+ });
+ const next = vi.fn(
+ async () =>
+ new Response("Blog
Latest updates.
", {
+ headers: { "Content-Type": "text/html; charset=utf-8" },
+ })
+ );
+
+ const response = await onRequest(
+ createContext(
+ new Request("http://localhost:4546/blog/", {
+ headers: { Accept: "text/markdown" },
+ })
+ ),
+ next as unknown as MiddlewareNext
+ );
+
+ expect(getMarkdown).not.toHaveBeenCalled();
+ expect(next).toHaveBeenCalledOnce();
+ expect(response).toBeInstanceOf(Response);
+
+ const markdownResponse = response as Response;
+ expect(markdownResponse.headers.get("Content-Type")).toBe(
+ MARKDOWN_CONTENT_TYPE
+ );
+ expect(await markdownResponse.text()).toBe(`# Blog
+
+Latest updates.
+`);
+ });
+
it("renders HTML-derived HEAD requests with GET so token counts are available", async () => {
const onRequest = createDevMarkdownMiddleware();
const next = vi.fn(
diff --git a/packages/astro-agent-markdown/src/html-sidecars.test.ts b/packages/astro-agent-markdown/src/html-sidecars.test.ts
index 20f55398..c48fee70 100644
--- a/packages/astro-agent-markdown/src/html-sidecars.test.ts
+++ b/packages/astro-agent-markdown/src/html-sidecars.test.ts
@@ -67,6 +67,29 @@ describe("HTML Markdown sidecars", () => {
expect(count).toBe(2);
});
+ it("exports Markdown sidecars for HTML pages missing from the assets map", async () => {
+ const outputDir = await fs.mkdtemp(path.join(os.tmpdir(), "html-md-"));
+
+ await fs.mkdir(path.join(outputDir, "docs", "guide"), {
+ recursive: true,
+ });
+ await fs.writeFile(
+ path.join(outputDir, "docs", "guide", "index.html"),
+ "Guide
Run bounded queries.
"
+ );
+
+ const count = await exportHtmlMarkdownSidecars({
+ assets: new Map(),
+ dir: pathToFileURL(`${outputDir}/`),
+ logger: createLogger(),
+ });
+
+ await expect(
+ fs.readFile(path.join(outputDir, "docs", "guide", "index.md"), "utf8")
+ ).resolves.toContain("# Guide");
+ expect(count).toBe(1);
+ });
+
it("does not overwrite content collection Markdown sidecars", async () => {
const outputDir = await fs.mkdtemp(path.join(os.tmpdir(), "html-md-"));
const blogDir = path.join(outputDir, "blog/post");
diff --git a/packages/astro-agent-markdown/src/html-sidecars.ts b/packages/astro-agent-markdown/src/html-sidecars.ts
index 28e4ccec..1334378f 100644
--- a/packages/astro-agent-markdown/src/html-sidecars.ts
+++ b/packages/astro-agent-markdown/src/html-sidecars.ts
@@ -66,6 +66,33 @@ async function exportHtmlFile(input: {
return true;
}
+async function collectHtmlFiles(directory: string) {
+ const htmlPaths: string[] = [];
+
+ async function visit(currentDirectory: string) {
+ const entries = await fs.readdir(currentDirectory, { withFileTypes: true });
+
+ await Promise.all(
+ entries.map(async (entry) => {
+ const entryPath = path.join(currentDirectory, entry.name);
+
+ if (entry.isDirectory()) {
+ await visit(entryPath);
+ return;
+ }
+
+ if (entry.isFile() && entry.name.endsWith(HTML_EXTENSION)) {
+ htmlPaths.push(entryPath);
+ }
+ })
+ );
+ }
+
+ await visit(directory);
+
+ return htmlPaths;
+}
+
export async function exportHtmlMarkdownSidecars(
options: ExportHtmlMarkdownSidecarsOptions
) {
@@ -81,6 +108,12 @@ export async function exportHtmlMarkdownSidecars(
}
}
+ // Workerd prerender builds can omit static HTML pages from the hook's assets
+ // map, so scan the emitted client directory as the source of truth.
+ for (const htmlPath of await collectHtmlFiles(outputDir)) {
+ htmlAssetPaths.add(htmlPath);
+ }
+
for (const htmlPath of htmlAssetPaths) {
const routePath = getHtmlRoutePath(path.relative(outputDir, htmlPath));