@contentrain/emitter-astro 0.16.1 → 0.18.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/README.md +39 -0
- package/dist/index.d.mts +117 -7
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +292 -38
- package/dist/index.mjs.map +1 -1
- package/package.json +3 -2
package/dist/index.mjs
CHANGED
|
@@ -722,9 +722,7 @@ function scaffoldFiles(ir, options, noindex = [], redirectsConfig = null, runtim
|
|
|
722
722
|
"@tailwindcss/vite": "^4.0.0"
|
|
723
723
|
} : {},
|
|
724
724
|
...sitemap ? { "@astrojs/sitemap": "^3.7.0" } : {},
|
|
725
|
-
...images ? { sharp: "^0.34.0" } : {}
|
|
726
|
-
},
|
|
727
|
-
devDependencies: {
|
|
725
|
+
...images ? { sharp: "^0.34.0" } : {},
|
|
728
726
|
"@astrojs/check": "^0.9.0",
|
|
729
727
|
typescript: "^5.7.0"
|
|
730
728
|
}
|
|
@@ -1733,7 +1731,9 @@ function scanTags(html) {
|
|
|
1733
1731
|
});
|
|
1734
1732
|
i = j + 1;
|
|
1735
1733
|
if (!closing && RAW_TEXT_TAGS.has(name)) {
|
|
1736
|
-
const
|
|
1734
|
+
const end = new RegExp(`</${name}`, "gi");
|
|
1735
|
+
end.lastIndex = i;
|
|
1736
|
+
const close = end.exec(html)?.index ?? -1;
|
|
1737
1737
|
i = close === -1 ? html.length : close;
|
|
1738
1738
|
}
|
|
1739
1739
|
}
|
|
@@ -3766,7 +3766,7 @@ const REDIRECT_STATUSES = [
|
|
|
3766
3766
|
/** Whitespace, a control character or DEL anywhere in the string. */
|
|
3767
3767
|
const hasControlOrSpace = (value) => [...value].some((char) => char.charCodeAt(0) <= 32 || char.charCodeAt(0) === 127);
|
|
3768
3768
|
/** `/old/` and `/old` are one address in the directory build format. */
|
|
3769
|
-
function addressKey$
|
|
3769
|
+
function addressKey$2(path) {
|
|
3770
3770
|
return path === "/" ? "/" : `${path.replace(/\/+$/, "")}/`;
|
|
3771
3771
|
}
|
|
3772
3772
|
/**
|
|
@@ -3777,7 +3777,7 @@ function addressKey$1(path) {
|
|
|
3777
3777
|
function builtAddresses(routes, content) {
|
|
3778
3778
|
const out = /* @__PURE__ */ new Map();
|
|
3779
3779
|
const put = (path, routeId) => {
|
|
3780
|
-
if (path !== null && !out.has(addressKey$
|
|
3780
|
+
if (path !== null && !out.has(addressKey$2(path))) out.set(addressKey$2(path), routeId);
|
|
3781
3781
|
};
|
|
3782
3782
|
for (const route of routes) {
|
|
3783
3783
|
const pagePath = patternToPagePath(route.pattern);
|
|
@@ -3795,7 +3795,7 @@ function builtAddresses(routes, content) {
|
|
|
3795
3795
|
return out;
|
|
3796
3796
|
}
|
|
3797
3797
|
/** The decoded path for a literal Astro redirect key, or why `from` cannot be one. */
|
|
3798
|
-
function checkFrom(from) {
|
|
3798
|
+
function checkFrom(from, hostOnly = false) {
|
|
3799
3799
|
if (!from.startsWith("/") || from.startsWith("//")) return { reason: "from is not a site-root path" };
|
|
3800
3800
|
if (/[?#]/.test(from)) return { reason: "from has a query string or fragment — a static redirect matches the path only" };
|
|
3801
3801
|
let path;
|
|
@@ -3808,9 +3808,24 @@ function checkFrom(from) {
|
|
|
3808
3808
|
if (/[[\]]/.test(path)) return { reason: "from contains [ or ], which Astro reads as a route parameter" };
|
|
3809
3809
|
const segments = path.split("/").filter(Boolean);
|
|
3810
3810
|
if (segments.some((s) => s === "." || s === "..")) return { reason: "from contains a . or .. segment" };
|
|
3811
|
-
if (/\.[a-z0-9]{1,5}$/i.test(segments.at(-1) ?? "")) return { reason: "from ends in a file name — the static build serves it as a directory, not at this address" };
|
|
3811
|
+
if (!hostOnly && /\.[a-z0-9]{1,5}$/i.test(segments.at(-1) ?? "")) return { reason: "from ends in a file name — the static build serves it as a directory, not at this address" };
|
|
3812
3812
|
return { path };
|
|
3813
3813
|
}
|
|
3814
|
+
/**
|
|
3815
|
+
* A path the build writes a file at: the emitter's own endpoints (robots,
|
|
3816
|
+
* llms.txt, every feed, @astrojs/sitemap's files), the 404 page, and the
|
|
3817
|
+
* directories of Astro's bundles and the legacy stylesheets. A host file
|
|
3818
|
+
* answers before the filesystem — a forced Netlify rule and every vercel.json
|
|
3819
|
+
* redirect — so a host-only rule here would shadow the file. `builtAddresses`
|
|
3820
|
+
* knows pages only. Compared without a trailing slash (a host file gets both
|
|
3821
|
+
* forms) and in lower case (Netlify matches rules case-insensitively).
|
|
3822
|
+
* WordPress's own `/sitemap.xml` and `/sitemap_index.xml` are not the build's:
|
|
3823
|
+
* a 301 from them is one a migration wants.
|
|
3824
|
+
*/
|
|
3825
|
+
function buildOutputFile(path) {
|
|
3826
|
+
const p = (path.replace(/\/+$/, "") || "/").toLowerCase();
|
|
3827
|
+
return p === "/robots.txt" || p === "/llms.txt" || p === "/404.html" || p.endsWith("/feed.xml") || /^\/sitemap-(?:index|\d+)\.xml$/.test(p) || /^\/(?:_astro|styles)(?:\/|$)/.test(p);
|
|
3828
|
+
}
|
|
3814
3829
|
function checkTo(to) {
|
|
3815
3830
|
if (hasControlOrSpace(to)) return false;
|
|
3816
3831
|
if (to.startsWith("/")) return !to.startsWith("//");
|
|
@@ -3823,9 +3838,11 @@ function checkTo(to) {
|
|
|
3823
3838
|
}
|
|
3824
3839
|
/**
|
|
3825
3840
|
* Split the rules into what `astro.config` serves and what the host has to.
|
|
3826
|
-
* `built` is `builtAddresses` of the emitted routes.
|
|
3841
|
+
* `built` is `builtAddresses` of the emitted routes. `hostOnly`: the rules go
|
|
3842
|
+
* to host files alone (`EmitInput.hostRedirects`), so the checks that hold
|
|
3843
|
+
* only for a static redirect page (a `from` naming a file) are left out.
|
|
3827
3844
|
*/
|
|
3828
|
-
function planRedirects(redirects, built) {
|
|
3845
|
+
function planRedirects(redirects, built, options = {}) {
|
|
3829
3846
|
const config = {};
|
|
3830
3847
|
const written = [];
|
|
3831
3848
|
const manual = [];
|
|
@@ -3849,16 +3866,20 @@ function planRedirects(redirects, built) {
|
|
|
3849
3866
|
skip(`status ${status} is not one of ${REDIRECT_STATUSES.join("/")}`);
|
|
3850
3867
|
continue;
|
|
3851
3868
|
}
|
|
3852
|
-
const from = checkFrom(redirect.from);
|
|
3869
|
+
const from = checkFrom(redirect.from, options.hostOnly);
|
|
3853
3870
|
if ("reason" in from) {
|
|
3854
3871
|
skip(from.reason);
|
|
3855
3872
|
continue;
|
|
3856
3873
|
}
|
|
3874
|
+
if (options.hostOnly && buildOutputFile(from.path)) {
|
|
3875
|
+
skip("from is a file the build writes — the host file would serve the redirect over it");
|
|
3876
|
+
continue;
|
|
3877
|
+
}
|
|
3857
3878
|
if (!checkTo(redirect.to)) {
|
|
3858
3879
|
skip("to is not a site-root path or an http(s) URL");
|
|
3859
3880
|
continue;
|
|
3860
3881
|
}
|
|
3861
|
-
const key = addressKey$
|
|
3882
|
+
const key = addressKey$2(from.path);
|
|
3862
3883
|
const owner = built.get(key);
|
|
3863
3884
|
if (owner !== void 0) {
|
|
3864
3885
|
skip(`the migrated site builds a page at ${key} (route ${owner}) — the page is kept`);
|
|
@@ -3880,7 +3901,7 @@ function planRedirects(redirects, built) {
|
|
|
3880
3901
|
} catch {
|
|
3881
3902
|
target = redirect.to;
|
|
3882
3903
|
}
|
|
3883
|
-
if (addressKey$
|
|
3904
|
+
if (addressKey$2(target) === key) {
|
|
3884
3905
|
skip("to is the same address as from");
|
|
3885
3906
|
continue;
|
|
3886
3907
|
}
|
|
@@ -3985,6 +4006,172 @@ function hostRedirectFiles(config, host) {
|
|
|
3985
4006
|
};
|
|
3986
4007
|
}
|
|
3987
4008
|
//#endregion
|
|
4009
|
+
//#region src/published.ts
|
|
4010
|
+
/**
|
|
4011
|
+
* Why an entry is not built, or null when it is. `now` is epoch ms. With
|
|
4012
|
+
* `requireStatus` an entry that carries no status is held back: the producer
|
|
4013
|
+
* said it passes every entry's status, so a missing one is a gap, not a
|
|
4014
|
+
* published entry.
|
|
4015
|
+
*/
|
|
4016
|
+
function holdReason(post, now, requireStatus = false) {
|
|
4017
|
+
if (post.visibility !== void 0 && post.visibility !== "public") return "visibility";
|
|
4018
|
+
if (post.status === void 0 && requireStatus) return "status_missing";
|
|
4019
|
+
if (post.status !== void 0 && post.status !== "published") return "status";
|
|
4020
|
+
if (post.publish_at === void 0) return null;
|
|
4021
|
+
const at = Date.parse(post.publish_at);
|
|
4022
|
+
if (Number.isNaN(at)) return "publish_at_invalid";
|
|
4023
|
+
return at > now ? "scheduled" : null;
|
|
4024
|
+
}
|
|
4025
|
+
const bareHost = (host) => host.replace(/^www\./, "").toLowerCase();
|
|
4026
|
+
/** `/a/b` and `/a/b/` are one address. */
|
|
4027
|
+
const addressKey$1 = (path) => path === "/" ? "/" : `${path.replace(/\/+$/, "")}/`;
|
|
4028
|
+
/**
|
|
4029
|
+
* The held-back address an href points at, or null: a site-root path, or an
|
|
4030
|
+
* absolute URL on the site's own host (`www.` either way).
|
|
4031
|
+
*/
|
|
4032
|
+
function withheldTarget(href, gone, siteUrl) {
|
|
4033
|
+
if (!gone.size) return null;
|
|
4034
|
+
let site;
|
|
4035
|
+
try {
|
|
4036
|
+
site = siteUrl ? new URL(siteUrl) : void 0;
|
|
4037
|
+
} catch {
|
|
4038
|
+
site = void 0;
|
|
4039
|
+
}
|
|
4040
|
+
let url;
|
|
4041
|
+
try {
|
|
4042
|
+
url = new URL(href, site ?? "http://site.invalid");
|
|
4043
|
+
} catch {
|
|
4044
|
+
return null;
|
|
4045
|
+
}
|
|
4046
|
+
if (!(site ? bareHost(url.host) === bareHost(site.host) : url.host === "site.invalid") || !/^https?:$/.test(url.protocol)) return null;
|
|
4047
|
+
let path = url.pathname;
|
|
4048
|
+
try {
|
|
4049
|
+
path = decodeURI(path);
|
|
4050
|
+
} catch {}
|
|
4051
|
+
return gone.has(addressKey$1(path)) ? path : null;
|
|
4052
|
+
}
|
|
4053
|
+
/**
|
|
4054
|
+
* The content with every unpublished entry removed — from its collection and
|
|
4055
|
+
* from the list pages that carry it as an item — and links to them unwrapped.
|
|
4056
|
+
* Content with nothing held back is returned as given.
|
|
4057
|
+
*/
|
|
4058
|
+
function publishedContent(routes, content, now, siteUrl, requireStatus = false) {
|
|
4059
|
+
const withheld = [];
|
|
4060
|
+
const keep = (collection) => (post) => {
|
|
4061
|
+
const reason = holdReason(post, now, requireStatus);
|
|
4062
|
+
if (!reason) return true;
|
|
4063
|
+
withheld.push({
|
|
4064
|
+
collection,
|
|
4065
|
+
slug: post.slug,
|
|
4066
|
+
...post.status ? { status: post.status } : {},
|
|
4067
|
+
...post.visibility ? { visibility: post.visibility } : {},
|
|
4068
|
+
...post.publish_at ? { publish_at: post.publish_at } : {},
|
|
4069
|
+
reason
|
|
4070
|
+
});
|
|
4071
|
+
return false;
|
|
4072
|
+
};
|
|
4073
|
+
const collections = content.collections ? Object.fromEntries(Object.entries(content.collections).map(([name, posts]) => [name, posts.filter(keep(name))])) : void 0;
|
|
4074
|
+
const isLive = (post) => holdReason(post, now, requireStatus) === null;
|
|
4075
|
+
const posts = content.posts?.filter(content.collections?.["posts"] ? isLive : keep(DEFAULT_COLLECTION));
|
|
4076
|
+
const queries = content.queries ? Object.fromEntries(Object.entries(content.queries).map(([id, pages]) => [id, pages.map((page) => page.items.every(isLive) ? page : {
|
|
4077
|
+
...page,
|
|
4078
|
+
items: page.items.filter(isLive)
|
|
4079
|
+
})])) : void 0;
|
|
4080
|
+
const listedOnly = Object.values(content.queries ?? {}).flat().flatMap((p) => p.items).some((item) => !isLive(item));
|
|
4081
|
+
if (!withheld.length && !listedOnly) return {
|
|
4082
|
+
content,
|
|
4083
|
+
withheld,
|
|
4084
|
+
links: [],
|
|
4085
|
+
gone: /* @__PURE__ */ new Set()
|
|
4086
|
+
};
|
|
4087
|
+
const filtered = {
|
|
4088
|
+
...content,
|
|
4089
|
+
...posts ? { posts } : {},
|
|
4090
|
+
...collections ? { collections } : {},
|
|
4091
|
+
...queries ? { queries } : {}
|
|
4092
|
+
};
|
|
4093
|
+
const built = builtAddresses(routes, filtered);
|
|
4094
|
+
const gone = /* @__PURE__ */ new Set();
|
|
4095
|
+
for (const route of routes) {
|
|
4096
|
+
const pagePath = patternToPagePath(route.pattern);
|
|
4097
|
+
if (!pagePath || !pagePath.includes("[") || route.kind !== "single" && route.collection === void 0) continue;
|
|
4098
|
+
const collection = route.collection ?? "posts";
|
|
4099
|
+
for (const post of collectionItems(content, collection)) {
|
|
4100
|
+
const path = entryPath(route.pattern, {
|
|
4101
|
+
...post.params,
|
|
4102
|
+
slug: post.slug
|
|
4103
|
+
});
|
|
4104
|
+
if (path && !isLive(post) && !built.has(addressKey$1(path))) gone.add(addressKey$1(path));
|
|
4105
|
+
}
|
|
4106
|
+
}
|
|
4107
|
+
const links = [];
|
|
4108
|
+
const unlink = (html, page) => {
|
|
4109
|
+
if (!html || !gone.size) return html;
|
|
4110
|
+
return html.replace(ANCHOR_RE, (anchor, double, single, inner) => {
|
|
4111
|
+
const to = withheldTarget((double ?? single ?? "").replace(/&/g, "&"), gone, siteUrl);
|
|
4112
|
+
if (!to) return anchor;
|
|
4113
|
+
links.push({
|
|
4114
|
+
page,
|
|
4115
|
+
href: to
|
|
4116
|
+
});
|
|
4117
|
+
return inner;
|
|
4118
|
+
});
|
|
4119
|
+
};
|
|
4120
|
+
const unlinkPost = (post, page) => {
|
|
4121
|
+
const body = unlink(post.body, page) ?? post.body;
|
|
4122
|
+
const excerpt_html = unlink(post.excerpt_html, page);
|
|
4123
|
+
return body === post.body && excerpt_html === post.excerpt_html ? post : {
|
|
4124
|
+
...post,
|
|
4125
|
+
body,
|
|
4126
|
+
...excerpt_html !== void 0 ? { excerpt_html } : {}
|
|
4127
|
+
};
|
|
4128
|
+
};
|
|
4129
|
+
const pageOf = (collection, post) => {
|
|
4130
|
+
const route = routes.find((r) => (r.kind === "single" || r.collection !== void 0) && (r.collection ?? "posts") === collection);
|
|
4131
|
+
return (route && entryPath(route.pattern, {
|
|
4132
|
+
...post.params,
|
|
4133
|
+
slug: post.slug
|
|
4134
|
+
})) ?? `${collection}/${post.slug}`;
|
|
4135
|
+
};
|
|
4136
|
+
if (filtered.collections) filtered.collections = Object.fromEntries(Object.entries(filtered.collections).map(([name, list]) => [name, list.map((p) => unlinkPost(p, pageOf(name, p)))]));
|
|
4137
|
+
if (filtered.posts) filtered.posts = filtered.posts.map((p) => unlinkPost(p, pageOf(DEFAULT_COLLECTION, p)));
|
|
4138
|
+
if (filtered.queries) filtered.queries = Object.fromEntries(Object.entries(filtered.queries).map(([id, pages]) => [id, pages.map((page) => {
|
|
4139
|
+
const route = routes.find((r) => r.query === id);
|
|
4140
|
+
const at = (route && entryPath(route.pattern, page.params)) ?? `query ${id}`;
|
|
4141
|
+
return {
|
|
4142
|
+
...page,
|
|
4143
|
+
items: page.items.map((item) => unlinkPost(item, at))
|
|
4144
|
+
};
|
|
4145
|
+
})]));
|
|
4146
|
+
return {
|
|
4147
|
+
content: filtered,
|
|
4148
|
+
withheld,
|
|
4149
|
+
links,
|
|
4150
|
+
gone
|
|
4151
|
+
};
|
|
4152
|
+
}
|
|
4153
|
+
const ANCHOR_RE = /<a\b[^>]*?\shref\s*=\s*(?:"([^"]*)"|'([^']*)')[^>]*>([\s\S]*?)<\/a\s*>/gi;
|
|
4154
|
+
/** The report lines for what was held back. */
|
|
4155
|
+
function withheldWarnings(withheld, links) {
|
|
4156
|
+
const out = [];
|
|
4157
|
+
if (withheld.length) {
|
|
4158
|
+
const count = (reason) => withheld.filter((w) => w.reason === reason).length;
|
|
4159
|
+
const parts = [
|
|
4160
|
+
count("visibility") && `${count("visibility")} password-protected or private`,
|
|
4161
|
+
count("status_missing") && `${count("status_missing")} with no status (options.requireStatus)`,
|
|
4162
|
+
count("status") && `${count("status")} not published (draft, in review, rejected or archived)`,
|
|
4163
|
+
count("scheduled") && `${count("scheduled")} scheduled for later — built by the first emit after their publish_at`,
|
|
4164
|
+
count("publish_at_invalid") && `${count("publish_at_invalid")} with a publish_at that is not a date`
|
|
4165
|
+
].filter(Boolean);
|
|
4166
|
+
out.push(`unpublished: ${withheld.length} entries not built — ${parts.join("; ")}. They stay in the content store; EmitResult.withheld lists each`);
|
|
4167
|
+
}
|
|
4168
|
+
if (links.length) {
|
|
4169
|
+
const sample = links.slice(0, 5).map((l) => `${l.href} (on ${l.page})`).join(", ");
|
|
4170
|
+
out.push(`unpublished: ${links.length} links from published pages to unpublished entries kept as plain text — ${sample}${links.length > 5 ? " …" : ""}`);
|
|
4171
|
+
}
|
|
4172
|
+
return out;
|
|
4173
|
+
}
|
|
4174
|
+
//#endregion
|
|
3988
4175
|
//#region src/seo-entry.ts
|
|
3989
4176
|
/**
|
|
3990
4177
|
* A plugin's unrendered template, in its own syntax: Yoast `%%title%%`,
|
|
@@ -4163,6 +4350,27 @@ function emitAstroProject(input) {
|
|
|
4163
4350
|
files[path] = content;
|
|
4164
4351
|
}
|
|
4165
4352
|
};
|
|
4353
|
+
let now = Date.now();
|
|
4354
|
+
if (input.options?.now !== void 0) {
|
|
4355
|
+
const at = Date.parse(input.options.now);
|
|
4356
|
+
if (Number.isNaN(at)) warnings.push(`options.now "${input.options.now}" is not a date — publish_at is compared with the time of this emit`);
|
|
4357
|
+
else now = at;
|
|
4358
|
+
}
|
|
4359
|
+
const published = publishedContent(ir.routes, input.content ?? {}, now, ir.site.url, input.options?.requireStatus === true);
|
|
4360
|
+
const content = published.content;
|
|
4361
|
+
warnings.push(...withheldWarnings(published.withheld, published.links));
|
|
4362
|
+
const withheldManual = [];
|
|
4363
|
+
const toPublished = (rules) => rules?.filter((redirect) => {
|
|
4364
|
+
const target = withheldTarget(redirect.to, published.gone, ir.site.url);
|
|
4365
|
+
if (target) withheldManual.push({
|
|
4366
|
+
redirect,
|
|
4367
|
+
reason: `to ${target}, an entry that is not published — the build has no page there`
|
|
4368
|
+
});
|
|
4369
|
+
return !target;
|
|
4370
|
+
});
|
|
4371
|
+
const siteRedirects = toPublished(input.redirects);
|
|
4372
|
+
const hostOnlyRedirects = toPublished(input.hostRedirects);
|
|
4373
|
+
if (withheldManual.length) warnings.push(`unpublished: ${withheldManual.length} redirects point at unpublished entries — not written (EmitResult.redirects.manual has each)`);
|
|
4166
4374
|
const siteLang = ir.site.locales?.[0] ?? "en";
|
|
4167
4375
|
const linkable = ir.site.url ? linkSources(ir.routes, siteLang) : [];
|
|
4168
4376
|
const feedSource = input.options?.feed !== false ? linkable.find((s) => s.collection === DEFAULT_COLLECTION) : void 0;
|
|
@@ -4170,11 +4378,11 @@ function emitAstroProject(input) {
|
|
|
4170
4378
|
const archiveFeeds = feedSource ? archiveFeedSources(ir.routes, siteLang) : [];
|
|
4171
4379
|
const archiveFeedRoutes = new Set(archiveFeeds.map((a) => a.routeId));
|
|
4172
4380
|
if (!ir.site.url && (input.options?.feed !== false || input.options?.llms !== false)) warnings.push("site.url is empty — no RSS feed and no llms.txt are built; both name pages by absolute address");
|
|
4173
|
-
const redirectPlan =
|
|
4174
|
-
if (redirectPlan?.manual.length) warnings.push(`redirects: ${redirectPlan.manual.length} of ${
|
|
4381
|
+
const redirectPlan = siteRedirects ? planRedirects(siteRedirects, builtAddresses(ir.routes, content)) : void 0;
|
|
4382
|
+
if (redirectPlan?.manual.length) warnings.push(`redirects: ${redirectPlan.manual.length} of ${siteRedirects.length} rules not written to astro.config — set them up at the host (EmitResult.redirects.manual has each with its reason)`);
|
|
4175
4383
|
const seo = input.options?.seo !== false;
|
|
4176
|
-
const noindex = noindexPaths(ir.routes,
|
|
4177
|
-
const built = builtAddresses(ir.routes,
|
|
4384
|
+
const noindex = noindexPaths(ir.routes, content);
|
|
4385
|
+
const built = builtAddresses(ir.routes, content);
|
|
4178
4386
|
const sourceConfig = redirectPlan?.config ?? {};
|
|
4179
4387
|
const claimed = new Set(Object.keys(sourceConfig).map((from) => from.replace(/\/+$/, "").toLowerCase()));
|
|
4180
4388
|
const feedRules = [];
|
|
@@ -4189,7 +4397,7 @@ function emitAstroProject(input) {
|
|
|
4189
4397
|
});
|
|
4190
4398
|
};
|
|
4191
4399
|
if (feedSource) addFeedRule(FEED_REDIRECT_FROM, FEED_PATH);
|
|
4192
|
-
for (const source of archiveFeeds) for (const page of
|
|
4400
|
+
for (const source of archiveFeeds) for (const page of content.queries?.[source.query] ?? []) {
|
|
4193
4401
|
const address = entryPath(source.pattern, page.params);
|
|
4194
4402
|
if (address) addFeedRule(`${address}feed/`, `${address}feed.xml`);
|
|
4195
4403
|
}
|
|
@@ -4201,22 +4409,60 @@ function emitAstroProject(input) {
|
|
|
4201
4409
|
};
|
|
4202
4410
|
const hasRedirects = Object.keys(redirectConfig).length > 0;
|
|
4203
4411
|
add(scaffoldFiles(ir, input.options ?? {}, noindex, hasRedirects ? astroRedirectsConfig(sortedConfig(redirectConfig)) : null, input.runtime));
|
|
4412
|
+
const hostOnlyPlan = hostOnlyRedirects ? planRedirects(hostOnlyRedirects, built, { hostOnly: true }) : void 0;
|
|
4413
|
+
const hostOnlyConfig = {};
|
|
4414
|
+
const hostOnlyRule = /* @__PURE__ */ new Map();
|
|
4415
|
+
const hostOnlyManual = [...hostOnlyPlan?.manual ?? []];
|
|
4416
|
+
if (hostOnlyPlan) {
|
|
4417
|
+
const byFrom = new Map(hostOnlyPlan.written.map((rule) => [decodedPath(rule.from), rule]));
|
|
4418
|
+
for (const [from, rule] of Object.entries(hostOnlyPlan.config)) {
|
|
4419
|
+
const redirect = byFrom.get(from);
|
|
4420
|
+
const key = from.replace(/\/+$/, "").toLowerCase();
|
|
4421
|
+
if (claimed.has(key)) {
|
|
4422
|
+
hostOnlyManual.push({
|
|
4423
|
+
redirect,
|
|
4424
|
+
reason: `another rule already redirects ${from}`
|
|
4425
|
+
});
|
|
4426
|
+
continue;
|
|
4427
|
+
}
|
|
4428
|
+
claimed.add(key);
|
|
4429
|
+
hostOnlyConfig[from] = rule;
|
|
4430
|
+
hostOnlyRule.set(from, redirect);
|
|
4431
|
+
}
|
|
4432
|
+
}
|
|
4204
4433
|
const feedOrder = new Map(feedRules.map((rule, index) => [decodedPath(rule.from), index]));
|
|
4205
4434
|
const feedKeys = Object.keys(feedPlan.config).toSorted((a, b) => (feedOrder.get(a) ?? 0) - (feedOrder.get(b) ?? 0));
|
|
4206
4435
|
const hostOrder = {
|
|
4207
4436
|
...sortedConfig(sourceConfig),
|
|
4208
|
-
...Object.fromEntries(feedKeys.map((key) => [key, feedPlan.config[key]]))
|
|
4437
|
+
...Object.fromEntries(feedKeys.map((key) => [key, feedPlan.config[key]])),
|
|
4438
|
+
...sortedConfig(hostOnlyConfig)
|
|
4209
4439
|
};
|
|
4210
|
-
const hostRedirects =
|
|
4440
|
+
const hostRedirects = Object.keys(hostOrder).length ? hostRedirectFiles(hostOrder, input.options?.redirectHost) : void 0;
|
|
4441
|
+
const hostOnly = (from) => hostOnlyRule.has(from);
|
|
4442
|
+
for (const from of hostRedirects?.skipped ?? []) if (hostOnly(from)) hostOnlyManual.push({
|
|
4443
|
+
redirect: hostOnlyRule.get(from),
|
|
4444
|
+
reason: "from contains \":\" or \"*\", which host redirect files read as a pattern"
|
|
4445
|
+
});
|
|
4446
|
+
const namedHost = input.options?.redirectHost;
|
|
4447
|
+
const hostOnlyOver = (hostRedirects?.over_limit ?? []).filter(hostOnly);
|
|
4448
|
+
if (namedHost !== void 0) for (const from of hostOnlyOver) hostOnlyManual.push({
|
|
4449
|
+
redirect: hostOnlyRule.get(from),
|
|
4450
|
+
reason: `left out of ${hostRedirects.written.join(", ")} at its rule limit (${HOST_RULE_LIMIT / 2} rules) — a host-only rule has no meta-refresh page to fall back on`
|
|
4451
|
+
});
|
|
4452
|
+
else if (hostOnlyOver.length) warnings.push(`hostRedirects: ${hostOnlyOver.length} host-only rules not written to vercel.json at its rule limit (${HOST_RULE_LIMIT / 2} rules) — public/_redirects (Netlify) has them; on Vercel they are not served. Name the host (options.redirectHost) to have them reported as manual`);
|
|
4453
|
+
const unservedHostOnly = new Set(hostOnlyManual.map((m) => m.redirect));
|
|
4454
|
+
if (hostOnlyManual.length) warnings.push(`hostRedirects: ${hostOnlyManual.length} of ${hostOnlyRedirects.length} host-only rules not written — set them up at the host (EmitResult.redirects.manual has each with its reason)`);
|
|
4211
4455
|
if (hostRedirects) {
|
|
4212
4456
|
add(hostRedirects.files);
|
|
4213
|
-
|
|
4457
|
+
const overLimit = hostRedirects.over_limit.filter((from) => !hostOnly(from));
|
|
4458
|
+
const skippedPatterns = hostRedirects.skipped.filter((from) => !hostOnly(from));
|
|
4459
|
+
if (overLimit.length) {
|
|
4214
4460
|
const limitedFiles = hostRedirects.written.filter((f) => !f.includes("(netlify)")).join(", ");
|
|
4215
|
-
const feedOver =
|
|
4461
|
+
const feedOver = overLimit.filter((from) => from in feedPlan.config).length;
|
|
4216
4462
|
const which = feedOver ? ` (${feedOver} of them feed redirects — /feed/ and archive feeds come after the site's own rules)` : "";
|
|
4217
|
-
warnings.push(`redirects: ${
|
|
4463
|
+
warnings.push(`redirects: ${overLimit.length} rules not written to ${limitedFiles}${which} — ${HOST_RULE_LIMIT / 2} rules (each path with and without its slash) fill Cloudflare Pages' 2,000 static and Vercel's 2,048 redirect limit. They are served by the meta-refresh fallback only; move them to the host's dynamic rules for a real status`);
|
|
4218
4464
|
}
|
|
4219
|
-
if (
|
|
4465
|
+
if (skippedPatterns.length) warnings.push(`redirects: ${skippedPatterns.length} rules contain ":" or "*", which host redirect files read as patterns — served by the meta-refresh fallback only: ${skippedPatterns.join(", ")}`);
|
|
4220
4466
|
}
|
|
4221
4467
|
if (input.options?.trailingSlash === false && (input.options.redirectHost ?? "vercel") === "vercel") {
|
|
4222
4468
|
const vercel = files["vercel.json"] ? JSON.parse(files["vercel.json"]) : {};
|
|
@@ -4241,7 +4487,7 @@ function emitAstroProject(input) {
|
|
|
4241
4487
|
const bodyMarkersByFamily = /* @__PURE__ */ new Map();
|
|
4242
4488
|
for (const route of ir.routes) {
|
|
4243
4489
|
if (route.kind !== "single" && route.collection === void 0) continue;
|
|
4244
|
-
const posts = collectionItems(
|
|
4490
|
+
const posts = collectionItems(content, route.collection ?? "posts");
|
|
4245
4491
|
const ids = bodyMarkersByFamily.get(route.family) ?? /* @__PURE__ */ new Set();
|
|
4246
4492
|
for (const post of posts) for (const id of componentMarkers(post.body)) ids.add(id);
|
|
4247
4493
|
if (ids.size) bodyMarkersByFamily.set(route.family, ids);
|
|
@@ -4250,7 +4496,7 @@ function emitAstroProject(input) {
|
|
|
4250
4496
|
const queryBound = /* @__PURE__ */ new Set();
|
|
4251
4497
|
for (const family of ir.families) for (const placement of family.components ?? []) {
|
|
4252
4498
|
if (!placement.query) continue;
|
|
4253
|
-
const pages =
|
|
4499
|
+
const pages = content.queries?.[placement.query];
|
|
4254
4500
|
if (!pages) {
|
|
4255
4501
|
warnings.push(`family ${family.id}: placement "${placement.component}" binds query "${placement.query}", which is not in input.content.queries — binding dropped, region left as cloned markup`);
|
|
4256
4502
|
continue;
|
|
@@ -4283,23 +4529,23 @@ function emitAstroProject(input) {
|
|
|
4283
4529
|
for (const ref of refs ?? []) if (!providedCss.has(ref.split("/").pop() ?? ref)) warnings.push(`${owner}: css file "${ref}" not provided in input.css`);
|
|
4284
4530
|
};
|
|
4285
4531
|
for (const family of ir.families) missingCss(family.css.files, `family ${family.id}`);
|
|
4286
|
-
const allPosts = [...
|
|
4532
|
+
const allPosts = [...content.posts ?? [], ...Object.values(content.collections ?? {}).flat()];
|
|
4287
4533
|
for (const post of allPosts) missingCss(post.css, `post ${post.slug}`);
|
|
4288
|
-
for (const [queryId, queryPages] of Object.entries(
|
|
4289
|
-
const trailOwners = [...Object.entries(
|
|
4534
|
+
for (const [queryId, queryPages] of Object.entries(content.queries ?? {})) for (const qp of queryPages) missingCss(qp.css, `query ${queryId}`);
|
|
4535
|
+
const trailOwners = [...Object.entries(content.collections ?? {}), ...content.posts ? [[DEFAULT_COLLECTION, content.posts]] : []];
|
|
4290
4536
|
for (const [name, posts] of trailOwners) {
|
|
4291
4537
|
const bad = (posts ?? []).filter((p) => badTrail(p.breadcrumbs)).length;
|
|
4292
4538
|
if (bad) warnings.push(`collection ${name}: ${bad} breadcrumb trails dropped — every crumb needs a name and a site-root-relative path`);
|
|
4293
4539
|
const badLd = (posts ?? []).filter((p) => badSchema(p.schema)).length;
|
|
4294
4540
|
if (badLd) warnings.push(`collection ${name}: ${badLd} schema values are not JSON-LD objects — those pages print the generated structured data instead`);
|
|
4295
4541
|
}
|
|
4296
|
-
for (const [queryId, queryPages] of Object.entries(
|
|
4542
|
+
for (const [queryId, queryPages] of Object.entries(content.queries ?? {})) {
|
|
4297
4543
|
const bad = queryPages.filter((qp) => badTrail(qp.breadcrumbs)).length;
|
|
4298
4544
|
if (bad) warnings.push(`query ${queryId}: ${bad} breadcrumb trails dropped — every crumb needs a name and a site-root-relative path`);
|
|
4299
4545
|
const badLd = queryPages.filter((qp) => badSchema(qp.schema)).length;
|
|
4300
4546
|
if (badLd) warnings.push(`query ${queryId}: ${badLd} schema values are not JSON-LD objects — those pages print the generated structured data instead`);
|
|
4301
4547
|
}
|
|
4302
|
-
let routeContent =
|
|
4548
|
+
let routeContent = content;
|
|
4303
4549
|
if (seo) {
|
|
4304
4550
|
const alternates = withAlternates(ir.routes, routeContent, lang);
|
|
4305
4551
|
routeContent = alternates.content;
|
|
@@ -4354,8 +4600,8 @@ function emitAstroProject(input) {
|
|
|
4354
4600
|
if (uncovered.length) warnings.push(`ui-strings: no dictionary ${declared ? "declared" : "declared in options.uiStrings.locales"} for ${uncovered.join(", ")} — comments and forms on those pages show English text unless ${stringsDir ?? ".contentrain/content/site/ui-strings"}/{locale}.json exists at build`);
|
|
4355
4601
|
}
|
|
4356
4602
|
if ((ir.components ?? []).some((c) => isRuntimeImplemented(c, input.runtime) && c.type === "comments")) {
|
|
4357
|
-
const collections = { ...
|
|
4358
|
-
if (
|
|
4603
|
+
const collections = { ...content.collections };
|
|
4604
|
+
if (content.posts && !collections["posts"]) collections[DEFAULT_COLLECTION] = content.posts;
|
|
4359
4605
|
for (const [name, posts] of Object.entries(collections)) {
|
|
4360
4606
|
const unbound = posts.filter((p) => !p.entry).length;
|
|
4361
4607
|
if (unbound) warnings.push(`collection ${name}: ${unbound} of ${posts.length} posts carry no entry address — the comments component renders nothing on those pages`);
|
|
@@ -4381,11 +4627,19 @@ function emitAstroProject(input) {
|
|
|
4381
4627
|
return {
|
|
4382
4628
|
files,
|
|
4383
4629
|
warnings,
|
|
4384
|
-
...redirectPlan ? { redirects: {
|
|
4385
|
-
written: redirectPlan
|
|
4386
|
-
manual:
|
|
4630
|
+
...redirectPlan || hostOnlyPlan ? { redirects: {
|
|
4631
|
+
written: [...redirectPlan?.written ?? [], ...[...hostOnlyRule.values()].filter((rule) => !unservedHostOnly.has(rule))],
|
|
4632
|
+
manual: [
|
|
4633
|
+
...redirectPlan?.manual ?? [],
|
|
4634
|
+
...hostOnlyManual,
|
|
4635
|
+
...withheldManual
|
|
4636
|
+
],
|
|
4387
4637
|
host_files: hostRedirects?.written ?? [],
|
|
4388
|
-
host_over_limit: hostRedirects?.over_limit ?? []
|
|
4638
|
+
host_over_limit: (hostRedirects?.over_limit ?? []).filter((from) => !hostOnly(from) || namedHost === void 0)
|
|
4639
|
+
} } : {},
|
|
4640
|
+
...published.withheld.length || published.links.length ? { withheld: {
|
|
4641
|
+
entries: published.withheld,
|
|
4642
|
+
links: published.links
|
|
4389
4643
|
} } : {}
|
|
4390
4644
|
};
|
|
4391
4645
|
}
|
|
@@ -4409,6 +4663,6 @@ async function writeEmit(result, dir) {
|
|
|
4409
4663
|
await Promise.all(Object.entries(result.files).map(([path, content]) => writeFile(join(dir, path), content, "utf8")));
|
|
4410
4664
|
}
|
|
4411
4665
|
//#endregion
|
|
4412
|
-
export { EMBED_TS, FEED_ITEMS, FEED_PATH, LLMS_LINKS, REDIRECT_STATUSES, RUNTIME_IMPLEMENTED, SEO_COMPONENT, UI_STRINGS_DIR, UI_STRINGS_MODEL, UI_STRING_DEFAULTS, astroRedirectsConfig, balanceWarning, bodySeoLeaks, builtAddresses, checkBalance, componentMarkers, emitAstroProject, entryPath, hostRedirectFiles, isRuntimeImplemented, linkSources, noindexPaths, pascalCase, patternToPagePath, planRedirects, publisherIdOf, rewriteFeedLinks, seoFromRawEntry, stableJson, stripSeoTags, websiteIdOf, withAlternates, wrapLegacyCss, writeEmit };
|
|
4666
|
+
export { EMBED_TS, FEED_ITEMS, FEED_PATH, LLMS_LINKS, REDIRECT_STATUSES, RUNTIME_IMPLEMENTED, SEO_COMPONENT, UI_STRINGS_DIR, UI_STRINGS_MODEL, UI_STRING_DEFAULTS, astroRedirectsConfig, balanceWarning, bodySeoLeaks, builtAddresses, checkBalance, componentMarkers, emitAstroProject, entryPath, holdReason, hostRedirectFiles, isRuntimeImplemented, linkSources, noindexPaths, pascalCase, patternToPagePath, planRedirects, publishedContent, publisherIdOf, rewriteFeedLinks, seoFromRawEntry, stableJson, stripSeoTags, websiteIdOf, withAlternates, wrapLegacyCss, writeEmit };
|
|
4413
4667
|
|
|
4414
4668
|
//# sourceMappingURL=index.mjs.map
|