@contentrain/emitter-astro 0.16.1 → 0.17.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 +6 -0
- package/dist/index.d.mts +27 -5
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +74 -15
- package/dist/index.mjs.map +1 -1
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -253,6 +253,8 @@ Each path is written percent-encoded, with and without its trailing slash.
|
|
|
253
253
|
`*` or a `:` segment is a pattern to a host file; it is left to the meta-refresh
|
|
254
254
|
fallback, with a warning. A rule that differs from a built page only in letter case is returned too: Netlify matches rules case-insensitively, and macOS and Windows file systems cannot hold `/About/` beside `/about/`, so it would be served over the page. Cloudflare Pages and Vercel files stop at 1,000 rules (2,000 entries with both slash forms, their static limit); the rest are named in `EmitResult.redirects.host_over_limit`, with a warning, and keep only the meta-refresh fallback there. Netlify's file has no limit.
|
|
255
255
|
|
|
256
|
+
**Host-only rules.** `input.hostRedirects` takes rules served only by the host's redirect file, never by `astro.config`, so the build writes no meta-refresh page for them. It is for bulk rules a site does not need as pages: one per WordPress attachment page runs to tens of thousands, and each would become an HTML file in `dist`. They pass the checks `redirects` do (a built page, a case twin, a query string, a pattern, a duplicate) — except the file-name one, since a host file matches `/old.php` itself; but never a file the build writes (robots, `llms.txt`, a feed, `sitemap-index.xml`/`sitemap-N.xml`, `404.html`, `/_astro/`, `/styles/`; in either slash form and any letter case), which a host file would shadow — and an address the site's own rules or the feed redirects already hold is theirs. In a Cloudflare or Vercel file they come after both, so at the rule limit they are the ones left out. With no fallback page behind them, a host-only rule the host file cannot hold is not served at all: a pattern, or one past a named host's limit (`options.redirectHost` `cloudflare` or `vercel`), comes back in `EmitResult.redirects.manual` with its reason, with one `hostRedirects:` warning. With no host named, the Netlify file holds every rule, so one past `vercel.json`'s limit stays written and is named in `host_over_limit` with its own warning — not served on Vercel; name the host to have it reported as manual. On a host with no redirect file (a plain static server) host-only rules are not served.
|
|
257
|
+
|
|
256
258
|
|
|
257
259
|
## Route collisions
|
|
258
260
|
|
|
@@ -280,3 +282,7 @@ collection: Astro serves one file per path, so one of the two routes does not
|
|
|
280
282
|
exist in the built site, and which one survived is an accident of ordering. Fix
|
|
281
283
|
it by giving one route a distinct pattern, or by expanding the narrower one into
|
|
282
284
|
literal routes.
|
|
285
|
+
|
|
286
|
+
## Checking the emitted types
|
|
287
|
+
|
|
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.
|
package/dist/index.d.mts
CHANGED
|
@@ -364,6 +364,18 @@ interface EmitInput {
|
|
|
364
364
|
* back in `EmitResult.redirects.manual` with the reason, to set up at the host.
|
|
365
365
|
*/
|
|
366
366
|
redirects?: RawRedirect[];
|
|
367
|
+
/**
|
|
368
|
+
* Rules served only by the host's redirect file (`public/_redirects`,
|
|
369
|
+
* `vercel.json`), never by `astro.config` — so no meta-refresh page is built
|
|
370
|
+
* for them. For bulk rules the site does not need as pages: one per
|
|
371
|
+
* WordPress attachment page runs to tens of thousands, and a static build
|
|
372
|
+
* would write an HTML file for each. They pass the same checks as
|
|
373
|
+
* `redirects`, and the site's own rules and the feed redirects win an
|
|
374
|
+
* address they share. Without a fallback page, a rule the host file cannot
|
|
375
|
+
* hold — a pattern, or past Cloudflare's or Vercel's rule limit, where these
|
|
376
|
+
* go last — is returned in `EmitResult.redirects.manual`.
|
|
377
|
+
*/
|
|
378
|
+
hostRedirects?: RawRedirect[];
|
|
367
379
|
}
|
|
368
380
|
/**
|
|
369
381
|
* Pure result: every generated file as path → content. Writing to disk is a
|
|
@@ -373,14 +385,20 @@ interface EmitInput {
|
|
|
373
385
|
interface EmitResult {
|
|
374
386
|
files: Record<string, string>;
|
|
375
387
|
warnings: string[];
|
|
376
|
-
/** Present when `EmitInput.redirects`
|
|
388
|
+
/** Present when `EmitInput.redirects` or `hostRedirects` was given. */
|
|
377
389
|
redirects?: {
|
|
378
|
-
written: RawRedirect[];
|
|
390
|
+
/** Rules written: `redirects` to astro.config and the host files, `hostRedirects` to the host files. */written: RawRedirect[]; /** Rules not written, with why — to set up by hand at the host. */
|
|
379
391
|
manual: Array<{
|
|
380
392
|
redirect: RawRedirect;
|
|
381
393
|
reason: string;
|
|
382
394
|
}>; /** The host redirect files written, e.g. `public/_redirects (netlify)`. Empty: meta-refresh only. */
|
|
383
|
-
host_files: string[];
|
|
395
|
+
host_files: string[];
|
|
396
|
+
/**
|
|
397
|
+
* Rules (by `from`) left out of a Cloudflare / Vercel file at its rule
|
|
398
|
+
* limit. A `redirects` rule keeps its meta-refresh page there. A
|
|
399
|
+
* `hostRedirects` rule is here only with no host named (the Netlify file
|
|
400
|
+
* holds it, `vercel.json` does not); past a named host's limit it is in `manual`.
|
|
401
|
+
*/
|
|
384
402
|
host_over_limit: string[];
|
|
385
403
|
};
|
|
386
404
|
}
|
|
@@ -665,9 +683,13 @@ interface RedirectPlan {
|
|
|
665
683
|
declare function builtAddresses(routes: RouteModel[], content: EmitContent): Map<string, string>;
|
|
666
684
|
/**
|
|
667
685
|
* Split the rules into what `astro.config` serves and what the host has to.
|
|
668
|
-
* `built` is `builtAddresses` of the emitted routes.
|
|
686
|
+
* `built` is `builtAddresses` of the emitted routes. `hostOnly`: the rules go
|
|
687
|
+
* to host files alone (`EmitInput.hostRedirects`), so the checks that hold
|
|
688
|
+
* only for a static redirect page (a `from` naming a file) are left out.
|
|
669
689
|
*/
|
|
670
|
-
declare function planRedirects(redirects: RawRedirect$1[], built: Map<string, string
|
|
690
|
+
declare function planRedirects(redirects: RawRedirect$1[], built: Map<string, string>, options?: {
|
|
691
|
+
hostOnly?: boolean;
|
|
692
|
+
}): RedirectPlan;
|
|
671
693
|
/** The `redirects` line for astro.config.mjs, or null when there is nothing to write. */
|
|
672
694
|
declare function astroRedirectsConfig(config: RedirectPlan['config']): string | null;
|
|
673
695
|
/** Hosts whose own redirect file the emitter can write. */
|
package/dist/index.d.mts.map
CHANGED
|
@@ -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":";;;;;AAYA;;;;;AAKA;AAAA,UALiB,WAAA;EACf,IAAA;EACA,IAAA;AAAA;AAAA,UAGe,QAAA;EACf,IAAA;EACA,KAAA;EA8DU;EA5DV,IAAA;EAuFc;;;;;;EAhFd,MAAA,GAAS,MAAA;EAAT;;;;EAKA,GAAA;EASA;EAPA,KAAA;EACA,MAAA;EAcA;;;;;EARA,UAAA;EACA,YAAA;EACA,WAAA;EAwBA;;;;;EAlBA,KAAA,GAAQ,KAAA,UAAe,WAAA;EA+BvB;EA7BA,QAAA;EACA,OAAA;EAwCA;EAtCA,YAAA;EAyCA;;;;EApCA,WAAA;EAgDc;;;;;EA1Cd,KAAA;EAyDQ;EAvDR,UAAA,GAAa,SAAA;EAkEa;EAhE1B,SAAA;EAgE0B;;;;;;EAzD1B,SAAA;EAmEwB;EAjExB,UAAA,GAAa,cAAA;EAkEL;EAhER,OAAA,GAAU,eAAA;EAuEF;;;;;;;;;EA7DR,MAAA;EAsDQ;EApDR,YAAA;EACA,WAAA;EA0DA;EAxDA,OAAA;EA0DA;EAxDA,KAAA,GAAQ,MAAA;EAoER;EAlEA,MAAA;EAoEA;;;;;;;EA5DA,WAAA,GAAc,UAAA;EAwEd;;;;;;EAjEA,KAAA,GAAQ,cAAA;EA8ER;;;;AAIF;EA5EE,OAAA;;EAEA,QAAA;AAAA;;;;;;AAwFF;;;UA7EiB,WAAA;EA8Ef;EA5EA,QAAA;EA8EA;EA5EA,OAAA;EA4EK;EA1EL,KAAA;AAAA;;UAIe,SAAA;EACf,MAAA,EAAQ,MAAA;EACR,KAAA,EAAO,QAAA;EA6EkB;;;;AAY3B;EAnFE,KAAA,GAAQ,MAAA;;EAER,GAAA;EAmFA;;;;;AASF;;;;EAlFE,aAAA;EA0Fc;EAxFd,QAAA,GAAW,WAAA;EA0FD;EAxFV,KAAA;EAwFgB;EAtFhB,WAAA;EA8EQ;EA5ER,KAAA;EAkFc;EAhFd,UAAA,GAAa,SAAA;EAkFb;EAhFA,SAAA;EAgFyB;EA9EzB,WAAA,GAAc,UAAA;EA8EoB;EA5ElC,OAAA;EAgF0B;EA9E1B,QAAA;EA+EA;EA7EA,UAAA,GAAa,cAAA;EAkFE;EAhFf,OAAA,GAAU,eAAA;;EAEV,MAAA;EA+EA;;;;EA1EA,OAAA,GAAU,aAAA;AAAA;;UAIK,aAAA;EACf,IAAA;EA8EA;EA5EA,WAAA;EA8EiB;EA5EjB,KAAA;EAgFA;EA9EA,OAAA;AAAA;AAiFF;;;;AAAA,UA1EiB,cAAA;EACf,KAAA;EACA,WAAA;EACA,KAAA;AAAA;AAAA,UAGe,eAAA,SAAwB,cAAA;EA4GvC;EA1GA,IAAA;AAAA;;UAIe,UAAA;EACf,IAAA;EAgI4B;EA9H5B,IAAA;AAAA;;AAyIF;;;;;UAhIiB,SAAA;EAoIL;EAlIV,KAAA;EACA,MAAA;
|
|
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":";;;;;AAYA;;;;;AAKA;AAAA,UALiB,WAAA;EACf,IAAA;EACA,IAAA;AAAA;AAAA,UAGe,QAAA;EACf,IAAA;EACA,KAAA;EA8DU;EA5DV,IAAA;EAuFc;;;;;;EAhFd,MAAA,GAAS,MAAA;EAAT;;;;EAKA,GAAA;EASA;EAPA,KAAA;EACA,MAAA;EAcA;;;;;EARA,UAAA;EACA,YAAA;EACA,WAAA;EAwBA;;;;;EAlBA,KAAA,GAAQ,KAAA,UAAe,WAAA;EA+BvB;EA7BA,QAAA;EACA,OAAA;EAwCA;EAtCA,YAAA;EAyCA;;;;EApCA,WAAA;EAgDc;;;;;EA1Cd,KAAA;EAyDQ;EAvDR,UAAA,GAAa,SAAA;EAkEa;EAhE1B,SAAA;EAgE0B;;;;;;EAzD1B,SAAA;EAmEwB;EAjExB,UAAA,GAAa,cAAA;EAkEL;EAhER,OAAA,GAAU,eAAA;EAuEF;;;;;;;;;EA7DR,MAAA;EAsDQ;EApDR,YAAA;EACA,WAAA;EA0DA;EAxDA,OAAA;EA0DA;EAxDA,KAAA,GAAQ,MAAA;EAoER;EAlEA,MAAA;EAoEA;;;;;;;EA5DA,WAAA,GAAc,UAAA;EAwEd;;;;;;EAjEA,KAAA,GAAQ,cAAA;EA8ER;;;;AAIF;EA5EE,OAAA;;EAEA,QAAA;AAAA;;;;;;AAwFF;;;UA7EiB,WAAA;EA8Ef;EA5EA,QAAA;EA8EA;EA5EA,OAAA;EA4EK;EA1EL,KAAA;AAAA;;UAIe,SAAA;EACf,MAAA,EAAQ,MAAA;EACR,KAAA,EAAO,QAAA;EA6EkB;;;;AAY3B;EAnFE,KAAA,GAAQ,MAAA;;EAER,GAAA;EAmFA;;;;;AASF;;;;EAlFE,aAAA;EA0Fc;EAxFd,QAAA,GAAW,WAAA;EA0FD;EAxFV,KAAA;EAwFgB;EAtFhB,WAAA;EA8EQ;EA5ER,KAAA;EAkFc;EAhFd,UAAA,GAAa,SAAA;EAkFb;EAhFA,SAAA;EAgFyB;EA9EzB,WAAA,GAAc,UAAA;EA8EoB;EA5ElC,OAAA;EAgF0B;EA9E1B,QAAA;EA+EA;EA7EA,UAAA,GAAa,cAAA;EAkFE;EAhFf,OAAA,GAAU,eAAA;;EAEV,MAAA;EA+EA;;;;EA1EA,OAAA,GAAU,aAAA;AAAA;;UAIK,aAAA;EACf,IAAA;EA8EA;EA5EA,WAAA;EA8EiB;EA5EjB,KAAA;EAgFA;EA9EA,OAAA;AAAA;AAiFF;;;;AAAA,UA1EiB,cAAA;EACf,KAAA;EACA,WAAA;EACA,KAAA;AAAA;AAAA,UAGe,eAAA,SAAwB,cAAA;EA4GvC;EA1GA,IAAA;AAAA;;UAIe,UAAA;EACf,IAAA;EAgI4B;EA9H5B,IAAA;AAAA;;AAyIF;;;;;UAhIiB,SAAA;EAoIL;EAlIV,KAAA;EACA,MAAA;EA0JgB;EAxJhB,IAAA;AAAA;AAAA,UAMe,WAAA;EAuHL;EArHV,KAAA,GAAQ,QAAA;EAsHF;;;;;EAhHN,WAAA,GAAc,MAAA,SAAe,QAAA;EA8HjB;EA5HZ,OAAA,GAAU,MAAA,SAAe,SAAA;AAAA;;UAIV,WAAA;EACf,IAAA;EACA,OAAA;AAAA;;UAIe,kBAAA;EACf,QAAA;EA+IU;EA7IV,QAAA;EA6Ie;EA3If,QAAA;AAAA;AAAA,UAGe,YAAA;EAoIf;EAlIA,OAAA;EAoIW;EAlIX,cAAA,GAAiB,kBAAA;EAoIP;EAlIV,MAAA;EAkI4B;EAhI5B,KAAA;AAAA;AAAA,UAGe,WAAA;EAsIE;EApIjB,WAAA;;EAEA,QAAA;;ACjRF;;;;;AAOA;EDkRE,GAAA;;;;;;;;;;;AElRF;;;EFgSE,OAAA;EEhSoB;AAItB;;;;;EFmSE,MAAA,GAAS,YAAA;EEjSY;;;;AAGvB;;EFqSE,IAAA;EEpSA;;AAUF;;EF+RE,IAAA;EE/R+E;EFiS/E,eAAA;EEjS0B;;;;;AAqL5B;;;;EFsHE,aAAA;EEnHA;;;;AAUF;;;;;EFmHE,SAAA;IAAc,GAAA;IAAc,OAAA;EAAA;;;;AG/V9B;;;;EHuWE,YAAA;AAAA;AAAA,UAGe,SAAA;EACf,EAAA,EAAI,SAAA;EACJ,OAAA,GAAU,WAAA;EACV,GAAA,GAAM,WAAA;EACN,OAAA,GAAU,WAAA;EGzTc;;;;;EH+TxB,OAAA,GAAU,cAAA;;AIpVZ;;;;;EJ2VE,SAAA,GAAY,WAAA;EIxVZ;;;;AASF;;;;;;;EJ2VE,aAAA,GAAgB,WAAA;AAAA;;;;;AK7WlB;ULuXiB,UAAA;EACf,KAAA,EAAO,MAAA;EACP,QAAA;EKzXsC;EL2XtC,SAAA;IK3X6D,wGL6X3D,OAAA,EAAS,WAAA,IK7X4E;IL+XrF,MAAA,EAAQ,KAAA;MAAQ,QAAA,EAAU,WAAA;MAAa,MAAA;IAAA,IM1P3B;IN4PZ,UAAA;IM5PuB;;;AAiD3B;;;INkNI,eAAA;EAAA;AAAA;;;UCnZa,aAAA;EACf,OAAA;EACA,OAAA;AAAA;AAAA,iBAKc,aAAA,CAAc,IAAA,UAAc,GAAA,WAAc,aAAA;;;;cCA7C,SAAA;;cAIA,UAAA;;cAEA,UAAA;;UAGI,UAAA;EACf,UAAA;EACA,OAAA;AAAA;;;;;;;iBASc,WAAA,CAAY,MAAA,EAAQ,UAAA,IAAc,UAAA,WAAqB,UAAA;AAAA,UAqLtD,cAAA;EACf,IAAA;EFrHA;EEuHA,SAAA;EFhHA;;;;;EEsHA,KAAA;AAAA;;iBAIc,gBAAA,CAAiB,IAAA,UAAc,OAAA,WAAkB,cAAA;;;;iBC5OjD,UAAA,CAAW,EAAA;;AHW3B;;;;;AAKA;;iBGAgB,iBAAA,CAAkB,OAAA;;iBAqClB,UAAA,CAAW,KAAA;;;UCrBV,QAAA;EACf,EAAA;EACA,IAAA;EACA,OAAA;;EAEA,KAAA;AAAA;;iBAOc,gBAAA,CAAiB,IAAA;;;;cCpBpB,mBAAA,EAAmB,GAAA;AAAA,iBAEhB,oBAAA,CAAqB,CAAA,EAAG,YAAA,EAAc,OAAA,EAAS,gBAAA;;;;;;ALf/D;;iBMoJgB,WAAA,CAAY,IAAA;;;;;;;;iBAiDZ,aAAA,CAAc,IAAA;AAAA,UA4Bb,WAAA;EACf,IAAA;EN5NA;EM8NA,OAAA;EN3NA;EM6NA,IAAA;ENtNS;;;;;;EM6NT,MAAA;AAAA;;iBAec,YAAA,CAAa,IAAA,WAAe,WAAA;;;;;;;;;;;iBAgD5B,YAAA,CAAa,IAAA;;;;;;cAahB,aAAA;;;;UC1TI,SAAA;EPCW;EOC1B,SAAA;EACA,WAAA;EACA,SAAA;EPEe;EOAf,OAAA;EACA,QAAA;EACA,UAAA,GAAa,cAAA;EACb,OAAA,GAAU,eAAA;EACV,MAAA;AAAA;AAAA,UAGe,iBAAA;EPyDL;EOvDV,OAAA,GAAU,WAAA;EPkFI;;;;;EO5Ed,GAAA;AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;iBAuFc,eAAA,CACd,MAAA,EAAQ,OAAA,CAAQ,MAAA,CAAO,WAAA,EAAa,WAAA,gBACpC,OAAA,GAAS,iBAAA,GACR,SAAA;;;UCnGc,SAAA;ERXA;EQaf,IAAA;;EAEA,IAAA;AAAA;ARVF;;;;;AAAA,iBQkBgB,SAAA,CAAU,OAAA,UAAiB,MAAA,EAAQ,MAAA;AAAA,UAuBlC,gBAAA;ERqBF;EQnBb,OAAA,EAAS,WAAA;EACT,QAAA;AAAA;;;;;;;;iBAYc,cAAA,CAAe,MAAA,EAAQ,UAAA,IAAc,OAAA,EAAS,WAAA,EAAa,UAAA,WAAqB,gBAAA;;;cCjEnF,QAAA;;;;cCSA,cAAA;AVLb;AAAA,cUQa,kBAAA;EAAA;;;;;;;;;;;;;;;;;;;;;;;;KA0BD,WAAA,gBAA2B,kBAAA;;;;;cAM1B,gBAAA,EAAkB,eAAA;;;UC8Bd,aAAA;;EAEf,QAAA;EXxEe;EW0Ef,QAAA;AAAA;AAAA,iBAGc,YAAA,CAAa,IAAA,WAAe,aAAA;;iBAiB5B,cAAA,CAAe,IAAA;;;;iBC1Ff,YAAA,CAAa,MAAA,EAAQ,UAAA,IAAc,OAAA,EAAS,WAAA;;;;cCW/C,iBAAA;AAAA,KACD,cAAA,UAAwB,iBAAA;;UAGnB,cAAA;EACf,QAAA,EAAU,aAAA;EACV,MAAA;AAAA;AAAA,UAGe,YAAA;EbRN;EaUT,MAAA,EAAQ,MAAA;IAAiB,MAAA,EAAQ,cAAA;IAAgB,WAAA;EAAA;Eb2CvC;EazCV,OAAA,EAAS,aAAA;EboEK;EalEd,MAAA,EAAQ,cAAA;AAAA;;;;;;iBAiBM,cAAA,CAAe,MAAA,EAAQ,UAAA,IAAc,OAAA,EAAS,WAAA,GAAc,GAAA;;;;;;;iBAkF5D,aAAA,CAAc,SAAA,EAAW,aAAA,IAAe,KAAA,EAAO,GAAA,kBAAqB,OAAA;EAAW,QAAA;AAAA,IAA4B,YAAA;;iBA+C3G,oBAAA,CAAqB,MAAA,EAAQ,YAAA;;KAQjC,YAAA;AAAA,UAQK,iBAAA;Eb7HF;Ea+Hb,KAAA,EAAO,MAAA;Eb7HG;Ea+HV,OAAA;EbnHA;EaqHA,OAAA;EblHA;;;;EauHA,UAAA;AAAA;;;;;;;AbjFF;;;;;;;;;AAUA;;iBayGgB,iBAAA,CAAkB,MAAA,EAAQ,YAAA,YAAwB,IAAA,GAAO,YAAA,GAAe,iBAAA;;;iBCvLxE,gBAAA,CAAiB,KAAA,EAAO,SAAA,GAAY,UAAA;;iBAwa9B,SAAA,CAAU,MAAA,EAAQ,UAAA,EAAY,GAAA,WAAc,OAAA"}
|
package/dist/index.mjs
CHANGED
|
@@ -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,11 +3866,15 @@ 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;
|
|
@@ -4201,22 +4222,60 @@ function emitAstroProject(input) {
|
|
|
4201
4222
|
};
|
|
4202
4223
|
const hasRedirects = Object.keys(redirectConfig).length > 0;
|
|
4203
4224
|
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;
|
|
4226
|
+
const hostOnlyConfig = {};
|
|
4227
|
+
const hostOnlyRule = /* @__PURE__ */ new Map();
|
|
4228
|
+
const hostOnlyManual = [...hostOnlyPlan?.manual ?? []];
|
|
4229
|
+
if (hostOnlyPlan) {
|
|
4230
|
+
const byFrom = new Map(hostOnlyPlan.written.map((rule) => [decodedPath(rule.from), rule]));
|
|
4231
|
+
for (const [from, rule] of Object.entries(hostOnlyPlan.config)) {
|
|
4232
|
+
const redirect = byFrom.get(from);
|
|
4233
|
+
const key = from.replace(/\/+$/, "").toLowerCase();
|
|
4234
|
+
if (claimed.has(key)) {
|
|
4235
|
+
hostOnlyManual.push({
|
|
4236
|
+
redirect,
|
|
4237
|
+
reason: `another rule already redirects ${from}`
|
|
4238
|
+
});
|
|
4239
|
+
continue;
|
|
4240
|
+
}
|
|
4241
|
+
claimed.add(key);
|
|
4242
|
+
hostOnlyConfig[from] = rule;
|
|
4243
|
+
hostOnlyRule.set(from, redirect);
|
|
4244
|
+
}
|
|
4245
|
+
}
|
|
4204
4246
|
const feedOrder = new Map(feedRules.map((rule, index) => [decodedPath(rule.from), index]));
|
|
4205
4247
|
const feedKeys = Object.keys(feedPlan.config).toSorted((a, b) => (feedOrder.get(a) ?? 0) - (feedOrder.get(b) ?? 0));
|
|
4206
4248
|
const hostOrder = {
|
|
4207
4249
|
...sortedConfig(sourceConfig),
|
|
4208
|
-
...Object.fromEntries(feedKeys.map((key) => [key, feedPlan.config[key]]))
|
|
4250
|
+
...Object.fromEntries(feedKeys.map((key) => [key, feedPlan.config[key]])),
|
|
4251
|
+
...sortedConfig(hostOnlyConfig)
|
|
4209
4252
|
};
|
|
4210
|
-
const hostRedirects =
|
|
4253
|
+
const hostRedirects = Object.keys(hostOrder).length ? hostRedirectFiles(hostOrder, input.options?.redirectHost) : void 0;
|
|
4254
|
+
const hostOnly = (from) => hostOnlyRule.has(from);
|
|
4255
|
+
for (const from of hostRedirects?.skipped ?? []) if (hostOnly(from)) hostOnlyManual.push({
|
|
4256
|
+
redirect: hostOnlyRule.get(from),
|
|
4257
|
+
reason: "from contains \":\" or \"*\", which host redirect files read as a pattern"
|
|
4258
|
+
});
|
|
4259
|
+
const namedHost = input.options?.redirectHost;
|
|
4260
|
+
const hostOnlyOver = (hostRedirects?.over_limit ?? []).filter(hostOnly);
|
|
4261
|
+
if (namedHost !== void 0) for (const from of hostOnlyOver) hostOnlyManual.push({
|
|
4262
|
+
redirect: hostOnlyRule.get(from),
|
|
4263
|
+
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`
|
|
4264
|
+
});
|
|
4265
|
+
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
|
+
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)`);
|
|
4211
4268
|
if (hostRedirects) {
|
|
4212
4269
|
add(hostRedirects.files);
|
|
4213
|
-
|
|
4270
|
+
const overLimit = hostRedirects.over_limit.filter((from) => !hostOnly(from));
|
|
4271
|
+
const skippedPatterns = hostRedirects.skipped.filter((from) => !hostOnly(from));
|
|
4272
|
+
if (overLimit.length) {
|
|
4214
4273
|
const limitedFiles = hostRedirects.written.filter((f) => !f.includes("(netlify)")).join(", ");
|
|
4215
|
-
const feedOver =
|
|
4274
|
+
const feedOver = overLimit.filter((from) => from in feedPlan.config).length;
|
|
4216
4275
|
const which = feedOver ? ` (${feedOver} of them feed redirects — /feed/ and archive feeds come after the site's own rules)` : "";
|
|
4217
|
-
warnings.push(`redirects: ${
|
|
4276
|
+
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
4277
|
}
|
|
4219
|
-
if (
|
|
4278
|
+
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
4279
|
}
|
|
4221
4280
|
if (input.options?.trailingSlash === false && (input.options.redirectHost ?? "vercel") === "vercel") {
|
|
4222
4281
|
const vercel = files["vercel.json"] ? JSON.parse(files["vercel.json"]) : {};
|
|
@@ -4381,11 +4440,11 @@ function emitAstroProject(input) {
|
|
|
4381
4440
|
return {
|
|
4382
4441
|
files,
|
|
4383
4442
|
warnings,
|
|
4384
|
-
...redirectPlan ? { redirects: {
|
|
4385
|
-
written: redirectPlan
|
|
4386
|
-
manual: redirectPlan
|
|
4443
|
+
...redirectPlan || hostOnlyPlan ? { redirects: {
|
|
4444
|
+
written: [...redirectPlan?.written ?? [], ...[...hostOnlyRule.values()].filter((rule) => !unservedHostOnly.has(rule))],
|
|
4445
|
+
manual: [...redirectPlan?.manual ?? [], ...hostOnlyManual],
|
|
4387
4446
|
host_files: hostRedirects?.written ?? [],
|
|
4388
|
-
host_over_limit: hostRedirects?.over_limit ?? []
|
|
4447
|
+
host_over_limit: (hostRedirects?.over_limit ?? []).filter((from) => !hostOnly(from) || namedHost === void 0)
|
|
4389
4448
|
} } : {}
|
|
4390
4449
|
};
|
|
4391
4450
|
}
|