nuxt-ai-ready 3.3.1 → 3.4.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.
- package/dist/chunks/agent-skills.mjs +1 -1
- package/dist/chunks/prerender.mjs +12 -1
- package/dist/module.d.mts +1 -0
- package/dist/module.json +1 -1
- package/dist/module.mjs +1 -1
- package/dist/runtime/server/middleware/markdown.prerender.js +9 -1
- package/dist/runtime/server/plugins/content-signal-capture-nitro3.prerender.d.ts +2 -0
- package/dist/runtime/server/plugins/content-signal-capture-nitro3.prerender.js +13 -0
- package/dist/runtime/server/plugins/content-signal-capture.prerender.d.ts +2 -0
- package/dist/runtime/server/plugins/content-signal-capture.prerender.js +13 -0
- package/dist/runtime/server/utils/prerender-html.d.ts +3 -0
- package/dist/runtime/server/utils/prerender-html.js +12 -0
- package/dist/shared/{nuxt-ai-ready.CKXM1jnE.mjs → nuxt-ai-ready.O6_9Egm8.mjs} +366 -244
- package/package.json +1 -1
|
@@ -2,7 +2,7 @@ import { createHash } from 'node:crypto';
|
|
|
2
2
|
import { realpath, readFile, readdir } from 'node:fs/promises';
|
|
3
3
|
import { join, resolve, isAbsolute, relative, sep } from 'node:path';
|
|
4
4
|
import { parseDocument } from 'yaml';
|
|
5
|
-
import { A as AGENT_SKILLS_SCHEMA } from '../shared/nuxt-ai-ready.
|
|
5
|
+
import { A as AGENT_SKILLS_SCHEMA } from '../shared/nuxt-ai-ready.O6_9Egm8.mjs';
|
|
6
6
|
import 'node:module';
|
|
7
7
|
import 'node:url';
|
|
8
8
|
import '@nuxt/kit';
|
|
@@ -6,7 +6,7 @@ import { resolveLocaleFromRoute } from 'nuxtseo-shared/i18n-runtime';
|
|
|
6
6
|
import { createPrerenderFetch } from 'nuxtseo-shared/prerender';
|
|
7
7
|
import { collectSitemap } from 'sitemapd/parse';
|
|
8
8
|
import { parseURL, withLeadingSlash, joinURL, withBase } from 'ufo';
|
|
9
|
-
import { l as logger, M as MARKDOWN_LINK_AVAILABILITY_FILE } from '../shared/nuxt-ai-ready.
|
|
9
|
+
import { l as logger, m as matchingStaticContentSignals, M as MARKDOWN_LINK_AVAILABILITY_FILE } from '../shared/nuxt-ai-ready.O6_9Egm8.mjs';
|
|
10
10
|
import { toLogicalRoute, toDeployedRoute } from '../../dist/runtime/route-path.js';
|
|
11
11
|
import { insertLlmsTxtDirective } from '../../dist/runtime/llms-txt-directive.js';
|
|
12
12
|
import { absoluteSiteUrl } from '../../dist/runtime/markdown-links.js';
|
|
@@ -497,6 +497,17 @@ function setupPrerenderHandler(options, dbPath, siteInfo, llmsTxtConfig, extras)
|
|
|
497
497
|
initPromise = initCrawler(state);
|
|
498
498
|
await initPromise;
|
|
499
499
|
const parsed = JSON.parse(route.contents || "{}");
|
|
500
|
+
if (parsed.contentSignal !== void 0) {
|
|
501
|
+
route.contentSignal = parsed.contentSignal;
|
|
502
|
+
for (const sourceRoute of pageRoute === "/" ? ["/"] : [pageRoute, `${pageRoute}/`]) {
|
|
503
|
+
if (matchingStaticContentSignals(nitro.options.routeRules, sourceRoute).includes(parsed.contentSignal))
|
|
504
|
+
continue;
|
|
505
|
+
const source = nitro.options.routeRules[sourceRoute] ||= {};
|
|
506
|
+
source.headers ||= {};
|
|
507
|
+
if (!Object.keys(source.headers).some((name) => name.toLowerCase() === "content-signal"))
|
|
508
|
+
source.headers["Content-Signal"] = parsed.contentSignal;
|
|
509
|
+
}
|
|
510
|
+
}
|
|
500
511
|
if (!state.prerenderedRoutes.has(pageRoute))
|
|
501
512
|
await processMarkdownRoute(state, nuxt, pageRoute, parsed);
|
|
502
513
|
const withSitemap = options.sitemapMd === false ? parsed.markdown : appendSitemapSection(parsed.markdown, absoluteSiteUrl(toDeployedRoute(SITEMAP_MD_ROUTE, nitro.options.baseURL), siteInfo?.url));
|
package/dist/module.d.mts
CHANGED
package/dist/module.json
CHANGED
package/dist/module.mjs
CHANGED
|
@@ -7,7 +7,7 @@ import '@nuxt/kit';
|
|
|
7
7
|
import 'defu';
|
|
8
8
|
import 'nuxt-site-config/kit';
|
|
9
9
|
import 'nuxtseo-shared/kit';
|
|
10
|
-
export {
|
|
10
|
+
export { a as default } from './shared/nuxt-ai-ready.O6_9Egm8.mjs';
|
|
11
11
|
import './shared/nuxt-ai-ready.B3hAuOLQ.mjs';
|
|
12
12
|
import '../dist/runtime/llms-txt-files.js';
|
|
13
13
|
import '../dist/runtime/route-path.js';
|
|
@@ -11,7 +11,7 @@ import { buildFrontmatter } from "../utils/frontmatter.js";
|
|
|
11
11
|
import { extractKeywords } from "../utils/keywords.js";
|
|
12
12
|
import { getMarkdownRenderInfo } from "../utils/markdown-request.js";
|
|
13
13
|
import { agentSkillArtifactPaths } from "../utils/negotiation-response.js";
|
|
14
|
-
import { consumePrerenderedHtml } from "../utils/prerender-html.js";
|
|
14
|
+
import { consumePrerenderedContentSignal, consumePrerenderedHtml } from "../utils/prerender-html.js";
|
|
15
15
|
import { isSitemapMdRequest } from "../utils/sitemap-md.js";
|
|
16
16
|
function extractHeadingsFromMarkdown(markdown) {
|
|
17
17
|
const headings = [];
|
|
@@ -37,6 +37,7 @@ export default defineEventHandler(async (event) => {
|
|
|
37
37
|
if (!renderInfo || "notAcceptable" in renderInfo)
|
|
38
38
|
return;
|
|
39
39
|
const { path } = renderInfo;
|
|
40
|
+
let contentSignal = consumePrerenderedContentSignal(path);
|
|
40
41
|
const runtimeConfig = fullRuntimeConfig["nuxt-ai-ready"];
|
|
41
42
|
const deployedPath = toDeployedRoute(path, fullRuntimeConfig.app.baseURL);
|
|
42
43
|
const canonicalUrl = withSiteUrl(event, deployedPath, { withBase: true });
|
|
@@ -62,6 +63,7 @@ ${absoluteMarkdownLinks(contentPage.markdown, canonicalUrl)}`;
|
|
|
62
63
|
description: contentPage.description || "",
|
|
63
64
|
headings,
|
|
64
65
|
keywords: keywords2,
|
|
66
|
+
...contentSignal !== void 0 && { contentSignal },
|
|
65
67
|
// Only a real content date feeds the page index and sitemap lastmod.
|
|
66
68
|
...contentPage.updatedAt && { updatedAt: contentPage.updatedAt }
|
|
67
69
|
});
|
|
@@ -95,8 +97,13 @@ ${absoluteMarkdownLinks(contentPage.markdown, canonicalUrl)}`;
|
|
|
95
97
|
});
|
|
96
98
|
}
|
|
97
99
|
html = await response.text();
|
|
100
|
+
const fetchedPolicy = response.headers.get("content-signal");
|
|
101
|
+
const sharedPolicy = fullRuntimeConfig["nuxt-robots"]?.contentSignalHeader;
|
|
102
|
+
if (fetchedPolicy !== null && fetchedPolicy !== sharedPolicy)
|
|
103
|
+
contentSignal = fetchedPolicy;
|
|
98
104
|
logger.debug(`[markdown.prerender] Fetched HTML for ${path} (${html.length} bytes)`);
|
|
99
105
|
consumePrerenderedHtml(path);
|
|
106
|
+
consumePrerenderedContentSignal(path);
|
|
100
107
|
}
|
|
101
108
|
if (html.includes("__NUXT_ERROR__") || html.includes("nuxt-error-page")) {
|
|
102
109
|
return createError({
|
|
@@ -125,6 +132,7 @@ ${absoluteMarkdownLinks(contentPage.markdown, canonicalUrl)}`;
|
|
|
125
132
|
description: result.description,
|
|
126
133
|
headings: result.headings,
|
|
127
134
|
keywords,
|
|
135
|
+
...contentSignal !== void 0 && { contentSignal },
|
|
128
136
|
...result.updatedAt && { updatedAt: result.updatedAt }
|
|
129
137
|
});
|
|
130
138
|
});
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { defineNitroPlugin, useRuntimeConfig } from "#nuxtseo/nitro";
|
|
2
|
+
import { toLogicalRoute } from "../../route-path.js";
|
|
3
|
+
import { storePrerenderedContentSignal } from "../utils/prerender-html.js";
|
|
4
|
+
export default defineNitroPlugin((nitro) => {
|
|
5
|
+
const hooks = nitro.hooks;
|
|
6
|
+
hooks.hook("response", (response, event) => {
|
|
7
|
+
const policy = response.headers.get("content-signal");
|
|
8
|
+
const config = useRuntimeConfig();
|
|
9
|
+
const sharedPolicy = config["nuxt-robots"]?.contentSignalHeader;
|
|
10
|
+
if (policy !== null && policy !== sharedPolicy)
|
|
11
|
+
storePrerenderedContentSignal(toLogicalRoute(event.url.pathname, config.app.baseURL), policy);
|
|
12
|
+
});
|
|
13
|
+
});
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { getRequestURL, getResponseHeader } from "#nuxtseo/h3";
|
|
2
|
+
import { defineNitroPlugin, useRuntimeConfig } from "#nuxtseo/nitro";
|
|
3
|
+
import { toLogicalRoute } from "../../route-path.js";
|
|
4
|
+
import { storePrerenderedContentSignal } from "../utils/prerender-html.js";
|
|
5
|
+
export default defineNitroPlugin((nitro) => {
|
|
6
|
+
nitro.hooks.hook("beforeResponse", (event) => {
|
|
7
|
+
const policy = getResponseHeader(event, "content-signal");
|
|
8
|
+
const config = useRuntimeConfig(event);
|
|
9
|
+
const sharedPolicy = config["nuxt-robots"]?.contentSignalHeader;
|
|
10
|
+
if (typeof policy === "string" && policy !== sharedPolicy)
|
|
11
|
+
storePrerenderedContentSignal(toLogicalRoute(getRequestURL(event).pathname, config.app.baseURL), policy);
|
|
12
|
+
});
|
|
13
|
+
});
|
|
@@ -1,3 +1,6 @@
|
|
|
1
1
|
import type { NuxtRenderHTMLContext } from 'nuxt/app';
|
|
2
2
|
export declare function storePrerenderedHtml(path: string, html: string | NuxtRenderHTMLContext): void;
|
|
3
|
+
/** Only rendered pages can hand a policy to a Markdown twin. */
|
|
4
|
+
export declare function storePrerenderedContentSignal(path: string, policy: string): void;
|
|
5
|
+
export declare function consumePrerenderedContentSignal(path: string): string | undefined;
|
|
3
6
|
export declare function consumePrerenderedHtml(path: string): string | undefined;
|
|
@@ -1,10 +1,22 @@
|
|
|
1
1
|
const htmlByPath = /* @__PURE__ */ new Map();
|
|
2
|
+
const contentSignalByPath = /* @__PURE__ */ new Map();
|
|
2
3
|
function normalizeHtmlCachePath(path) {
|
|
3
4
|
return path.replace(/\/+$/, "") || "/";
|
|
4
5
|
}
|
|
5
6
|
export function storePrerenderedHtml(path, html) {
|
|
6
7
|
htmlByPath.set(normalizeHtmlCachePath(path), html);
|
|
7
8
|
}
|
|
9
|
+
export function storePrerenderedContentSignal(path, policy) {
|
|
10
|
+
const key = normalizeHtmlCachePath(path);
|
|
11
|
+
if (htmlByPath.has(key))
|
|
12
|
+
contentSignalByPath.set(key, policy);
|
|
13
|
+
}
|
|
14
|
+
export function consumePrerenderedContentSignal(path) {
|
|
15
|
+
const key = normalizeHtmlCachePath(path);
|
|
16
|
+
const policy = contentSignalByPath.get(key);
|
|
17
|
+
contentSignalByPath.delete(key);
|
|
18
|
+
return policy;
|
|
19
|
+
}
|
|
8
20
|
export function consumePrerenderedHtml(path) {
|
|
9
21
|
const key = normalizeHtmlCachePath(path);
|
|
10
22
|
const html = htmlByPath.get(key);
|
|
@@ -563,6 +563,334 @@ async function readCloudflareAssetRouting(rootDir) {
|
|
|
563
563
|
}
|
|
564
564
|
}
|
|
565
565
|
|
|
566
|
+
function escapeRegExp(value) {
|
|
567
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
568
|
+
}
|
|
569
|
+
function ensureStaticHeader(contents, route, name, value, mode = "default") {
|
|
570
|
+
const eol = contents.includes("\r\n") ? "\r\n" : "\n";
|
|
571
|
+
const routePattern = new RegExp(`^${escapeRegExp(route)}[\\t ]*\\r?$`, "gm");
|
|
572
|
+
const routeMatches = [...contents.matchAll(routePattern)];
|
|
573
|
+
const routeMatch = routeMatches.at(-1);
|
|
574
|
+
if (!routeMatch || routeMatch.index === void 0) {
|
|
575
|
+
const separator = contents.length === 0 ? "" : contents.endsWith(`${eol}${eol}`) ? "" : contents.endsWith(eol) ? eol : `${eol}${eol}`;
|
|
576
|
+
return `${contents}${separator}${route}${eol} ${name}: ${value}${eol}`;
|
|
577
|
+
}
|
|
578
|
+
const routeLineEnd = routeMatch.index + routeMatch[0].length;
|
|
579
|
+
const blockStart = contents[routeLineEnd] === "\n" ? routeLineEnd + 1 : routeLineEnd;
|
|
580
|
+
const nextBlockPattern = /^(?![\t ]|\r?$).+/gm;
|
|
581
|
+
nextBlockPattern.lastIndex = blockStart;
|
|
582
|
+
const nextBlock = nextBlockPattern.exec(contents);
|
|
583
|
+
const blockEnd = nextBlock?.index ?? contents.length;
|
|
584
|
+
const block = contents.slice(blockStart, blockEnd);
|
|
585
|
+
const escapedName = escapeRegExp(name);
|
|
586
|
+
const existingHeaderPattern = new RegExp(
|
|
587
|
+
`^[\\t ]+(?:${escapedName}[\\t ]*:|![\\t ]*${escapedName}[\\t ]*\\r?$)`,
|
|
588
|
+
"im"
|
|
589
|
+
);
|
|
590
|
+
if (existingHeaderPattern.test(block)) {
|
|
591
|
+
if (mode === "append") {
|
|
592
|
+
const removalPattern = new RegExp(`^[\\t ]+![\\t ]*${escapedName}[\\t ]*\\r?$`, "im");
|
|
593
|
+
if (removalPattern.test(block))
|
|
594
|
+
return contents;
|
|
595
|
+
const valuePattern = new RegExp(`^([\\t ]+${escapedName}[\\t ]*:[\\t ]*)([^\\r\\n]*)(\\r?$)`, "im");
|
|
596
|
+
const match = block.match(valuePattern);
|
|
597
|
+
if (match) {
|
|
598
|
+
const merged = block.replace(valuePattern, (_line, prefix2, current, suffix) => {
|
|
599
|
+
const appended = name.toLowerCase() === "link" ? current.includes(value) ? current : [current, value].filter(Boolean).join(", ") : mergeVaryHeader(current, value);
|
|
600
|
+
return `${prefix2}${appended}${suffix}`;
|
|
601
|
+
});
|
|
602
|
+
return `${contents.slice(0, blockStart)}${merged}${contents.slice(blockEnd)}`;
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
return contents;
|
|
606
|
+
}
|
|
607
|
+
const prefix = blockStart === routeLineEnd ? eol : "";
|
|
608
|
+
return `${contents.slice(0, blockStart)}${prefix} ${name}: ${value}${eol}${contents.slice(blockStart)}`;
|
|
609
|
+
}
|
|
610
|
+
function mergeStaticDiscoveryHeaders(contents, baseURL, entries) {
|
|
611
|
+
if (entries.length === 0)
|
|
612
|
+
return contents;
|
|
613
|
+
const route = toDeployedRoute("/*", baseURL);
|
|
614
|
+
const blocks = splitHeaderBlocks(contents);
|
|
615
|
+
const matching = blocks.filter((block) => block.route === route);
|
|
616
|
+
let merged = matching[0]?.text || "";
|
|
617
|
+
for (const block of matching.slice(1)) {
|
|
618
|
+
for (const line of block.text.split(/(?<=\n)/).slice(1)) {
|
|
619
|
+
const header = line.match(/^[\t ]+([^:\s]+):[\t ]*([^\r\n]*)/);
|
|
620
|
+
if (header?.[1]?.toLowerCase() === "link") {
|
|
621
|
+
merged = ensureStaticHeader(merged, route, header[1], header[2], "append");
|
|
622
|
+
} else if (header) {
|
|
623
|
+
const existing = new RegExp(`^[\\t ]+${escapeRegExp(header[1])}[\\t ]*:[^\\r\\n]*(?:\\r?\\n|$)`, "gim");
|
|
624
|
+
merged = ensureStaticHeader(merged.replace(existing, ""), route, header[1], header[2]);
|
|
625
|
+
} else {
|
|
626
|
+
merged += line;
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
if (matching.length > 1) {
|
|
631
|
+
let inserted = false;
|
|
632
|
+
contents = blocks.map((block) => {
|
|
633
|
+
if (block.route !== route)
|
|
634
|
+
return block.text;
|
|
635
|
+
if (inserted)
|
|
636
|
+
return "";
|
|
637
|
+
inserted = true;
|
|
638
|
+
return merged;
|
|
639
|
+
}).join("");
|
|
640
|
+
}
|
|
641
|
+
return entries.reduce((headers, entry) => ensureStaticHeader(headers, route, "Link", entry, "append"), contents);
|
|
642
|
+
}
|
|
643
|
+
function parseHeaderLine(line) {
|
|
644
|
+
const colon = line.indexOf(":");
|
|
645
|
+
if (!/^[\t ]/.test(line) || colon < 0)
|
|
646
|
+
return void 0;
|
|
647
|
+
const name = line.slice(0, colon).trim();
|
|
648
|
+
if (!name || name.startsWith("!") || /\s/.test(name))
|
|
649
|
+
return void 0;
|
|
650
|
+
const eol = line.endsWith("\r\n") ? "\r\n" : line.endsWith("\n") ? "\n" : "";
|
|
651
|
+
return {
|
|
652
|
+
name,
|
|
653
|
+
value: line.slice(colon + 1, line.length - eol.length).trim(),
|
|
654
|
+
withValue: (value) => `${line.slice(0, colon + 1)} ${value}${eol}`
|
|
655
|
+
};
|
|
656
|
+
}
|
|
657
|
+
const RE_DETACH_LINE = /^[\t ]+![\t ]*([^\s:]+)/;
|
|
658
|
+
function patternToRegExp(pattern) {
|
|
659
|
+
const source = pattern.split(/(\*|:\w+)/).map((part) => part === "*" ? ".*" : part.startsWith(":") ? "[^/]+" : escapeRegExp(part)).join("");
|
|
660
|
+
return new RegExp(`^${source}$`);
|
|
661
|
+
}
|
|
662
|
+
function matchingStaticContentSignals(routeRules, route) {
|
|
663
|
+
return Object.entries(routeRules).flatMap(([pattern, rule]) => {
|
|
664
|
+
if (!patternToRegExp(pattern).test(route))
|
|
665
|
+
return [];
|
|
666
|
+
const headers = rule?.headers;
|
|
667
|
+
return Object.entries(headers || {}).filter(([name]) => name.toLowerCase() === "content-signal").map(([, value]) => value);
|
|
668
|
+
});
|
|
669
|
+
}
|
|
670
|
+
function normalizeStaticContentSignalHeaders(contents) {
|
|
671
|
+
const parsed = splitHeaderBlocks(contents);
|
|
672
|
+
if (!parsed.some((block) => /^\s+content-signal\s*:/im.test(block.text)))
|
|
673
|
+
return contents;
|
|
674
|
+
const policyLine = /^[\t ]+(?:content-signal\s*:.*|!\s*content-signal\s*)$/i;
|
|
675
|
+
const hasPolicy = (block) => block.text.split(/\r?\n/).slice(1).some((line) => policyLine.test(line));
|
|
676
|
+
const repeated = /* @__PURE__ */ new Map();
|
|
677
|
+
for (const block of parsed) {
|
|
678
|
+
if (block.route !== null)
|
|
679
|
+
repeated.set(block.route, [...repeated.get(block.route) || [], block]);
|
|
680
|
+
}
|
|
681
|
+
const combined = /* @__PURE__ */ new Set();
|
|
682
|
+
const blocks = parsed.flatMap((block) => {
|
|
683
|
+
const group = block.route === null ? [] : repeated.get(block.route);
|
|
684
|
+
if (group.length < 2 || !group.some(hasPolicy))
|
|
685
|
+
return [block];
|
|
686
|
+
if (combined.has(block.route))
|
|
687
|
+
return [];
|
|
688
|
+
combined.add(block.route);
|
|
689
|
+
const lines = group.at(-1).text.split(/\r?\n/).slice(1).filter((line) => !policyLine.test(line));
|
|
690
|
+
const policy = group.filter(hasPolicy).at(-1).text.split(/\r?\n/).slice(1).filter((line) => policyLine.test(line));
|
|
691
|
+
return [{ route: block.route, text: `${block.route}
|
|
692
|
+
${[...lines, ...policy].join("\n")}
|
|
693
|
+
` }];
|
|
694
|
+
});
|
|
695
|
+
const policyOnly = (block) => hasPolicy(block) && block.text.split(/\r?\n/).slice(1).every((line) => !line.trim() || line.trimStart().startsWith("#") || policyLine.test(line));
|
|
696
|
+
const policyValue = (block) => block.text.match(/^[\t ]+content-signal[\t ]*:(.*)$/im)?.[1]?.trim() ?? null;
|
|
697
|
+
const edges = blocks.map(() => /* @__PURE__ */ new Set());
|
|
698
|
+
const incoming = blocks.map(() => 0);
|
|
699
|
+
const connect = (before, after) => {
|
|
700
|
+
if (!edges[before].has(after)) {
|
|
701
|
+
edges[before].add(after);
|
|
702
|
+
incoming[after]++;
|
|
703
|
+
}
|
|
704
|
+
};
|
|
705
|
+
let previousUnrelated;
|
|
706
|
+
for (let index = 0; index < blocks.length; index++) {
|
|
707
|
+
if (!policyOnly(blocks[index])) {
|
|
708
|
+
if (previousUnrelated !== void 0)
|
|
709
|
+
connect(previousUnrelated, index);
|
|
710
|
+
previousUnrelated = index;
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
for (let broad = 0; broad < blocks.length; broad++) {
|
|
714
|
+
const source = blocks[broad];
|
|
715
|
+
if (!hasPolicy(source) || source.route === null)
|
|
716
|
+
continue;
|
|
717
|
+
for (let narrow = 0; narrow < blocks.length; narrow++) {
|
|
718
|
+
const target = blocks[narrow];
|
|
719
|
+
if (broad !== narrow && hasPolicy(target) && target.route !== null && policyValue(source) !== policyValue(target) && coversContentSignalPattern(source.route, target.route)) {
|
|
720
|
+
connect(broad, narrow);
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
const ordered = [];
|
|
725
|
+
const emitted = /* @__PURE__ */ new Set();
|
|
726
|
+
while (ordered.length < blocks.length) {
|
|
727
|
+
const next = incoming.findIndex((count, index) => count === 0 && !emitted.has(index));
|
|
728
|
+
if (next === -1)
|
|
729
|
+
throw new Error("Content-Signal rules conflict with other header ordering. Place broader mixed policy rules before narrower rules.");
|
|
730
|
+
emitted.add(next);
|
|
731
|
+
ordered.push(blocks[next]);
|
|
732
|
+
for (const target of edges[next])
|
|
733
|
+
incoming[target]--;
|
|
734
|
+
}
|
|
735
|
+
return ordered.map((block) => {
|
|
736
|
+
const text = block.text.trimEnd();
|
|
737
|
+
if (!/^\s+content-signal\s*:/im.test(text) || /^\s+!\s*content-signal\s*$/im.test(text))
|
|
738
|
+
return `${text}
|
|
739
|
+
`;
|
|
740
|
+
return `${text.replace(/^(\s+)(content-signal\s*:)/im, "$1! Content-Signal\n$1$2")}
|
|
741
|
+
`;
|
|
742
|
+
}).join("\n");
|
|
743
|
+
}
|
|
744
|
+
function coversPattern(broad, narrow) {
|
|
745
|
+
if (broad === narrow || !broad.startsWith("/") || !narrow.startsWith("/"))
|
|
746
|
+
return false;
|
|
747
|
+
if (!/[*:]/.test(narrow))
|
|
748
|
+
return patternToRegExp(broad).test(narrow);
|
|
749
|
+
return broad.indexOf("*") === broad.length - 1 && !broad.includes(":") && narrow.startsWith(broad.slice(0, -1));
|
|
750
|
+
}
|
|
751
|
+
function coversContentSignalPattern(broad, narrow) {
|
|
752
|
+
if (coversPattern(broad, narrow))
|
|
753
|
+
return true;
|
|
754
|
+
if (broad === narrow || !broad.startsWith("/") || !narrow.startsWith("/") || broad.indexOf("*") !== broad.length - 1 || !broad.includes(":")) {
|
|
755
|
+
return false;
|
|
756
|
+
}
|
|
757
|
+
const prefix = narrow.split("*")[0];
|
|
758
|
+
return !prefix.includes(":") && patternToRegExp(broad).test(prefix);
|
|
759
|
+
}
|
|
760
|
+
function splitLinkEntries(value) {
|
|
761
|
+
const entries = [];
|
|
762
|
+
let current = "";
|
|
763
|
+
let inUrl = false;
|
|
764
|
+
let inQuote = false;
|
|
765
|
+
for (const char of value) {
|
|
766
|
+
if (char === "<" && !inQuote)
|
|
767
|
+
inUrl = true;
|
|
768
|
+
else if (char === ">" && !inQuote)
|
|
769
|
+
inUrl = false;
|
|
770
|
+
else if (char === '"' && !inUrl)
|
|
771
|
+
inQuote = !inQuote;
|
|
772
|
+
if (char === "," && !inUrl && !inQuote) {
|
|
773
|
+
entries.push(current.trim());
|
|
774
|
+
current = "";
|
|
775
|
+
continue;
|
|
776
|
+
}
|
|
777
|
+
current += char;
|
|
778
|
+
}
|
|
779
|
+
entries.push(current.trim());
|
|
780
|
+
return entries.filter(Boolean);
|
|
781
|
+
}
|
|
782
|
+
function headerTokens(name, value) {
|
|
783
|
+
if (name === "link")
|
|
784
|
+
return splitLinkEntries(value);
|
|
785
|
+
if (name === "vary")
|
|
786
|
+
return value.split(",").map((token) => token.trim()).filter(Boolean);
|
|
787
|
+
return [value.trim()];
|
|
788
|
+
}
|
|
789
|
+
function tokenKey(name, token) {
|
|
790
|
+
return name === "vary" ? token.toLowerCase() : token;
|
|
791
|
+
}
|
|
792
|
+
function dedupeStaticHeaders(contents, names) {
|
|
793
|
+
const owned = new Set(names.map((name) => name.toLowerCase()));
|
|
794
|
+
const blocks = splitHeaderBlocks(contents);
|
|
795
|
+
const detached = /* @__PURE__ */ new Set();
|
|
796
|
+
const parsed = blocks.map((block) => {
|
|
797
|
+
const headers = [];
|
|
798
|
+
for (const line of block.text.split(/(?<=\n)/).slice(block.route === null ? 0 : 1)) {
|
|
799
|
+
const detach = line.match(RE_DETACH_LINE);
|
|
800
|
+
if (detach) {
|
|
801
|
+
detached.add(detach[1].toLowerCase());
|
|
802
|
+
continue;
|
|
803
|
+
}
|
|
804
|
+
const header = parseHeaderLine(line);
|
|
805
|
+
if (!header)
|
|
806
|
+
continue;
|
|
807
|
+
const name = header.name.toLowerCase();
|
|
808
|
+
if (owned.has(name))
|
|
809
|
+
headers.push({ name, tokens: headerTokens(name, header.value) });
|
|
810
|
+
}
|
|
811
|
+
return headers;
|
|
812
|
+
});
|
|
813
|
+
const lastRuleIndex = /* @__PURE__ */ new Map();
|
|
814
|
+
blocks.forEach((block, index) => {
|
|
815
|
+
if (block.route !== null)
|
|
816
|
+
lastRuleIndex.set(block.route, index);
|
|
817
|
+
});
|
|
818
|
+
let changed = false;
|
|
819
|
+
const output = blocks.map((block) => {
|
|
820
|
+
const route = block.route;
|
|
821
|
+
if (route === null)
|
|
822
|
+
return block.text;
|
|
823
|
+
const provided = /* @__PURE__ */ new Map();
|
|
824
|
+
lastRuleIndex.forEach((index, pattern) => {
|
|
825
|
+
if (!coversPattern(pattern, route))
|
|
826
|
+
return;
|
|
827
|
+
for (const { name, tokens } of parsed[index]) {
|
|
828
|
+
if (detached.has(name))
|
|
829
|
+
continue;
|
|
830
|
+
const values = provided.get(name) ?? /* @__PURE__ */ new Set();
|
|
831
|
+
tokens.forEach((token) => values.add(tokenKey(name, token)));
|
|
832
|
+
provided.set(name, values);
|
|
833
|
+
}
|
|
834
|
+
});
|
|
835
|
+
if (provided.size === 0)
|
|
836
|
+
return block.text;
|
|
837
|
+
const [routeLine, ...lines] = block.text.split(/(?<=\n)/);
|
|
838
|
+
let headerLines = 0;
|
|
839
|
+
const kept = lines.flatMap((line) => {
|
|
840
|
+
const header = parseHeaderLine(line);
|
|
841
|
+
if (!header) {
|
|
842
|
+
if (RE_DETACH_LINE.test(line))
|
|
843
|
+
headerLines++;
|
|
844
|
+
return [line];
|
|
845
|
+
}
|
|
846
|
+
headerLines++;
|
|
847
|
+
const name = header.name.toLowerCase();
|
|
848
|
+
const covered = provided.get(name);
|
|
849
|
+
if (!covered)
|
|
850
|
+
return [line];
|
|
851
|
+
const tokens = headerTokens(name, header.value);
|
|
852
|
+
const remaining = tokens.filter((token) => !covered.has(tokenKey(name, token)));
|
|
853
|
+
if (remaining.length === tokens.length)
|
|
854
|
+
return [line];
|
|
855
|
+
changed = true;
|
|
856
|
+
if (remaining.length > 0)
|
|
857
|
+
return [header.withValue(remaining.join(", "))];
|
|
858
|
+
headerLines--;
|
|
859
|
+
return [];
|
|
860
|
+
});
|
|
861
|
+
return headerLines === 0 ? kept.join("") : `${routeLine}${kept.join("")}`;
|
|
862
|
+
});
|
|
863
|
+
return changed ? output.join("") : contents;
|
|
864
|
+
}
|
|
865
|
+
const CLOUDFLARE_STATIC_HEADER_RULE_LIMIT = 100;
|
|
866
|
+
const RE_EXACT_MARKDOWN_ROUTE$1 = /^\/[^*\s]*\.md[\t ]*\r?$/;
|
|
867
|
+
function splitHeaderBlocks(contents) {
|
|
868
|
+
const blocks = [];
|
|
869
|
+
let current = { route: null, text: "" };
|
|
870
|
+
for (const line of contents.split(/(?<=\n)/)) {
|
|
871
|
+
const isRoute = /^[^\s#]/.test(line);
|
|
872
|
+
if (isRoute) {
|
|
873
|
+
blocks.push(current);
|
|
874
|
+
current = { route: line.trimEnd(), text: "" };
|
|
875
|
+
}
|
|
876
|
+
current.text += line;
|
|
877
|
+
}
|
|
878
|
+
blocks.push(current);
|
|
879
|
+
return blocks;
|
|
880
|
+
}
|
|
881
|
+
function enforceStaticHeaderBudget(contents, limit) {
|
|
882
|
+
const blocks = splitHeaderBlocks(contents);
|
|
883
|
+
const ruleCount = blocks.filter((block) => block.route !== null).length;
|
|
884
|
+
if (ruleCount <= limit)
|
|
885
|
+
return { contents, total: ruleCount, dropped: 0 };
|
|
886
|
+
const kept = blocks.filter((block) => block.route === null || !RE_EXACT_MARKDOWN_ROUTE$1.test(block.route) || /^\s+(?:content-signal\s*:|!\s*content-signal\s*$)/im.test(block.text));
|
|
887
|
+
return {
|
|
888
|
+
contents: kept.map((block) => block.text).join(""),
|
|
889
|
+
total: kept.filter((block) => block.route !== null).length,
|
|
890
|
+
dropped: ruleCount - kept.filter((block) => block.route !== null).length
|
|
891
|
+
};
|
|
892
|
+
}
|
|
893
|
+
|
|
566
894
|
const RE_MD_EXT = /\.md$/;
|
|
567
895
|
function isStaticMarkdownSourceRoute(route) {
|
|
568
896
|
if (route.includes("*") || route.includes(":"))
|
|
@@ -607,41 +935,68 @@ function prerenderedMarkdownHeaderRules(prerenderedRoutes, baseURL, describedby,
|
|
|
607
935
|
headers: {
|
|
608
936
|
"Content-Type": "text/markdown; charset=utf-8",
|
|
609
937
|
"Vary": "Accept",
|
|
610
|
-
"Link": buildStaticMarkdownLinkHeader(pageRoute, baseURL, describedby)
|
|
938
|
+
"Link": buildStaticMarkdownLinkHeader(pageRoute, baseURL, describedby),
|
|
939
|
+
...entry.contentSignal !== void 0 && { "Content-Signal": entry.contentSignal }
|
|
611
940
|
}
|
|
612
941
|
});
|
|
613
942
|
}
|
|
614
943
|
}
|
|
615
944
|
return [...rules.values()];
|
|
616
945
|
}
|
|
617
|
-
const RE_EXACT_MARKDOWN_ROUTE
|
|
946
|
+
const RE_EXACT_MARKDOWN_ROUTE = /^\/[^*:\s]*\.md$/;
|
|
618
947
|
function isStaticHeaderRouteRule([, rule]) {
|
|
619
948
|
return Boolean(rule && "headers" in rule && rule.headers);
|
|
620
949
|
}
|
|
950
|
+
function hasContentSignal$1(headers) {
|
|
951
|
+
return Object.keys(headers || {}).some((name) => name.toLowerCase() === "content-signal");
|
|
952
|
+
}
|
|
621
953
|
function planStaticMarkdownHeaderRules(routeRules, rules, limit) {
|
|
954
|
+
rules = rules.map((rule) => {
|
|
955
|
+
if (!hasContentSignal$1(rule.headers) || matchingStaticContentSignals(routeRules, rule.route).length === 0)
|
|
956
|
+
return rule;
|
|
957
|
+
return { ...rule, headers: Object.fromEntries(Object.entries(rule.headers).filter(([name]) => name.toLowerCase() !== "content-signal")) };
|
|
958
|
+
});
|
|
622
959
|
if (limit === null)
|
|
623
960
|
return { _tag: "apply", rules };
|
|
624
961
|
const entries = Object.entries(routeRules).filter(isStaticHeaderRouteRule);
|
|
625
|
-
const registeredMarkdown = entries.map(([route]) => route).filter((route) => RE_EXACT_MARKDOWN_ROUTE
|
|
962
|
+
const registeredMarkdown = entries.map(([route]) => route).filter((route) => RE_EXACT_MARKDOWN_ROUTE.test(route));
|
|
626
963
|
const other = entries.length - registeredMarkdown.length;
|
|
627
964
|
const markdown = /* @__PURE__ */ new Set([...registeredMarkdown, ...rules.map((rule) => rule.route)]);
|
|
628
965
|
const total = other + markdown.size;
|
|
629
966
|
if (total <= limit)
|
|
630
967
|
return { _tag: "apply", rules };
|
|
631
|
-
|
|
968
|
+
const policyRules = rules.filter((rule) => hasContentSignal$1(rule.headers));
|
|
969
|
+
const policyRoutes = /* @__PURE__ */ new Set([
|
|
970
|
+
...policyRules.map((rule) => rule.route),
|
|
971
|
+
...registeredMarkdown.filter((route) => hasContentSignal$1(routeRules[route].headers))
|
|
972
|
+
]);
|
|
973
|
+
if (other + policyRoutes.size > limit)
|
|
974
|
+
return { _tag: "blocked", total: other + policyRoutes.size, limit };
|
|
975
|
+
return { _tag: "skip", drop: registeredMarkdown.filter((route) => !policyRoutes.has(route)), total, dropped: markdown.size - policyRoutes.size, rules: policyRules };
|
|
632
976
|
}
|
|
633
977
|
function applyStaticMarkdownHeaderPlan(routeRules, plan) {
|
|
978
|
+
if (plan._tag === "blocked")
|
|
979
|
+
throw new Error(`Content-Signal policy rules exceed the static header limit (${plan.total}/${plan.limit}). Reduce per-page policies or use a shared policy.`);
|
|
634
980
|
if (plan._tag === "skip") {
|
|
635
981
|
for (const route of plan.drop) {
|
|
636
982
|
const rule = routeRules[route];
|
|
637
983
|
if (rule)
|
|
638
984
|
rule.headers = void 0;
|
|
639
985
|
}
|
|
640
|
-
return;
|
|
641
986
|
}
|
|
642
987
|
for (const { route, headers } of plan.rules) {
|
|
643
988
|
const existing = routeRules[route];
|
|
644
989
|
const merged = defu({ headers: { ...headers } }, existing);
|
|
990
|
+
if (hasContentSignal$1(existing?.headers)) {
|
|
991
|
+
for (const name of Object.keys(merged.headers)) {
|
|
992
|
+
if (name.toLowerCase() === "content-signal")
|
|
993
|
+
delete merged.headers[name];
|
|
994
|
+
}
|
|
995
|
+
for (const [name, value] of Object.entries(existing.headers)) {
|
|
996
|
+
if (name.toLowerCase() === "content-signal")
|
|
997
|
+
merged.headers[name] = value;
|
|
998
|
+
}
|
|
999
|
+
}
|
|
645
1000
|
const vary = Object.entries(existing?.headers || {}).filter(([name]) => name.toLowerCase() === "vary").map(([, value]) => value).join(", ");
|
|
646
1001
|
if (headers.Vary) {
|
|
647
1002
|
for (const name of Object.keys(merged.headers)) {
|
|
@@ -1256,243 +1611,6 @@ function opaqueJsonModule(exports) {
|
|
|
1256
1611
|
].join("\n");
|
|
1257
1612
|
}
|
|
1258
1613
|
|
|
1259
|
-
function escapeRegExp(value) {
|
|
1260
|
-
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1261
|
-
}
|
|
1262
|
-
function ensureStaticHeader(contents, route, name, value, mode = "default") {
|
|
1263
|
-
const eol = contents.includes("\r\n") ? "\r\n" : "\n";
|
|
1264
|
-
const routePattern = new RegExp(`^${escapeRegExp(route)}[\\t ]*\\r?$`, "gm");
|
|
1265
|
-
const routeMatches = [...contents.matchAll(routePattern)];
|
|
1266
|
-
const routeMatch = routeMatches.at(-1);
|
|
1267
|
-
if (!routeMatch || routeMatch.index === void 0) {
|
|
1268
|
-
const separator = contents.length === 0 ? "" : contents.endsWith(`${eol}${eol}`) ? "" : contents.endsWith(eol) ? eol : `${eol}${eol}`;
|
|
1269
|
-
return `${contents}${separator}${route}${eol} ${name}: ${value}${eol}`;
|
|
1270
|
-
}
|
|
1271
|
-
const routeLineEnd = routeMatch.index + routeMatch[0].length;
|
|
1272
|
-
const blockStart = contents[routeLineEnd] === "\n" ? routeLineEnd + 1 : routeLineEnd;
|
|
1273
|
-
const nextBlockPattern = /^(?![\t ]|\r?$).+/gm;
|
|
1274
|
-
nextBlockPattern.lastIndex = blockStart;
|
|
1275
|
-
const nextBlock = nextBlockPattern.exec(contents);
|
|
1276
|
-
const blockEnd = nextBlock?.index ?? contents.length;
|
|
1277
|
-
const block = contents.slice(blockStart, blockEnd);
|
|
1278
|
-
const escapedName = escapeRegExp(name);
|
|
1279
|
-
const existingHeaderPattern = new RegExp(
|
|
1280
|
-
`^[\\t ]+(?:${escapedName}[\\t ]*:|![\\t ]*${escapedName}[\\t ]*\\r?$)`,
|
|
1281
|
-
"im"
|
|
1282
|
-
);
|
|
1283
|
-
if (existingHeaderPattern.test(block)) {
|
|
1284
|
-
if (mode === "append") {
|
|
1285
|
-
const removalPattern = new RegExp(`^[\\t ]+![\\t ]*${escapedName}[\\t ]*\\r?$`, "im");
|
|
1286
|
-
if (removalPattern.test(block))
|
|
1287
|
-
return contents;
|
|
1288
|
-
const valuePattern = new RegExp(`^([\\t ]+${escapedName}[\\t ]*:[\\t ]*)([^\\r\\n]*)(\\r?$)`, "im");
|
|
1289
|
-
const match = block.match(valuePattern);
|
|
1290
|
-
if (match) {
|
|
1291
|
-
const merged = block.replace(valuePattern, (_line, prefix2, current, suffix) => {
|
|
1292
|
-
const appended = name.toLowerCase() === "link" ? current.includes(value) ? current : [current, value].filter(Boolean).join(", ") : mergeVaryHeader(current, value);
|
|
1293
|
-
return `${prefix2}${appended}${suffix}`;
|
|
1294
|
-
});
|
|
1295
|
-
return `${contents.slice(0, blockStart)}${merged}${contents.slice(blockEnd)}`;
|
|
1296
|
-
}
|
|
1297
|
-
}
|
|
1298
|
-
return contents;
|
|
1299
|
-
}
|
|
1300
|
-
const prefix = blockStart === routeLineEnd ? eol : "";
|
|
1301
|
-
return `${contents.slice(0, blockStart)}${prefix} ${name}: ${value}${eol}${contents.slice(blockStart)}`;
|
|
1302
|
-
}
|
|
1303
|
-
function mergeStaticDiscoveryHeaders(contents, baseURL, entries) {
|
|
1304
|
-
if (entries.length === 0)
|
|
1305
|
-
return contents;
|
|
1306
|
-
const route = toDeployedRoute("/*", baseURL);
|
|
1307
|
-
const blocks = splitHeaderBlocks(contents);
|
|
1308
|
-
const matching = blocks.filter((block) => block.route === route);
|
|
1309
|
-
let merged = matching[0]?.text || "";
|
|
1310
|
-
for (const block of matching.slice(1)) {
|
|
1311
|
-
for (const line of block.text.split(/(?<=\n)/).slice(1)) {
|
|
1312
|
-
const header = line.match(/^[\t ]+([^:\s]+):[\t ]*([^\r\n]*)/);
|
|
1313
|
-
if (header?.[1]?.toLowerCase() === "link") {
|
|
1314
|
-
merged = ensureStaticHeader(merged, route, header[1], header[2], "append");
|
|
1315
|
-
} else if (header) {
|
|
1316
|
-
const existing = new RegExp(`^[\\t ]+${escapeRegExp(header[1])}[\\t ]*:[^\\r\\n]*(?:\\r?\\n|$)`, "gim");
|
|
1317
|
-
merged = ensureStaticHeader(merged.replace(existing, ""), route, header[1], header[2]);
|
|
1318
|
-
} else {
|
|
1319
|
-
merged += line;
|
|
1320
|
-
}
|
|
1321
|
-
}
|
|
1322
|
-
}
|
|
1323
|
-
if (matching.length > 1) {
|
|
1324
|
-
let inserted = false;
|
|
1325
|
-
contents = blocks.map((block) => {
|
|
1326
|
-
if (block.route !== route)
|
|
1327
|
-
return block.text;
|
|
1328
|
-
if (inserted)
|
|
1329
|
-
return "";
|
|
1330
|
-
inserted = true;
|
|
1331
|
-
return merged;
|
|
1332
|
-
}).join("");
|
|
1333
|
-
}
|
|
1334
|
-
return entries.reduce((headers, entry) => ensureStaticHeader(headers, route, "Link", entry, "append"), contents);
|
|
1335
|
-
}
|
|
1336
|
-
function parseHeaderLine(line) {
|
|
1337
|
-
const colon = line.indexOf(":");
|
|
1338
|
-
if (!/^[\t ]/.test(line) || colon < 0)
|
|
1339
|
-
return void 0;
|
|
1340
|
-
const name = line.slice(0, colon).trim();
|
|
1341
|
-
if (!name || name.startsWith("!") || /\s/.test(name))
|
|
1342
|
-
return void 0;
|
|
1343
|
-
const eol = line.endsWith("\r\n") ? "\r\n" : line.endsWith("\n") ? "\n" : "";
|
|
1344
|
-
return {
|
|
1345
|
-
name,
|
|
1346
|
-
value: line.slice(colon + 1, line.length - eol.length).trim(),
|
|
1347
|
-
withValue: (value) => `${line.slice(0, colon + 1)} ${value}${eol}`
|
|
1348
|
-
};
|
|
1349
|
-
}
|
|
1350
|
-
const RE_DETACH_LINE = /^[\t ]+![\t ]*([^\s:]+)/;
|
|
1351
|
-
function patternToRegExp(pattern) {
|
|
1352
|
-
const source = pattern.split(/(\*|:\w+)/).map((part) => part === "*" ? ".*" : part.startsWith(":") ? "[^/]+" : escapeRegExp(part)).join("");
|
|
1353
|
-
return new RegExp(`^${source}$`);
|
|
1354
|
-
}
|
|
1355
|
-
function coversPattern(broad, narrow) {
|
|
1356
|
-
if (broad === narrow || !broad.startsWith("/") || !narrow.startsWith("/"))
|
|
1357
|
-
return false;
|
|
1358
|
-
if (!/[*:]/.test(narrow))
|
|
1359
|
-
return patternToRegExp(broad).test(narrow);
|
|
1360
|
-
return broad.indexOf("*") === broad.length - 1 && !broad.includes(":") && narrow.startsWith(broad.slice(0, -1));
|
|
1361
|
-
}
|
|
1362
|
-
function splitLinkEntries(value) {
|
|
1363
|
-
const entries = [];
|
|
1364
|
-
let current = "";
|
|
1365
|
-
let inUrl = false;
|
|
1366
|
-
let inQuote = false;
|
|
1367
|
-
for (const char of value) {
|
|
1368
|
-
if (char === "<" && !inQuote)
|
|
1369
|
-
inUrl = true;
|
|
1370
|
-
else if (char === ">" && !inQuote)
|
|
1371
|
-
inUrl = false;
|
|
1372
|
-
else if (char === '"' && !inUrl)
|
|
1373
|
-
inQuote = !inQuote;
|
|
1374
|
-
if (char === "," && !inUrl && !inQuote) {
|
|
1375
|
-
entries.push(current.trim());
|
|
1376
|
-
current = "";
|
|
1377
|
-
continue;
|
|
1378
|
-
}
|
|
1379
|
-
current += char;
|
|
1380
|
-
}
|
|
1381
|
-
entries.push(current.trim());
|
|
1382
|
-
return entries.filter(Boolean);
|
|
1383
|
-
}
|
|
1384
|
-
function headerTokens(name, value) {
|
|
1385
|
-
if (name === "link")
|
|
1386
|
-
return splitLinkEntries(value);
|
|
1387
|
-
if (name === "vary")
|
|
1388
|
-
return value.split(",").map((token) => token.trim()).filter(Boolean);
|
|
1389
|
-
return [value.trim()];
|
|
1390
|
-
}
|
|
1391
|
-
function tokenKey(name, token) {
|
|
1392
|
-
return name === "vary" ? token.toLowerCase() : token;
|
|
1393
|
-
}
|
|
1394
|
-
function dedupeStaticHeaders(contents, names) {
|
|
1395
|
-
const owned = new Set(names.map((name) => name.toLowerCase()));
|
|
1396
|
-
const blocks = splitHeaderBlocks(contents);
|
|
1397
|
-
const detached = /* @__PURE__ */ new Set();
|
|
1398
|
-
const parsed = blocks.map((block) => {
|
|
1399
|
-
const headers = [];
|
|
1400
|
-
for (const line of block.text.split(/(?<=\n)/).slice(block.route === null ? 0 : 1)) {
|
|
1401
|
-
const detach = line.match(RE_DETACH_LINE);
|
|
1402
|
-
if (detach) {
|
|
1403
|
-
detached.add(detach[1].toLowerCase());
|
|
1404
|
-
continue;
|
|
1405
|
-
}
|
|
1406
|
-
const header = parseHeaderLine(line);
|
|
1407
|
-
if (!header)
|
|
1408
|
-
continue;
|
|
1409
|
-
const name = header.name.toLowerCase();
|
|
1410
|
-
if (owned.has(name))
|
|
1411
|
-
headers.push({ name, tokens: headerTokens(name, header.value) });
|
|
1412
|
-
}
|
|
1413
|
-
return headers;
|
|
1414
|
-
});
|
|
1415
|
-
const lastRuleIndex = /* @__PURE__ */ new Map();
|
|
1416
|
-
blocks.forEach((block, index) => {
|
|
1417
|
-
if (block.route !== null)
|
|
1418
|
-
lastRuleIndex.set(block.route, index);
|
|
1419
|
-
});
|
|
1420
|
-
let changed = false;
|
|
1421
|
-
const output = blocks.map((block) => {
|
|
1422
|
-
const route = block.route;
|
|
1423
|
-
if (route === null)
|
|
1424
|
-
return block.text;
|
|
1425
|
-
const provided = /* @__PURE__ */ new Map();
|
|
1426
|
-
lastRuleIndex.forEach((index, pattern) => {
|
|
1427
|
-
if (!coversPattern(pattern, route))
|
|
1428
|
-
return;
|
|
1429
|
-
for (const { name, tokens } of parsed[index]) {
|
|
1430
|
-
if (detached.has(name))
|
|
1431
|
-
continue;
|
|
1432
|
-
const values = provided.get(name) ?? /* @__PURE__ */ new Set();
|
|
1433
|
-
tokens.forEach((token) => values.add(tokenKey(name, token)));
|
|
1434
|
-
provided.set(name, values);
|
|
1435
|
-
}
|
|
1436
|
-
});
|
|
1437
|
-
if (provided.size === 0)
|
|
1438
|
-
return block.text;
|
|
1439
|
-
const [routeLine, ...lines] = block.text.split(/(?<=\n)/);
|
|
1440
|
-
let headerLines = 0;
|
|
1441
|
-
const kept = lines.flatMap((line) => {
|
|
1442
|
-
const header = parseHeaderLine(line);
|
|
1443
|
-
if (!header) {
|
|
1444
|
-
if (RE_DETACH_LINE.test(line))
|
|
1445
|
-
headerLines++;
|
|
1446
|
-
return [line];
|
|
1447
|
-
}
|
|
1448
|
-
headerLines++;
|
|
1449
|
-
const name = header.name.toLowerCase();
|
|
1450
|
-
const covered = provided.get(name);
|
|
1451
|
-
if (!covered)
|
|
1452
|
-
return [line];
|
|
1453
|
-
const tokens = headerTokens(name, header.value);
|
|
1454
|
-
const remaining = tokens.filter((token) => !covered.has(tokenKey(name, token)));
|
|
1455
|
-
if (remaining.length === tokens.length)
|
|
1456
|
-
return [line];
|
|
1457
|
-
changed = true;
|
|
1458
|
-
if (remaining.length > 0)
|
|
1459
|
-
return [header.withValue(remaining.join(", "))];
|
|
1460
|
-
headerLines--;
|
|
1461
|
-
return [];
|
|
1462
|
-
});
|
|
1463
|
-
return headerLines === 0 ? kept.join("") : `${routeLine}${kept.join("")}`;
|
|
1464
|
-
});
|
|
1465
|
-
return changed ? output.join("") : contents;
|
|
1466
|
-
}
|
|
1467
|
-
const CLOUDFLARE_STATIC_HEADER_RULE_LIMIT = 100;
|
|
1468
|
-
const RE_EXACT_MARKDOWN_ROUTE = /^\/[^*\s]*\.md[\t ]*\r?$/;
|
|
1469
|
-
function splitHeaderBlocks(contents) {
|
|
1470
|
-
const blocks = [];
|
|
1471
|
-
let current = { route: null, text: "" };
|
|
1472
|
-
for (const line of contents.split(/(?<=\n)/)) {
|
|
1473
|
-
const isRoute = /^[^\s#]/.test(line);
|
|
1474
|
-
if (isRoute) {
|
|
1475
|
-
blocks.push(current);
|
|
1476
|
-
current = { route: line.trimEnd(), text: "" };
|
|
1477
|
-
}
|
|
1478
|
-
current.text += line;
|
|
1479
|
-
}
|
|
1480
|
-
blocks.push(current);
|
|
1481
|
-
return blocks;
|
|
1482
|
-
}
|
|
1483
|
-
function enforceStaticHeaderBudget(contents, limit) {
|
|
1484
|
-
const blocks = splitHeaderBlocks(contents);
|
|
1485
|
-
const ruleCount = blocks.filter((block) => block.route !== null).length;
|
|
1486
|
-
if (ruleCount <= limit)
|
|
1487
|
-
return { contents, total: ruleCount, dropped: 0 };
|
|
1488
|
-
const kept = blocks.filter((block) => block.route === null || !RE_EXACT_MARKDOWN_ROUTE.test(block.route));
|
|
1489
|
-
return {
|
|
1490
|
-
contents: kept.map((block) => block.text).join(""),
|
|
1491
|
-
total: kept.filter((block) => block.route !== null).length,
|
|
1492
|
-
dropped: ruleCount - kept.filter((block) => block.route !== null).length
|
|
1493
|
-
};
|
|
1494
|
-
}
|
|
1495
|
-
|
|
1496
1614
|
const DEFAULT_MAX_OUTPUT_CHARS = 1500;
|
|
1497
1615
|
const DEFAULT_LIST_LIMIT = 20;
|
|
1498
1616
|
const DEFAULT_SEARCH_LIMIT = 10;
|
|
@@ -2335,9 +2453,10 @@ export function trackDrizzleWork(event, work) { return work }
|
|
|
2335
2453
|
handler: resolve$1("./runtime/server/middleware/markdown.prerender")
|
|
2336
2454
|
});
|
|
2337
2455
|
const htmlCapturePlugin = resolve$1("./runtime/server/plugins/html-capture.prerender");
|
|
2456
|
+
const policyCapturePlugin = resolve$1(getNitroVersion(nuxt) === 3 ? "./runtime/server/plugins/content-signal-capture-nitro3.prerender" : "./runtime/server/plugins/content-signal-capture.prerender");
|
|
2338
2457
|
nuxt.hook("nitro:init", (nitro) => {
|
|
2339
2458
|
nitro.hooks.hook("prerender:config", (prerenderConfig) => {
|
|
2340
|
-
prerenderConfig.plugins = [...prerenderConfig.plugins || [], htmlCapturePlugin];
|
|
2459
|
+
prerenderConfig.plugins = [...prerenderConfig.plugins || [], htmlCapturePlugin, policyCapturePlugin];
|
|
2341
2460
|
});
|
|
2342
2461
|
});
|
|
2343
2462
|
addPlugin({
|
|
@@ -2598,11 +2717,14 @@ export function trackDrizzleWork(event, work) { return work }
|
|
|
2598
2717
|
discovery.push(`<${encodeURI(apiCatalogConfig.href)}>; rel="api-catalog"`);
|
|
2599
2718
|
mergedHeaders = mergeStaticDiscoveryHeaders(mergedHeaders, staticBaseURL, discovery);
|
|
2600
2719
|
mergedHeaders = dedupeStaticHeaders(mergedHeaders, ["Content-Type", "Link", "Vary"]);
|
|
2720
|
+
mergedHeaders = normalizeStaticContentSignalHeaders(mergedHeaders);
|
|
2601
2721
|
const budget = enforceStaticHeaderBudget(mergedHeaders, CLOUDFLARE_STATIC_HEADER_RULE_LIMIT);
|
|
2602
2722
|
if (budget.dropped > 0) {
|
|
2603
2723
|
mergedHeaders = budget.contents;
|
|
2604
2724
|
noteSharedMarkdownRule(budget.total + budget.dropped, budget.dropped);
|
|
2605
2725
|
}
|
|
2726
|
+
if (budget.total > CLOUDFLARE_STATIC_HEADER_RULE_LIMIT && /^\s+(?:content-signal\s*:|!\s*content-signal\s*$)/im.test(budget.contents))
|
|
2727
|
+
throw new Error(`Content-Signal policy rules exceed Cloudflare's ${CLOUDFLARE_STATIC_HEADER_RULE_LIMIT}-rule limit. Reduce per-page policies or use a shared policy.`);
|
|
2606
2728
|
if (budget.total > CLOUDFLARE_STATIC_HEADER_RULE_LIMIT)
|
|
2607
2729
|
logger.warn(`_headers still has ${budget.total} rules after dropping the .md rules. Cloudflare will reject the deploy until the other rules fit under ${CLOUDFLARE_STATIC_HEADER_RULE_LIMIT}.`);
|
|
2608
2730
|
}
|
|
@@ -2616,4 +2738,4 @@ export function trackDrizzleWork(event, work) { return work }
|
|
|
2616
2738
|
}
|
|
2617
2739
|
});
|
|
2618
2740
|
|
|
2619
|
-
export { AGENT_SKILLS_SCHEMA as A, MARKDOWN_LINK_AVAILABILITY_FILE as M, logger as l,
|
|
2741
|
+
export { AGENT_SKILLS_SCHEMA as A, MARKDOWN_LINK_AVAILABILITY_FILE as M, module$1 as a, logger as l, matchingStaticContentSignals as m };
|