From 605922ca32bfc5713054416cc83632b3390050c4 Mon Sep 17 00:00:00 2001
From: logelog <194732487+logelog@users.noreply.github.com>
Date: Thu, 16 Jul 2026 02:07:15 +0200
Subject: [PATCH 1/2] fix(head): avoid CSP warnings when disabled
---
.changeset/quiet-heads-rest.md | 5 ++++
packages/core/src/astro/integration/index.ts | 3 ++
packages/core/src/components/EmDashHead.astro | 11 ++------
packages/core/src/page/metadata.ts | 27 ++++++++++++++++++
packages/core/src/virtual-modules.d.ts | 1 +
.../tests/unit/plugins/page-metadata.test.ts | 28 ++++++++++++++++++-
6 files changed, 66 insertions(+), 9 deletions(-)
create mode 100644 .changeset/quiet-heads-rest.md
diff --git a/.changeset/quiet-heads-rest.md b/.changeset/quiet-heads-rest.md
new file mode 100644
index 0000000000..18bd4d076d
--- /dev/null
+++ b/.changeset/quiet-heads-rest.md
@@ -0,0 +1,5 @@
+---
+"emdash": patch
+---
+
+Avoid production render warnings when Astro's built-in CSP is disabled while preserving JSON-LD script hashes when it is enabled.
diff --git a/packages/core/src/astro/integration/index.ts b/packages/core/src/astro/integration/index.ts
index 47985fcab3..96798766e9 100644
--- a/packages/core/src/astro/integration/index.ts
+++ b/packages/core/src/astro/integration/index.ts
@@ -381,6 +381,9 @@ export function emdash(config: EmDashConfig = {}): AstroIntegration {
if (astroVersion !== undefined) {
serializableConfig.astroVersion = astroVersion;
}
+ // EmDashHead must not access Astro.csp when the host has disabled
+ // Astro's built-in CSP runtime; Astro logs a warning for that access.
+ serializableConfig.astroCspEnabled = Boolean(astroConfig.security.csp);
// Extract i18n config from Astro config
// Astro locales can be strings OR { path, codes } objects — normalize to paths
if (astroConfig.i18n) {
diff --git a/packages/core/src/components/EmDashHead.astro b/packages/core/src/components/EmDashHead.astro
index 999952cb5e..7155ca3741 100644
--- a/packages/core/src/components/EmDashHead.astro
+++ b/packages/core/src/components/EmDashHead.astro
@@ -17,8 +17,9 @@
* ```
*/
import type { PublicPageContext, PageMetadataContribution } from "../plugins/types.js";
+import virtualConfig from "virtual:emdash/config";
import {
- createSha256CspHash,
+ registerJsonLdCspHashes,
resolvePageMetadata,
renderPageMetadata,
type ResolvedPageMetadata,
@@ -124,13 +125,7 @@ if (runtime) {
metadataHtml = renderPageMetadata(resolved, { includeJsonLd: false });
}
-if (Astro.csp && jsonLdScripts.length > 0) {
- await Promise.all(
- jsonLdScripts.map(async ({ json }) => {
- Astro.csp?.insertScriptHash(await createSha256CspHash(json));
- }),
- );
-}
+await registerJsonLdCspHashes(virtualConfig.astroCspEnabled === true, () => Astro.csp, jsonLdScripts);
---
diff --git a/packages/core/src/page/metadata.ts b/packages/core/src/page/metadata.ts
index 6c015f0319..487feceef1 100644
--- a/packages/core/src/page/metadata.ts
+++ b/packages/core/src/page/metadata.ts
@@ -80,6 +80,33 @@ export async function createSha256CspHash(value: string): Promise {
return `sha256-${btoa(binary)}`;
}
+interface CspScriptHashSink {
+ insertScriptHash(hash: string): void;
+}
+
+/**
+ * Register JSON-LD hashes with Astro's CSP runtime when CSP is enabled.
+ *
+ * The getter stays lazy because Astro warns when `Astro.csp` is accessed on a
+ * project that has not enabled its built-in CSP support.
+ */
+export async function registerJsonLdCspHashes(
+ enabled: boolean,
+ getCsp: () => CspScriptHashSink | undefined,
+ scripts: ReadonlyArray<{ json: string }>,
+): Promise {
+ if (!enabled || scripts.length === 0) return;
+
+ const csp = getCsp();
+ if (!csp) return;
+
+ await Promise.all(
+ scripts.map(async ({ json }) => {
+ csp.insertScriptHash(await createSha256CspHash(json));
+ }),
+ );
+}
+
// ── Merge / dedupe ──────────────────────────────────────────────
/**
diff --git a/packages/core/src/virtual-modules.d.ts b/packages/core/src/virtual-modules.d.ts
index 3ed8fd8c17..6f4ddd4a7a 100644
--- a/packages/core/src/virtual-modules.d.ts
+++ b/packages/core/src/virtual-modules.d.ts
@@ -21,6 +21,7 @@ declare module "virtual:emdash/config" {
authProviders?: AuthProviderDescriptor[];
i18n?: I18nConfig | null;
toolbar?: "server" | "client" | false;
+ astroCspEnabled?: boolean;
}
const config: VirtualConfig;
diff --git a/packages/core/tests/unit/plugins/page-metadata.test.ts b/packages/core/tests/unit/plugins/page-metadata.test.ts
index 40cccef562..abbb9712f3 100644
--- a/packages/core/tests/unit/plugins/page-metadata.test.ts
+++ b/packages/core/tests/unit/plugins/page-metadata.test.ts
@@ -8,7 +8,7 @@
* - HTML attribute escaping
*/
-import { describe, it, expect } from "vitest";
+import { describe, it, expect, vi } from "vitest";
import {
resolvePageMetadata,
@@ -16,6 +16,7 @@ import {
safeJsonLdSerialize,
escapeHtmlAttr,
createSha256CspHash,
+ registerJsonLdCspHashes,
} from "../../../src/page/metadata.js";
import type { PageMetadataContribution } from "../../../src/plugins/types.js";
@@ -317,6 +318,31 @@ describe("createSha256CspHash", () => {
});
});
+describe("registerJsonLdCspHashes", () => {
+ it("does not access Astro's CSP getter when CSP is disabled", async () => {
+ const getCsp = vi.fn(() => {
+ throw new Error("CSP getter must stay lazy");
+ });
+
+ await registerJsonLdCspHashes(false, getCsp, [{ json: '{"@type":"WebSite"}' }]);
+
+ expect(getCsp).not.toHaveBeenCalled();
+ });
+
+ it("registers every JSON-LD hash when CSP is enabled", async () => {
+ const insertScriptHash = vi.fn();
+ const getCsp = vi.fn(() => ({ insertScriptHash }));
+ const scripts = [{ json: '{"@type":"WebSite"}' }, { json: '{"@type":"Organization"}' }];
+
+ await registerJsonLdCspHashes(true, getCsp, scripts);
+
+ expect(getCsp).toHaveBeenCalledOnce();
+ expect(insertScriptHash).toHaveBeenCalledTimes(2);
+ expect(insertScriptHash).toHaveBeenNthCalledWith(1, await createSha256CspHash(scripts[0].json));
+ expect(insertScriptHash).toHaveBeenNthCalledWith(2, await createSha256CspHash(scripts[1].json));
+ });
+});
+
describe("escapeHtmlAttr", () => {
it("escapes double quotes", () => {
expect(escapeHtmlAttr('say "hello"')).toBe("say "hello"");
From 28628ee943b4b1e1bd5486d41cd434464d0cbfbf Mon Sep 17 00:00:00 2001
From: logelog <194732487+logelog@users.noreply.github.com>
Date: Fri, 17 Jul 2026 02:41:17 +0200
Subject: [PATCH 2/2] test(head): avoid CSP hash ordering assumptions
---
packages/core/src/components/EmDashHead.astro | 1 +
packages/core/tests/unit/plugins/page-metadata.test.ts | 6 ++++--
2 files changed, 5 insertions(+), 2 deletions(-)
diff --git a/packages/core/src/components/EmDashHead.astro b/packages/core/src/components/EmDashHead.astro
index 7155ca3741..02a1b7a199 100644
--- a/packages/core/src/components/EmDashHead.astro
+++ b/packages/core/src/components/EmDashHead.astro
@@ -17,6 +17,7 @@
* ```
*/
import type { PublicPageContext, PageMetadataContribution } from "../plugins/types.js";
+// @ts-ignore - virtual module
import virtualConfig from "virtual:emdash/config";
import {
registerJsonLdCspHashes,
diff --git a/packages/core/tests/unit/plugins/page-metadata.test.ts b/packages/core/tests/unit/plugins/page-metadata.test.ts
index abbb9712f3..7d21da53af 100644
--- a/packages/core/tests/unit/plugins/page-metadata.test.ts
+++ b/packages/core/tests/unit/plugins/page-metadata.test.ts
@@ -335,11 +335,13 @@ describe("registerJsonLdCspHashes", () => {
const scripts = [{ json: '{"@type":"WebSite"}' }, { json: '{"@type":"Organization"}' }];
await registerJsonLdCspHashes(true, getCsp, scripts);
+ const hash0 = await createSha256CspHash(scripts[0].json);
+ const hash1 = await createSha256CspHash(scripts[1].json);
expect(getCsp).toHaveBeenCalledOnce();
expect(insertScriptHash).toHaveBeenCalledTimes(2);
- expect(insertScriptHash).toHaveBeenNthCalledWith(1, await createSha256CspHash(scripts[0].json));
- expect(insertScriptHash).toHaveBeenNthCalledWith(2, await createSha256CspHash(scripts[1].json));
+ expect(insertScriptHash).toHaveBeenCalledWith(hash0);
+ expect(insertScriptHash).toHaveBeenCalledWith(hash1);
});
});