@contentrain/emitter-astro 0.17.0 → 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 CHANGED
@@ -185,6 +185,39 @@ The feed lists every post, as WordPress's does — a post the source kept out of
185
185
 
186
186
  **Archive feeds.** WordPress serves a feed for every archive page — `/category/news/feed/`, `/tag/x/feed/`, `/author/ada/feed/`. Every dynamic archive route with a query in the site's default language (not its paginated continuation) gets one per page it builds, at `<archive>/feed.xml` (`src/pages/<archive>/feed.xml.ts`): the 10 newest posts that page lists, each at its own address, titled with the page's title. Each archive page links its feed in its head, and `<archive>/feed/` gets a 301 to it in `astro.config` and the host's redirect file, as the main feed does. The feed redirects pass the checks the site's own rules do — none over a page the site builds or a case twin of one, and a source rule for the same address wins — and in a host file with a rule limit they come after the site's own rules, `/feed/` first, so an overflow drops a feed redirect, which the warning counts. The template head's own archive feed link names one term's feed on every page, so it is removed with the other feed links the site does not build (comments, Atom). `options.feed: false` turns these off with the main feed.
187
187
 
188
+ ## Unpublished entries
189
+
190
+ The content store keeps every entry the source had; only the published ones are
191
+ built. `EmitPost.status` is the entry's `EntryMeta.status` (unset reads as
192
+ `published`) and `publish_at` its `EntryMeta.publish_at` — a WordPress post
193
+ scheduled for later is imported as `published` with a `publish_at` ahead. An
194
+ entry is built when its status is `published` and its `publish_at`, if set, is
195
+ not after `options.now` (default: the time of the emit). `EmitPost.visibility`
196
+ (the store's `visibility` field) other than `public` — a password-protected or
197
+ private post — is never built, whatever its status. Every other entry — a
198
+ draft, one in review, rejected or archived, one scheduled for later, one whose
199
+ `publish_at` is not a date, one that is not public — is held back before anything reads the content: no
200
+ page, sitemap line, feed item, llms.txt link, list card or hreflang alternate.
201
+ `EmitResult.withheld.entries` names each with its reason, and a warning counts
202
+ them.
203
+
204
+ An entry with no `status` is published, as before statuses existed — which
205
+ fails open if a producer forgets to pass one. `options.requireStatus: true`
206
+ turns that around: every entry (in posts, collections and list items) must
207
+ carry a status, and one without it is held back (`status_missing`) and
208
+ reported. A producer that reads statuses from the content store sets it.
209
+
210
+ The data files hold only what is live at the emit, so a scheduled entry is
211
+ built by the first emit after its time; rebuilding the same files does not
212
+ publish it.
213
+
214
+ A link on a published page (its body or `excerpt_html`, on its own page or as a
215
+ list card) to a held-back entry would 404: the anchor is replaced by its text
216
+ and listed in `EmitResult.withheld.links`. A link to an address a published
217
+ entry still builds is left alone. A redirect (`redirects` or `hostRedirects`)
218
+ whose target is a held-back entry is not written; it is returned in
219
+ `EmitResult.redirects.manual` with the reason.
220
+
188
221
  ## Binding the runtime later
189
222
 
190
223
  `src/data/runtime.json` is the only place the binding lives; the components
@@ -285,4 +318,4 @@ literal routes.
285
318
 
286
319
  ## Checking the emitted types
287
320
 
288
- The unit suite asserts what the emitter writes; `pnpm --filter @contentrain/emitter-astro test:astro` (after `pnpm build`) checks it the way a migrated site's build does. It emits a site with every kind of page and endpoint — posts, pages, nested category and tag archives with their feeds, an author profile, a list, a static page, the feed, llms.txt, redirects — once with trailing slashes and once without, installs it with npm from its own `package.json`, and runs `astro check`. A pass is a result naming 0 errors. CI and the release workflow run it on every change.
321
+ The unit suite asserts what the emitter writes; `pnpm --filter @contentrain/emitter-astro test:astro` (after `pnpm build`) checks it the way a migrated site's build does. It emits a site with every kind of page and endpoint — posts, pages, nested category and tag archives with their feeds, an author profile, a list, a static page, the feed, llms.txt, redirects, and a draft, an entry in review, a post scheduled for later and password-protected and private posts beside published ones — once with trailing slashes and once without, installs it with npm from its own `package.json` the way a production host does (`NODE_ENV=production`, `CI` set, no terminal), and runs `astro check` and then the site's own `npm run build`. A pass is a result naming 0 errors and a completed build; Astro's "requires @astrojs/check" notice fails it even though Astro exits 0 there. The first site is then built with `astro build`, and the output is read: the published posts have pages, sitemap lines and feed items, the held-back ones have none (nor llms.txt links or list cards), no file holds the password-protected post's text, and the live page's links to held-back entries are text. CI and the release workflow run it on every change.
package/dist/index.d.mts CHANGED
@@ -1,4 +1,4 @@
1
- import { ComponentDef, EntrySourceRef, ModelDefinition, ProjectIR, RawRedirect, RawRedirect as RawRedirect$1, RawSeoEntry, RouteModel, RuntimeBinding, RuntimeBinding as RuntimeBinding$1, SeoProvider } from "@contentrain/types";
1
+ import { ComponentDef, ContentStatus, EntrySourceRef, ModelDefinition, ProjectIR, RawRedirect, RawRedirect as RawRedirect$1, RawSeoEntry, RouteModel, RuntimeBinding, RuntimeBinding as RuntimeBinding$1, SeoProvider } from "@contentrain/types";
2
2
 
3
3
  //#region src/types.d.ts
4
4
  /**
@@ -120,6 +120,49 @@ interface EmitPost {
120
120
  noindex?: boolean;
121
121
  /** The source page was `nofollow`: `<meta name="robots" content="nofollow">`. */
122
122
  nofollow?: boolean;
123
+ /**
124
+ * The entry's workflow status in the content store (`EntryMeta.status`).
125
+ * Only `published` is built; unset reads as published unless
126
+ * `EmitOptions.requireStatus` is set. Any other status
127
+ * keeps the entry out of the site — no page, sitemap line, feed item,
128
+ * llms.txt link or list card — and `EmitResult.withheld` names it.
129
+ */
130
+ status?: ContentStatus;
131
+ /**
132
+ * When a published entry goes live (`EntryMeta.publish_at`, ISO 8601) — a
133
+ * WordPress post scheduled for later. Until then it is held back like a
134
+ * draft; compared with `EmitOptions.now`. A value that is not a date holds
135
+ * the entry back too.
136
+ */
137
+ publish_at?: string;
138
+ /**
139
+ * Who may read the entry (the store's `visibility` field — WordPress
140
+ * `public`, `private`, or `password` for a password-protected post). Anything
141
+ * but `public` is never built, whatever `status` says: no page, sitemap
142
+ * line, feed item, list card, share card or link to it. Unset reads as public.
143
+ */
144
+ visibility?: 'public' | 'private' | 'password' | (string & {});
145
+ }
146
+ /** An entry the build leaves out because it is not published yet. */
147
+ interface WithheldEntry {
148
+ collection: string;
149
+ slug: string;
150
+ status?: ContentStatus;
151
+ publish_at?: string;
152
+ visibility?: string;
153
+ /**
154
+ * `visibility`: password-protected or private · `status`: not published ·
155
+ * `status_missing`: no status, with `EmitOptions.requireStatus` ·
156
+ * `scheduled`: publish_at is ahead · `publish_at_invalid`: publish_at is not a date.
157
+ */
158
+ reason: 'visibility' | 'status' | 'status_missing' | 'scheduled' | 'publish_at_invalid';
159
+ }
160
+ /** A link on a published page to a held-back entry, kept as its text. */
161
+ interface WithheldLink {
162
+ /** The page it was on (site-root path), or `<collection>/<slug>` when no route builds it. */
163
+ page: string;
164
+ /** The address it pointed at. */
165
+ href: string;
123
166
  }
124
167
  /**
125
168
  * One block of a list: a template, optional wrapper, and how many items it takes.
@@ -345,6 +388,20 @@ interface EmitOptions {
345
388
  * Cloudflare Pages site must name its host (no forced rules there).
346
389
  */
347
390
  redirectHost?: 'netlify' | 'cloudflare' | 'vercel';
391
+ /**
392
+ * The moment `EmitPost.publish_at` is compared with, ISO 8601. Default: the
393
+ * time of the emit. The data files hold only what is live then, so a
394
+ * scheduled entry is built by the first emit after its time.
395
+ */
396
+ now?: string;
397
+ /**
398
+ * Hold back every entry — in posts, collections and list items — that
399
+ * carries no `EmitPost.status`, and say so. Default false: an entry with
400
+ * no status is published, as before statuses existed. A producer that
401
+ * reads statuses from the content store sets it, so an entry it failed to
402
+ * give one fails closed instead of going out as published.
403
+ */
404
+ requireStatus?: boolean;
348
405
  }
349
406
  interface EmitInput {
350
407
  ir: ProjectIR;
@@ -401,6 +458,15 @@ interface EmitResult {
401
458
  */
402
459
  host_over_limit: string[];
403
460
  };
461
+ /**
462
+ * Present when entries were held back (`EmitPost.status`, `publish_at`):
463
+ * each one, and each link to one that a published page carried — kept as
464
+ * its text. Redirects to a held-back entry are in `redirects.manual`.
465
+ */
466
+ withheld?: {
467
+ entries: WithheldEntry[];
468
+ links: WithheldLink[];
469
+ };
404
470
  }
405
471
  //#endregion
406
472
  //#region src/css.d.ts
@@ -655,6 +721,28 @@ declare function balanceWarning(html: string): string | null;
655
721
  /** Site-root paths (directory format, `/a/b/`) of every noindex page, sorted. */
656
722
  declare function noindexPaths(routes: RouteModel[], content: EmitContent): string[];
657
723
  //#endregion
724
+ //#region src/published.d.ts
725
+ /**
726
+ * Why an entry is not built, or null when it is. `now` is epoch ms. With
727
+ * `requireStatus` an entry that carries no status is held back: the producer
728
+ * said it passes every entry's status, so a missing one is a gap, not a
729
+ * published entry.
730
+ */
731
+ declare function holdReason(post: Pick<EmitPost, 'status' | 'publish_at' | 'visibility'>, now: number, requireStatus?: boolean): WithheldEntry['reason'] | null;
732
+ interface PublishedContent {
733
+ content: EmitContent;
734
+ withheld: WithheldEntry[];
735
+ links: WithheldLink[];
736
+ /** The addresses held-back entries would have had and no published entry builds (`/a/b/`). */
737
+ gone: Set<string>;
738
+ }
739
+ /**
740
+ * The content with every unpublished entry removed — from its collection and
741
+ * from the list pages that carry it as an item — and links to them unwrapped.
742
+ * Content with nothing held back is returned as given.
743
+ */
744
+ declare function publishedContent(routes: RouteModel[], content: EmitContent, now: number, siteUrl?: string, requireStatus?: boolean): PublishedContent;
745
+ //#endregion
658
746
  //#region src/redirects.d.ts
659
747
  /** The statuses written to `astro.config`; any other is a manual rule. */
660
748
  declare const REDIRECT_STATUSES: readonly [301, 302, 307, 308];
@@ -731,5 +819,5 @@ declare function emitAstroProject(input: EmitInput): EmitResult;
731
819
  /** Write an EmitResult to disk. Separate from emit so the core stays pure. */
732
820
  declare function writeEmit(result: EmitResult, dir: string): Promise<void>;
733
821
  //#endregion
734
- export { type Alternate, type AlternatesResult, type AuthorProfile, type BalanceReport, type Breadcrumb, EMBED_TS, type EmitContent, type EmitCssFile, type EmitInput, type EmitOptions, type EmitPost, type EmitResult, type EmitTermRef, type EntrySourceRef, FEED_ITEMS, FEED_PATH, type FeedLinkResult, type HostRedirectFiles, type ImageMeta, LLMS_LINKS, type LinkSource, type ManualRedirect, type MountRef, type QueryPage, REDIRECT_STATUSES, RUNTIME_IMPLEMENTED, type RawRedirect, type RedirectHost, type RedirectPlan, type RedirectStatus, type RuntimeBinding, SEO_COMPONENT, type SeoFields, type SeoFromRawOptions, type SocialOverride, type StripResult, type TwitterOverride, UI_STRINGS_DIR, UI_STRINGS_MODEL, UI_STRING_DEFAULTS, type UiStringKey, astroRedirectsConfig, balanceWarning, bodySeoLeaks, builtAddresses, checkBalance, componentMarkers, emitAstroProject, entryPath, hostRedirectFiles, isRuntimeImplemented, linkSources, noindexPaths, pascalCase, patternToPagePath, planRedirects, publisherIdOf, rewriteFeedLinks, seoFromRawEntry, stableJson, stripSeoTags, websiteIdOf, withAlternates, wrapLegacyCss, writeEmit };
822
+ export { type Alternate, type AlternatesResult, type AuthorProfile, type BalanceReport, type Breadcrumb, EMBED_TS, type EmitContent, type EmitCssFile, type EmitInput, type EmitOptions, type EmitPost, type EmitResult, type EmitTermRef, type EntrySourceRef, FEED_ITEMS, FEED_PATH, type FeedLinkResult, type HostRedirectFiles, type ImageMeta, LLMS_LINKS, type LinkSource, type ManualRedirect, type MountRef, type PublishedContent, type QueryPage, REDIRECT_STATUSES, RUNTIME_IMPLEMENTED, type RawRedirect, type RedirectHost, type RedirectPlan, type RedirectStatus, type RuntimeBinding, SEO_COMPONENT, type SeoFields, type SeoFromRawOptions, type SocialOverride, type StripResult, type TwitterOverride, UI_STRINGS_DIR, UI_STRINGS_MODEL, UI_STRING_DEFAULTS, type UiStringKey, type WithheldEntry, type WithheldLink, 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 };
735
823
  //# sourceMappingURL=index.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.mts","names":[],"sources":["../src/types.ts","../src/css.ts","../src/feed.ts","../src/util.ts","../src/layouts.ts","../src/components.ts","../src/seo.ts","../src/seo-entry.ts","../src/alternates.ts","../src/embed.ts","../src/ui-strings.ts","../src/balance.ts","../src/noindex.ts","../src/redirects.ts","../src/index.ts"],"mappings":"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"}
1
+ {"version":3,"file":"index.d.mts","names":[],"sources":["../src/types.ts","../src/css.ts","../src/feed.ts","../src/util.ts","../src/layouts.ts","../src/components.ts","../src/seo.ts","../src/seo-entry.ts","../src/alternates.ts","../src/embed.ts","../src/ui-strings.ts","../src/balance.ts","../src/noindex.ts","../src/published.ts","../src/redirects.ts","../src/index.ts"],"mappings":"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"}
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
  }
@@ -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$1(path) {
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$1(path))) out.set(addressKey$1(path), routeId);
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);
@@ -3879,7 +3879,7 @@ function planRedirects(redirects, built, options = {}) {
3879
3879
  skip("to is not a site-root path or an http(s) URL");
3880
3880
  continue;
3881
3881
  }
3882
- const key = addressKey$1(from.path);
3882
+ const key = addressKey$2(from.path);
3883
3883
  const owner = built.get(key);
3884
3884
  if (owner !== void 0) {
3885
3885
  skip(`the migrated site builds a page at ${key} (route ${owner}) — the page is kept`);
@@ -3901,7 +3901,7 @@ function planRedirects(redirects, built, options = {}) {
3901
3901
  } catch {
3902
3902
  target = redirect.to;
3903
3903
  }
3904
- if (addressKey$1(target) === key) {
3904
+ if (addressKey$2(target) === key) {
3905
3905
  skip("to is the same address as from");
3906
3906
  continue;
3907
3907
  }
@@ -4006,6 +4006,172 @@ function hostRedirectFiles(config, host) {
4006
4006
  };
4007
4007
  }
4008
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(/&amp;/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
4009
4175
  //#region src/seo-entry.ts
4010
4176
  /**
4011
4177
  * A plugin's unrendered template, in its own syntax: Yoast `%%title%%`,
@@ -4184,6 +4350,27 @@ function emitAstroProject(input) {
4184
4350
  files[path] = content;
4185
4351
  }
4186
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)`);
4187
4374
  const siteLang = ir.site.locales?.[0] ?? "en";
4188
4375
  const linkable = ir.site.url ? linkSources(ir.routes, siteLang) : [];
4189
4376
  const feedSource = input.options?.feed !== false ? linkable.find((s) => s.collection === DEFAULT_COLLECTION) : void 0;
@@ -4191,11 +4378,11 @@ function emitAstroProject(input) {
4191
4378
  const archiveFeeds = feedSource ? archiveFeedSources(ir.routes, siteLang) : [];
4192
4379
  const archiveFeedRoutes = new Set(archiveFeeds.map((a) => a.routeId));
4193
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");
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)`);
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)`);
4196
4383
  const seo = input.options?.seo !== false;
4197
- const noindex = noindexPaths(ir.routes, input.content ?? {});
4198
- const built = builtAddresses(ir.routes, input.content ?? {});
4384
+ const noindex = noindexPaths(ir.routes, content);
4385
+ const built = builtAddresses(ir.routes, content);
4199
4386
  const sourceConfig = redirectPlan?.config ?? {};
4200
4387
  const claimed = new Set(Object.keys(sourceConfig).map((from) => from.replace(/\/+$/, "").toLowerCase()));
4201
4388
  const feedRules = [];
@@ -4210,7 +4397,7 @@ function emitAstroProject(input) {
4210
4397
  });
4211
4398
  };
4212
4399
  if (feedSource) addFeedRule(FEED_REDIRECT_FROM, FEED_PATH);
4213
- for (const source of archiveFeeds) for (const page of input.content?.queries?.[source.query] ?? []) {
4400
+ for (const source of archiveFeeds) for (const page of content.queries?.[source.query] ?? []) {
4214
4401
  const address = entryPath(source.pattern, page.params);
4215
4402
  if (address) addFeedRule(`${address}feed/`, `${address}feed.xml`);
4216
4403
  }
@@ -4222,7 +4409,7 @@ function emitAstroProject(input) {
4222
4409
  };
4223
4410
  const hasRedirects = Object.keys(redirectConfig).length > 0;
4224
4411
  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;
4412
+ const hostOnlyPlan = hostOnlyRedirects ? planRedirects(hostOnlyRedirects, built, { hostOnly: true }) : void 0;
4226
4413
  const hostOnlyConfig = {};
4227
4414
  const hostOnlyRule = /* @__PURE__ */ new Map();
4228
4415
  const hostOnlyManual = [...hostOnlyPlan?.manual ?? []];
@@ -4264,7 +4451,7 @@ function emitAstroProject(input) {
4264
4451
  });
4265
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`);
4266
4453
  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)`);
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)`);
4268
4455
  if (hostRedirects) {
4269
4456
  add(hostRedirects.files);
4270
4457
  const overLimit = hostRedirects.over_limit.filter((from) => !hostOnly(from));
@@ -4300,7 +4487,7 @@ function emitAstroProject(input) {
4300
4487
  const bodyMarkersByFamily = /* @__PURE__ */ new Map();
4301
4488
  for (const route of ir.routes) {
4302
4489
  if (route.kind !== "single" && route.collection === void 0) continue;
4303
- const posts = collectionItems(input.content ?? {}, route.collection ?? "posts");
4490
+ const posts = collectionItems(content, route.collection ?? "posts");
4304
4491
  const ids = bodyMarkersByFamily.get(route.family) ?? /* @__PURE__ */ new Set();
4305
4492
  for (const post of posts) for (const id of componentMarkers(post.body)) ids.add(id);
4306
4493
  if (ids.size) bodyMarkersByFamily.set(route.family, ids);
@@ -4309,7 +4496,7 @@ function emitAstroProject(input) {
4309
4496
  const queryBound = /* @__PURE__ */ new Set();
4310
4497
  for (const family of ir.families) for (const placement of family.components ?? []) {
4311
4498
  if (!placement.query) continue;
4312
- const pages = input.content?.queries?.[placement.query];
4499
+ const pages = content.queries?.[placement.query];
4313
4500
  if (!pages) {
4314
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`);
4315
4502
  continue;
@@ -4342,23 +4529,23 @@ function emitAstroProject(input) {
4342
4529
  for (const ref of refs ?? []) if (!providedCss.has(ref.split("/").pop() ?? ref)) warnings.push(`${owner}: css file "${ref}" not provided in input.css`);
4343
4530
  };
4344
4531
  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()];
4532
+ const allPosts = [...content.posts ?? [], ...Object.values(content.collections ?? {}).flat()];
4346
4533
  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]] : []];
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]] : []];
4349
4536
  for (const [name, posts] of trailOwners) {
4350
4537
  const bad = (posts ?? []).filter((p) => badTrail(p.breadcrumbs)).length;
4351
4538
  if (bad) warnings.push(`collection ${name}: ${bad} breadcrumb trails dropped — every crumb needs a name and a site-root-relative path`);
4352
4539
  const badLd = (posts ?? []).filter((p) => badSchema(p.schema)).length;
4353
4540
  if (badLd) warnings.push(`collection ${name}: ${badLd} schema values are not JSON-LD objects — those pages print the generated structured data instead`);
4354
4541
  }
4355
- for (const [queryId, queryPages] of Object.entries(input.content?.queries ?? {})) {
4542
+ for (const [queryId, queryPages] of Object.entries(content.queries ?? {})) {
4356
4543
  const bad = queryPages.filter((qp) => badTrail(qp.breadcrumbs)).length;
4357
4544
  if (bad) warnings.push(`query ${queryId}: ${bad} breadcrumb trails dropped — every crumb needs a name and a site-root-relative path`);
4358
4545
  const badLd = queryPages.filter((qp) => badSchema(qp.schema)).length;
4359
4546
  if (badLd) warnings.push(`query ${queryId}: ${badLd} schema values are not JSON-LD objects — those pages print the generated structured data instead`);
4360
4547
  }
4361
- let routeContent = input.content ?? {};
4548
+ let routeContent = content;
4362
4549
  if (seo) {
4363
4550
  const alternates = withAlternates(ir.routes, routeContent, lang);
4364
4551
  routeContent = alternates.content;
@@ -4413,8 +4600,8 @@ function emitAstroProject(input) {
4413
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`);
4414
4601
  }
4415
4602
  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;
4603
+ const collections = { ...content.collections };
4604
+ if (content.posts && !collections["posts"]) collections[DEFAULT_COLLECTION] = content.posts;
4418
4605
  for (const [name, posts] of Object.entries(collections)) {
4419
4606
  const unbound = posts.filter((p) => !p.entry).length;
4420
4607
  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 +4629,17 @@ function emitAstroProject(input) {
4442
4629
  warnings,
4443
4630
  ...redirectPlan || hostOnlyPlan ? { redirects: {
4444
4631
  written: [...redirectPlan?.written ?? [], ...[...hostOnlyRule.values()].filter((rule) => !unservedHostOnly.has(rule))],
4445
- manual: [...redirectPlan?.manual ?? [], ...hostOnlyManual],
4632
+ manual: [
4633
+ ...redirectPlan?.manual ?? [],
4634
+ ...hostOnlyManual,
4635
+ ...withheldManual
4636
+ ],
4446
4637
  host_files: hostRedirects?.written ?? [],
4447
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
4448
4643
  } } : {}
4449
4644
  };
4450
4645
  }
@@ -4468,6 +4663,6 @@ async function writeEmit(result, dir) {
4468
4663
  await Promise.all(Object.entries(result.files).map(([path, content]) => writeFile(join(dir, path), content, "utf8")));
4469
4664
  }
4470
4665
  //#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 };
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 };
4472
4667
 
4473
4668
  //# sourceMappingURL=index.mjs.map