@contentrain/emitter-astro 0.17.0 → 0.18.1

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/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 close = html.toLowerCase().indexOf(`</${name}`, i);
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
  }
@@ -3228,7 +3228,8 @@ export function runtimeOf(host: HTMLElement): Runtime {
3228
3228
  }
3229
3229
 
3230
3230
  function entryOf(host: HTMLElement): EntryRef {
3231
- return { model_id: host.dataset.model ?? '', entry_id: host.dataset.entry ?? '', locale: host.dataset.locale || undefined }
3231
+ const locale = host.dataset.locale
3232
+ return { model_id: host.dataset.model ?? '', entry_id: host.dataset.entry ?? '', ...(locale ? { locale } : {}) }
3232
3233
  }
3233
3234
 
3234
3235
  function setStatus(form: HTMLFormElement, html: string): void {
@@ -3766,7 +3767,7 @@ const REDIRECT_STATUSES = [
3766
3767
  /** Whitespace, a control character or DEL anywhere in the string. */
3767
3768
  const hasControlOrSpace = (value) => [...value].some((char) => char.charCodeAt(0) <= 32 || char.charCodeAt(0) === 127);
3768
3769
  /** `/old/` and `/old` are one address in the directory build format. */
3769
- function addressKey$1(path) {
3770
+ function addressKey$2(path) {
3770
3771
  return path === "/" ? "/" : `${path.replace(/\/+$/, "")}/`;
3771
3772
  }
3772
3773
  /**
@@ -3777,7 +3778,7 @@ function addressKey$1(path) {
3777
3778
  function builtAddresses(routes, content) {
3778
3779
  const out = /* @__PURE__ */ new Map();
3779
3780
  const put = (path, routeId) => {
3780
- if (path !== null && !out.has(addressKey$1(path))) out.set(addressKey$1(path), routeId);
3781
+ if (path !== null && !out.has(addressKey$2(path))) out.set(addressKey$2(path), routeId);
3781
3782
  };
3782
3783
  for (const route of routes) {
3783
3784
  const pagePath = patternToPagePath(route.pattern);
@@ -3879,7 +3880,7 @@ function planRedirects(redirects, built, options = {}) {
3879
3880
  skip("to is not a site-root path or an http(s) URL");
3880
3881
  continue;
3881
3882
  }
3882
- const key = addressKey$1(from.path);
3883
+ const key = addressKey$2(from.path);
3883
3884
  const owner = built.get(key);
3884
3885
  if (owner !== void 0) {
3885
3886
  skip(`the migrated site builds a page at ${key} (route ${owner}) — the page is kept`);
@@ -3901,7 +3902,7 @@ function planRedirects(redirects, built, options = {}) {
3901
3902
  } catch {
3902
3903
  target = redirect.to;
3903
3904
  }
3904
- if (addressKey$1(target) === key) {
3905
+ if (addressKey$2(target) === key) {
3905
3906
  skip("to is the same address as from");
3906
3907
  continue;
3907
3908
  }
@@ -4006,6 +4007,172 @@ function hostRedirectFiles(config, host) {
4006
4007
  };
4007
4008
  }
4008
4009
  //#endregion
4010
+ //#region src/published.ts
4011
+ /**
4012
+ * Why an entry is not built, or null when it is. `now` is epoch ms. With
4013
+ * `requireStatus` an entry that carries no status is held back: the producer
4014
+ * said it passes every entry's status, so a missing one is a gap, not a
4015
+ * published entry.
4016
+ */
4017
+ function holdReason(post, now, requireStatus = false) {
4018
+ if (post.visibility !== void 0 && post.visibility !== "public") return "visibility";
4019
+ if (post.status === void 0 && requireStatus) return "status_missing";
4020
+ if (post.status !== void 0 && post.status !== "published") return "status";
4021
+ if (post.publish_at === void 0) return null;
4022
+ const at = Date.parse(post.publish_at);
4023
+ if (Number.isNaN(at)) return "publish_at_invalid";
4024
+ return at > now ? "scheduled" : null;
4025
+ }
4026
+ const bareHost = (host) => host.replace(/^www\./, "").toLowerCase();
4027
+ /** `/a/b` and `/a/b/` are one address. */
4028
+ const addressKey$1 = (path) => path === "/" ? "/" : `${path.replace(/\/+$/, "")}/`;
4029
+ /**
4030
+ * The held-back address an href points at, or null: a site-root path, or an
4031
+ * absolute URL on the site's own host (`www.` either way).
4032
+ */
4033
+ function withheldTarget(href, gone, siteUrl) {
4034
+ if (!gone.size) return null;
4035
+ let site;
4036
+ try {
4037
+ site = siteUrl ? new URL(siteUrl) : void 0;
4038
+ } catch {
4039
+ site = void 0;
4040
+ }
4041
+ let url;
4042
+ try {
4043
+ url = new URL(href, site ?? "http://site.invalid");
4044
+ } catch {
4045
+ return null;
4046
+ }
4047
+ if (!(site ? bareHost(url.host) === bareHost(site.host) : url.host === "site.invalid") || !/^https?:$/.test(url.protocol)) return null;
4048
+ let path = url.pathname;
4049
+ try {
4050
+ path = decodeURI(path);
4051
+ } catch {}
4052
+ return gone.has(addressKey$1(path)) ? path : null;
4053
+ }
4054
+ /**
4055
+ * The content with every unpublished entry removed — from its collection and
4056
+ * from the list pages that carry it as an item — and links to them unwrapped.
4057
+ * Content with nothing held back is returned as given.
4058
+ */
4059
+ function publishedContent(routes, content, now, siteUrl, requireStatus = false) {
4060
+ const withheld = [];
4061
+ const keep = (collection) => (post) => {
4062
+ const reason = holdReason(post, now, requireStatus);
4063
+ if (!reason) return true;
4064
+ withheld.push({
4065
+ collection,
4066
+ slug: post.slug,
4067
+ ...post.status ? { status: post.status } : {},
4068
+ ...post.visibility ? { visibility: post.visibility } : {},
4069
+ ...post.publish_at ? { publish_at: post.publish_at } : {},
4070
+ reason
4071
+ });
4072
+ return false;
4073
+ };
4074
+ const collections = content.collections ? Object.fromEntries(Object.entries(content.collections).map(([name, posts]) => [name, posts.filter(keep(name))])) : void 0;
4075
+ const isLive = (post) => holdReason(post, now, requireStatus) === null;
4076
+ const posts = content.posts?.filter(content.collections?.["posts"] ? isLive : keep(DEFAULT_COLLECTION));
4077
+ const queries = content.queries ? Object.fromEntries(Object.entries(content.queries).map(([id, pages]) => [id, pages.map((page) => page.items.every(isLive) ? page : {
4078
+ ...page,
4079
+ items: page.items.filter(isLive)
4080
+ })])) : void 0;
4081
+ const listedOnly = Object.values(content.queries ?? {}).flat().flatMap((p) => p.items).some((item) => !isLive(item));
4082
+ if (!withheld.length && !listedOnly) return {
4083
+ content,
4084
+ withheld,
4085
+ links: [],
4086
+ gone: /* @__PURE__ */ new Set()
4087
+ };
4088
+ const filtered = {
4089
+ ...content,
4090
+ ...posts ? { posts } : {},
4091
+ ...collections ? { collections } : {},
4092
+ ...queries ? { queries } : {}
4093
+ };
4094
+ const built = builtAddresses(routes, filtered);
4095
+ const gone = /* @__PURE__ */ new Set();
4096
+ for (const route of routes) {
4097
+ const pagePath = patternToPagePath(route.pattern);
4098
+ if (!pagePath || !pagePath.includes("[") || route.kind !== "single" && route.collection === void 0) continue;
4099
+ const collection = route.collection ?? "posts";
4100
+ for (const post of collectionItems(content, collection)) {
4101
+ const path = entryPath(route.pattern, {
4102
+ ...post.params,
4103
+ slug: post.slug
4104
+ });
4105
+ if (path && !isLive(post) && !built.has(addressKey$1(path))) gone.add(addressKey$1(path));
4106
+ }
4107
+ }
4108
+ const links = [];
4109
+ const unlink = (html, page) => {
4110
+ if (!html || !gone.size) return html;
4111
+ return html.replace(ANCHOR_RE, (anchor, double, single, inner) => {
4112
+ const to = withheldTarget((double ?? single ?? "").replace(/&amp;/g, "&"), gone, siteUrl);
4113
+ if (!to) return anchor;
4114
+ links.push({
4115
+ page,
4116
+ href: to
4117
+ });
4118
+ return inner;
4119
+ });
4120
+ };
4121
+ const unlinkPost = (post, page) => {
4122
+ const body = unlink(post.body, page) ?? post.body;
4123
+ const excerpt_html = unlink(post.excerpt_html, page);
4124
+ return body === post.body && excerpt_html === post.excerpt_html ? post : {
4125
+ ...post,
4126
+ body,
4127
+ ...excerpt_html !== void 0 ? { excerpt_html } : {}
4128
+ };
4129
+ };
4130
+ const pageOf = (collection, post) => {
4131
+ const route = routes.find((r) => (r.kind === "single" || r.collection !== void 0) && (r.collection ?? "posts") === collection);
4132
+ return (route && entryPath(route.pattern, {
4133
+ ...post.params,
4134
+ slug: post.slug
4135
+ })) ?? `${collection}/${post.slug}`;
4136
+ };
4137
+ if (filtered.collections) filtered.collections = Object.fromEntries(Object.entries(filtered.collections).map(([name, list]) => [name, list.map((p) => unlinkPost(p, pageOf(name, p)))]));
4138
+ if (filtered.posts) filtered.posts = filtered.posts.map((p) => unlinkPost(p, pageOf(DEFAULT_COLLECTION, p)));
4139
+ if (filtered.queries) filtered.queries = Object.fromEntries(Object.entries(filtered.queries).map(([id, pages]) => [id, pages.map((page) => {
4140
+ const route = routes.find((r) => r.query === id);
4141
+ const at = (route && entryPath(route.pattern, page.params)) ?? `query ${id}`;
4142
+ return {
4143
+ ...page,
4144
+ items: page.items.map((item) => unlinkPost(item, at))
4145
+ };
4146
+ })]));
4147
+ return {
4148
+ content: filtered,
4149
+ withheld,
4150
+ links,
4151
+ gone
4152
+ };
4153
+ }
4154
+ const ANCHOR_RE = /<a\b[^>]*?\shref\s*=\s*(?:"([^"]*)"|'([^']*)')[^>]*>([\s\S]*?)<\/a\s*>/gi;
4155
+ /** The report lines for what was held back. */
4156
+ function withheldWarnings(withheld, links) {
4157
+ const out = [];
4158
+ if (withheld.length) {
4159
+ const count = (reason) => withheld.filter((w) => w.reason === reason).length;
4160
+ const parts = [
4161
+ count("visibility") && `${count("visibility")} password-protected or private`,
4162
+ count("status_missing") && `${count("status_missing")} with no status (options.requireStatus)`,
4163
+ count("status") && `${count("status")} not published (draft, in review, rejected or archived)`,
4164
+ count("scheduled") && `${count("scheduled")} scheduled for later — built by the first emit after their publish_at`,
4165
+ count("publish_at_invalid") && `${count("publish_at_invalid")} with a publish_at that is not a date`
4166
+ ].filter(Boolean);
4167
+ out.push(`unpublished: ${withheld.length} entries not built — ${parts.join("; ")}. They stay in the content store; EmitResult.withheld lists each`);
4168
+ }
4169
+ if (links.length) {
4170
+ const sample = links.slice(0, 5).map((l) => `${l.href} (on ${l.page})`).join(", ");
4171
+ out.push(`unpublished: ${links.length} links from published pages to unpublished entries kept as plain text — ${sample}${links.length > 5 ? " …" : ""}`);
4172
+ }
4173
+ return out;
4174
+ }
4175
+ //#endregion
4009
4176
  //#region src/seo-entry.ts
4010
4177
  /**
4011
4178
  * A plugin's unrendered template, in its own syntax: Yoast `%%title%%`,
@@ -4184,6 +4351,27 @@ function emitAstroProject(input) {
4184
4351
  files[path] = content;
4185
4352
  }
4186
4353
  };
4354
+ let now = Date.now();
4355
+ if (input.options?.now !== void 0) {
4356
+ const at = Date.parse(input.options.now);
4357
+ 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`);
4358
+ else now = at;
4359
+ }
4360
+ const published = publishedContent(ir.routes, input.content ?? {}, now, ir.site.url, input.options?.requireStatus === true);
4361
+ const content = published.content;
4362
+ warnings.push(...withheldWarnings(published.withheld, published.links));
4363
+ const withheldManual = [];
4364
+ const toPublished = (rules) => rules?.filter((redirect) => {
4365
+ const target = withheldTarget(redirect.to, published.gone, ir.site.url);
4366
+ if (target) withheldManual.push({
4367
+ redirect,
4368
+ reason: `to ${target}, an entry that is not published — the build has no page there`
4369
+ });
4370
+ return !target;
4371
+ });
4372
+ const siteRedirects = toPublished(input.redirects);
4373
+ const hostOnlyRedirects = toPublished(input.hostRedirects);
4374
+ if (withheldManual.length) warnings.push(`unpublished: ${withheldManual.length} redirects point at unpublished entries — not written (EmitResult.redirects.manual has each)`);
4187
4375
  const siteLang = ir.site.locales?.[0] ?? "en";
4188
4376
  const linkable = ir.site.url ? linkSources(ir.routes, siteLang) : [];
4189
4377
  const feedSource = input.options?.feed !== false ? linkable.find((s) => s.collection === DEFAULT_COLLECTION) : void 0;
@@ -4191,11 +4379,11 @@ function emitAstroProject(input) {
4191
4379
  const archiveFeeds = feedSource ? archiveFeedSources(ir.routes, siteLang) : [];
4192
4380
  const archiveFeedRoutes = new Set(archiveFeeds.map((a) => a.routeId));
4193
4381
  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");
4194
- const redirectPlan = input.redirects ? planRedirects(input.redirects, builtAddresses(ir.routes, input.content ?? {})) : void 0;
4195
- if (redirectPlan?.manual.length) warnings.push(`redirects: ${redirectPlan.manual.length} of ${input.redirects.length} rules not written to astro.config — set them up at the host (EmitResult.redirects.manual has each with its reason)`);
4382
+ const redirectPlan = siteRedirects ? planRedirects(siteRedirects, builtAddresses(ir.routes, content)) : void 0;
4383
+ 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)`);
4196
4384
  const seo = input.options?.seo !== false;
4197
- const noindex = noindexPaths(ir.routes, input.content ?? {});
4198
- const built = builtAddresses(ir.routes, input.content ?? {});
4385
+ const noindex = noindexPaths(ir.routes, content);
4386
+ const built = builtAddresses(ir.routes, content);
4199
4387
  const sourceConfig = redirectPlan?.config ?? {};
4200
4388
  const claimed = new Set(Object.keys(sourceConfig).map((from) => from.replace(/\/+$/, "").toLowerCase()));
4201
4389
  const feedRules = [];
@@ -4210,7 +4398,7 @@ function emitAstroProject(input) {
4210
4398
  });
4211
4399
  };
4212
4400
  if (feedSource) addFeedRule(FEED_REDIRECT_FROM, FEED_PATH);
4213
- for (const source of archiveFeeds) for (const page of input.content?.queries?.[source.query] ?? []) {
4401
+ for (const source of archiveFeeds) for (const page of content.queries?.[source.query] ?? []) {
4214
4402
  const address = entryPath(source.pattern, page.params);
4215
4403
  if (address) addFeedRule(`${address}feed/`, `${address}feed.xml`);
4216
4404
  }
@@ -4222,7 +4410,7 @@ function emitAstroProject(input) {
4222
4410
  };
4223
4411
  const hasRedirects = Object.keys(redirectConfig).length > 0;
4224
4412
  add(scaffoldFiles(ir, input.options ?? {}, noindex, hasRedirects ? astroRedirectsConfig(sortedConfig(redirectConfig)) : null, input.runtime));
4225
- const hostOnlyPlan = input.hostRedirects ? planRedirects(input.hostRedirects, built, { hostOnly: true }) : void 0;
4413
+ const hostOnlyPlan = hostOnlyRedirects ? planRedirects(hostOnlyRedirects, built, { hostOnly: true }) : void 0;
4226
4414
  const hostOnlyConfig = {};
4227
4415
  const hostOnlyRule = /* @__PURE__ */ new Map();
4228
4416
  const hostOnlyManual = [...hostOnlyPlan?.manual ?? []];
@@ -4264,7 +4452,7 @@ function emitAstroProject(input) {
4264
4452
  });
4265
4453
  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`);
4266
4454
  const unservedHostOnly = new Set(hostOnlyManual.map((m) => m.redirect));
4267
- if (hostOnlyManual.length) warnings.push(`hostRedirects: ${hostOnlyManual.length} of ${input.hostRedirects.length} host-only rules not written — set them up at the host (EmitResult.redirects.manual has each with its reason)`);
4455
+ 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)`);
4268
4456
  if (hostRedirects) {
4269
4457
  add(hostRedirects.files);
4270
4458
  const overLimit = hostRedirects.over_limit.filter((from) => !hostOnly(from));
@@ -4300,7 +4488,7 @@ function emitAstroProject(input) {
4300
4488
  const bodyMarkersByFamily = /* @__PURE__ */ new Map();
4301
4489
  for (const route of ir.routes) {
4302
4490
  if (route.kind !== "single" && route.collection === void 0) continue;
4303
- const posts = collectionItems(input.content ?? {}, route.collection ?? "posts");
4491
+ const posts = collectionItems(content, route.collection ?? "posts");
4304
4492
  const ids = bodyMarkersByFamily.get(route.family) ?? /* @__PURE__ */ new Set();
4305
4493
  for (const post of posts) for (const id of componentMarkers(post.body)) ids.add(id);
4306
4494
  if (ids.size) bodyMarkersByFamily.set(route.family, ids);
@@ -4309,7 +4497,7 @@ function emitAstroProject(input) {
4309
4497
  const queryBound = /* @__PURE__ */ new Set();
4310
4498
  for (const family of ir.families) for (const placement of family.components ?? []) {
4311
4499
  if (!placement.query) continue;
4312
- const pages = input.content?.queries?.[placement.query];
4500
+ const pages = content.queries?.[placement.query];
4313
4501
  if (!pages) {
4314
4502
  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`);
4315
4503
  continue;
@@ -4342,23 +4530,23 @@ function emitAstroProject(input) {
4342
4530
  for (const ref of refs ?? []) if (!providedCss.has(ref.split("/").pop() ?? ref)) warnings.push(`${owner}: css file "${ref}" not provided in input.css`);
4343
4531
  };
4344
4532
  for (const family of ir.families) missingCss(family.css.files, `family ${family.id}`);
4345
- const allPosts = [...input.content?.posts ?? [], ...Object.values(input.content?.collections ?? {}).flat()];
4533
+ const allPosts = [...content.posts ?? [], ...Object.values(content.collections ?? {}).flat()];
4346
4534
  for (const post of allPosts) missingCss(post.css, `post ${post.slug}`);
4347
- for (const [queryId, queryPages] of Object.entries(input.content?.queries ?? {})) for (const qp of queryPages) missingCss(qp.css, `query ${queryId}`);
4348
- const trailOwners = [...Object.entries(input.content?.collections ?? {}), ...input.content?.posts ? [[DEFAULT_COLLECTION, input.content.posts]] : []];
4535
+ for (const [queryId, queryPages] of Object.entries(content.queries ?? {})) for (const qp of queryPages) missingCss(qp.css, `query ${queryId}`);
4536
+ const trailOwners = [...Object.entries(content.collections ?? {}), ...content.posts ? [[DEFAULT_COLLECTION, content.posts]] : []];
4349
4537
  for (const [name, posts] of trailOwners) {
4350
4538
  const bad = (posts ?? []).filter((p) => badTrail(p.breadcrumbs)).length;
4351
4539
  if (bad) warnings.push(`collection ${name}: ${bad} breadcrumb trails dropped — every crumb needs a name and a site-root-relative path`);
4352
4540
  const badLd = (posts ?? []).filter((p) => badSchema(p.schema)).length;
4353
4541
  if (badLd) warnings.push(`collection ${name}: ${badLd} schema values are not JSON-LD objects — those pages print the generated structured data instead`);
4354
4542
  }
4355
- for (const [queryId, queryPages] of Object.entries(input.content?.queries ?? {})) {
4543
+ for (const [queryId, queryPages] of Object.entries(content.queries ?? {})) {
4356
4544
  const bad = queryPages.filter((qp) => badTrail(qp.breadcrumbs)).length;
4357
4545
  if (bad) warnings.push(`query ${queryId}: ${bad} breadcrumb trails dropped — every crumb needs a name and a site-root-relative path`);
4358
4546
  const badLd = queryPages.filter((qp) => badSchema(qp.schema)).length;
4359
4547
  if (badLd) warnings.push(`query ${queryId}: ${badLd} schema values are not JSON-LD objects — those pages print the generated structured data instead`);
4360
4548
  }
4361
- let routeContent = input.content ?? {};
4549
+ let routeContent = content;
4362
4550
  if (seo) {
4363
4551
  const alternates = withAlternates(ir.routes, routeContent, lang);
4364
4552
  routeContent = alternates.content;
@@ -4413,8 +4601,8 @@ function emitAstroProject(input) {
4413
4601
  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`);
4414
4602
  }
4415
4603
  if ((ir.components ?? []).some((c) => isRuntimeImplemented(c, input.runtime) && c.type === "comments")) {
4416
- const collections = { ...input.content?.collections };
4417
- if (input.content?.posts && !collections["posts"]) collections[DEFAULT_COLLECTION] = input.content.posts;
4604
+ const collections = { ...content.collections };
4605
+ if (content.posts && !collections["posts"]) collections[DEFAULT_COLLECTION] = content.posts;
4418
4606
  for (const [name, posts] of Object.entries(collections)) {
4419
4607
  const unbound = posts.filter((p) => !p.entry).length;
4420
4608
  if (unbound) warnings.push(`collection ${name}: ${unbound} of ${posts.length} posts carry no entry address — the comments component renders nothing on those pages`);
@@ -4442,9 +4630,17 @@ function emitAstroProject(input) {
4442
4630
  warnings,
4443
4631
  ...redirectPlan || hostOnlyPlan ? { redirects: {
4444
4632
  written: [...redirectPlan?.written ?? [], ...[...hostOnlyRule.values()].filter((rule) => !unservedHostOnly.has(rule))],
4445
- manual: [...redirectPlan?.manual ?? [], ...hostOnlyManual],
4633
+ manual: [
4634
+ ...redirectPlan?.manual ?? [],
4635
+ ...hostOnlyManual,
4636
+ ...withheldManual
4637
+ ],
4446
4638
  host_files: hostRedirects?.written ?? [],
4447
4639
  host_over_limit: (hostRedirects?.over_limit ?? []).filter((from) => !hostOnly(from) || namedHost === void 0)
4640
+ } } : {},
4641
+ ...published.withheld.length || published.links.length ? { withheld: {
4642
+ entries: published.withheld,
4643
+ links: published.links
4448
4644
  } } : {}
4449
4645
  };
4450
4646
  }
@@ -4468,6 +4664,6 @@ async function writeEmit(result, dir) {
4468
4664
  await Promise.all(Object.entries(result.files).map(([path, content]) => writeFile(join(dir, path), content, "utf8")));
4469
4665
  }
4470
4666
  //#endregion
4471
- 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 };
4667
+ 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 };
4472
4668
 
4473
4669
  //# sourceMappingURL=index.mjs.map