@contentrain/wp-import 0.8.0 → 0.9.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
@@ -140,6 +140,40 @@ contains `menus_require_auth` — an empty menu list is never presented as the
140
140
  site having none. The `menus` model gains a `locations` field only when the
141
141
  source named them.
142
142
 
143
+ A menu item that names a public post or term links to that post's or term's
144
+ public address (its REST `link`). The address the item stored is not used: a
145
+ block keeps the URL it had when it was saved, and a hand-typed `?page_id=` is a
146
+ form the migrated site cannot serve. A hand-typed `?page_id=` / `?p=` counts
147
+ as this site's with or without a leading `www.`.
148
+
149
+ ### Redirects over REST
150
+
151
+ REST reads one source of redirects: the Redirection plugin (`redirection/v1`).
152
+ This needs an application password of a user who may manage the plugin. Its
153
+ rules are shaped as the Bridge shapes them. Each rule lands in exactly one of
154
+ two places:
155
+
156
+ - `redirects`: the rules the site serves, with the plugin's query, case and
157
+ trailing-slash modes. A "gone" rule (410, 451) is served with an empty `to`.
158
+ - `redirects_excluded`: every other rule, with its reason (disabled,
159
+ conditional, a 404 or pass-through action).
160
+
161
+ Yoast Premium, Rank Math, Safe Redirect Manager and `.htaccess` keep their
162
+ rules where only the Bridge export reads them. So `gaps` always contains
163
+ `redirects_partial`. It also contains `redirects_require_auth` when the site
164
+ runs Redirection but its rules could not be read with the credential given.
165
+
166
+ A conditional rule (login, cookie, header, IP, role…) is excluded. Only a login
167
+ rule keeps its `condition`, which is its two addresses. Any other condition
168
+ holds the value a visitor must present, often a secret, so it is never
169
+ exported; the reason names its type.
170
+
171
+ Each attachment carries its attachment page as `link`, so the migrated site
172
+ can redirect it. The link is kept only when the attachment has no parent or
173
+ its parent is proven public, because the page sits under the parent's address
174
+ and would otherwise carry a draft's, a private post's or a scheduled post's
175
+ slug.
176
+
143
177
  ### ACF fields, custom post types and taxonomies over REST
144
178
 
145
179
  ACF / Secure Custom Fields values arrive on a post's `acf` key — only for field
package/dist/index.d.mts CHANGED
@@ -71,6 +71,10 @@ interface RestImportResult {
71
71
  * What this rung could not read, as codes a caller can act on (the text is in `warnings`).
72
72
  * `menus_require_auth`: menus and block navigation are only readable with an application password
73
73
  * of a user who may edit theme options — none was given, the site rejected it, or it lacks that right.
74
+ * `redirects_partial`: always, over REST — it reads only the Redirection plugin's rules; Yoast Premium, Rank Math,
75
+ * Safe Redirect Manager and `.htaccess` keep theirs where only the Bridge export reads them.
76
+ * `redirects_require_auth`: the site runs Redirection, and its rules need an application password of a user who
77
+ * may manage it — none was given, the site rejected it, or it lacks that right.
74
78
  * `acf_partial`: the site runs ACF / Secure Custom Fields; REST shows only the field groups set to
75
79
  * `show_in_rest` (off by default) and no options page — the Bridge export reads the rest. Where the site
76
80
  * states no field types (plain ACF, no `<name>_source`), a secret field is recognised by its name only.
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.mts","names":[],"sources":["../src/wxr.ts","../src/rest.ts","../src/contentrain.ts","../src/comments.ts","../src/php-unserialize.ts","../src/core.ts","../src/delta.ts"],"mappings":";;;;UA2BiB,QAAA;EACf,UAAA;IAAc,QAAA;IAAkB,OAAA;IAAiB,MAAA;IAAgB,IAAA;EAAA;EACjE,UAAA;EACA,gBAAA;EACA,MAAA,EAAQ,MAAA;AAAA;AAAA,UAGO,cAAA;EACf,GAAA,EAAK,KAAA;EACL,KAAA,EAAO,QAAA;AAAA;AAAA,iBAiBa,QAAA,CAAS,KAAA,WAAgB,QAAA,EAAU,IAAA;EAAS,IAAA;AAAA,IAAkB,OAAA,CAAQ,cAAA;;;UCpC3E,iBAAA;EACf,MAAA;;EAEA,SAAA,UAAmB,KAAA;EDOI;;;;;;;;ECEvB,IAAA;IAAS,IAAA;IAAc,WAAA;EAAA;EACvB,OAAA;EDCc;AAGhB;;;;ECEE,WAAA;EDDK;;;;ECML,QAAA;EACA,IAAA;AAAA;AAAA,UAGe,gBAAA;EACf,GAAA,EAAK,KAAA;EACL,QAAA;EDMkF;;;;;;;;ECGlF,UAAA;IAAc,MAAA;IAA0C,SAAA;EAAA;;;AAvC1D;;;;;;EAgDE,IAAA;AAAA;AAAA,iBA2EoB,cAAA,CAAe,OAAA,EAAS,iBAAA,GAAoB,OAAA,CAAQ,gBAAA;;;UChHzD,YAAA;EACf,cAAA;EACA,aAAA;EACA,cAAA;EACA,WAAA,EAAa,MAAA;EACb,UAAA,EAAY,MAAA;EFFE;EEId,mBAAA,EAAqB,MAAA;EFPP;EESd,oBAAA;EACA,aAAA;EACA,iBAAA;EACA,MAAA,EAAQ,MAAA;IAAiB,IAAA;IAAc,MAAA;IAAgB,MAAA;IAAgB,OAAA;EAAA;EFNxD;EEQf,OAAA;;EAEA,kBAAA;EFTA;;;;;;EEgBA,yBAAA;EFE4B;;;;EEG5B,kBAAA;AAAA;AAAA,UAGe,iBAAA;EACf,KAAA,EAAO,MAAA;EACP,gBAAA,EAAkB,cAAA;EAClB,MAAA,EAAQ,YAAA;AAAA;AAAA,iBA0CM,gBAAA,CAAiB,GAAA,EAAK,KAAA,EAAO,IAAA;EAAS,SAAA;AAAA,IAAuB,iBAAA;;;;;;AF7E7E;;;iBGbgB,cAAA,CAAe,GAAA,EAAK,KAAA;EAAU,QAAA,EAAU,UAAA;EAAc,QAAA,EAAU,WAAA,CAAY,cAAA;AAAA;AAAA,iBAgB5E,mBAAA,CAAoB,GAAA,EAAK,KAAA,EAAO,OAAA,EAAS,cAAA,EAAgB,IAAA;EAAS,YAAA;AAAA,IAA0B,cAAA;;iBAiB5F,iBAAA,CAAkB,GAAA,EAAK,cAAA,GAAiB,eAAA;;;UC3CvC,iBAAA;EACf,EAAA;EACA,UAAA;EACA,KAAA;EACA,KAAA;AAAA;AAAA,iBAGc,eAAA,CAAgB,CAAA,YAAa,CAAA;AAAA,iBAI7B,cAAA,CAAe,GAAA;;iBAyFf,cAAA,CAAe,CAAA,WAAY,iBAAA;;;cC9F9B,KAAA,GAAS,CAAA;;cAST,KAAA,GAAS,CAAA;;cAcT,KAAA,GAAS,CAAA;;;;;;;;;ALCtB;cK8Ba,UAAA,EAAU,MAAA;;cAIV,WAAA,EAAW,MAAA;;cAOX,UAAA,GAAc,CAAA;AAAA,cAGd,OAAA,GAAW,CAAA;;;;UC3DP,UAAA;EACf,KAAA,EAAO,MAAA;EACP,gBAAA,EAAkB,cAAA;AAAA;AAAA,UAGH,oBAAA;ENOD;EMLd,KAAA,EAAO,eAAA;ENEO;EMAd,KAAA,EAAO,UAAA;ENA0C;EMEjD,QAAA,GAAW,UAAA;ENDX;;;;;EMOA,UAAA;ENFe;;;;EMOf,SAAA;AAAA;AAAA,cAGW,kBAAA;AAAA,cACA,iBAAA;;;ANQb;;;;;;;;;;iBM8NgB,eAAA,CAAgB,KAAA,EAAO,oBAAA,GAAuB,eAAA;;iBAkC9C,uBAAA,CAAwB,IAAA,EAAM,eAAA"}
1
+ {"version":3,"file":"index.d.mts","names":[],"sources":["../src/wxr.ts","../src/rest.ts","../src/contentrain.ts","../src/comments.ts","../src/php-unserialize.ts","../src/core.ts","../src/delta.ts"],"mappings":";;;;UA2BiB,QAAA;EACf,UAAA;IAAc,QAAA;IAAkB,OAAA;IAAiB,MAAA;IAAgB,IAAA;EAAA;EACjE,UAAA;EACA,gBAAA;EACA,MAAA,EAAQ,MAAA;AAAA;AAAA,UAGO,cAAA;EACf,GAAA,EAAK,KAAA;EACL,KAAA,EAAO,QAAA;AAAA;AAAA,iBAiBa,QAAA,CAAS,KAAA,WAAgB,QAAA,EAAU,IAAA;EAAS,IAAA;AAAA,IAAkB,OAAA,CAAQ,cAAA;;;UCnC3E,iBAAA;EACf,MAAA;;EAEA,SAAA,UAAmB,KAAA;EDMI;;;;;;;;ECGvB,IAAA;IAAS,IAAA;IAAc,WAAA;EAAA;EACvB,OAAA;EDAc;AAGhB;;;;ECGE,WAAA;EDFK;;;;ECOL,QAAA;EACA,IAAA;AAAA;AAAA,UAGe,gBAAA;EACf,GAAA,EAAK,KAAA;EACL,QAAA;EDKkF;;;;;;;;ECIlF,UAAA;IAAc,MAAA;IAA0C,SAAA;EAAA;;;AAvC1D;;;;;;;;;;EAoDE,IAAA;AAAA;AAAA,iBA2EoB,cAAA,CAAe,OAAA,EAAS,iBAAA,GAAoB,OAAA,CAAQ,gBAAA;;;UCrHzD,YAAA;EACf,cAAA;EACA,aAAA;EACA,cAAA;EACA,WAAA,EAAa,MAAA;EACb,UAAA,EAAY,MAAA;EFFE;EEId,mBAAA,EAAqB,MAAA;EFPP;EESd,oBAAA;EACA,aAAA;EACA,iBAAA;EACA,MAAA,EAAQ,MAAA;IAAiB,IAAA;IAAc,MAAA;IAAgB,MAAA;IAAgB,OAAA;EAAA;EFNxD;EEQf,OAAA;;EAEA,kBAAA;EFTA;;;;;;EEgBA,yBAAA;EFE4B;;;;EEG5B,kBAAA;AAAA;AAAA,UAGe,iBAAA;EACf,KAAA,EAAO,MAAA;EACP,gBAAA,EAAkB,cAAA;EAClB,MAAA,EAAQ,YAAA;AAAA;AAAA,iBA0CM,gBAAA,CAAiB,GAAA,EAAK,KAAA,EAAO,IAAA;EAAS,SAAA;AAAA,IAAuB,iBAAA;;;;;;AF7E7E;;;iBGbgB,cAAA,CAAe,GAAA,EAAK,KAAA;EAAU,QAAA,EAAU,UAAA;EAAc,QAAA,EAAU,WAAA,CAAY,cAAA;AAAA;AAAA,iBAgB5E,mBAAA,CAAoB,GAAA,EAAK,KAAA,EAAO,OAAA,EAAS,cAAA,EAAgB,IAAA;EAAS,YAAA;AAAA,IAA0B,cAAA;;iBAiB5F,iBAAA,CAAkB,GAAA,EAAK,cAAA,GAAiB,eAAA;;;UC3CvC,iBAAA;EACf,EAAA;EACA,UAAA;EACA,KAAA;EACA,KAAA;AAAA;AAAA,iBAGc,eAAA,CAAgB,CAAA,YAAa,CAAA;AAAA,iBAI7B,cAAA,CAAe,GAAA;;iBAyFf,cAAA,CAAe,CAAA,WAAY,iBAAA;;;cC9F9B,KAAA,GAAS,CAAA;;cAST,KAAA,GAAS,CAAA;;cAcT,KAAA,GAAS,CAAA;;;;;;;;;ALCtB;cK8Ba,UAAA,EAAU,MAAA;;cAIV,WAAA,EAAW,MAAA;;cAOX,UAAA,GAAc,CAAA;AAAA,cAGd,OAAA,GAAW,CAAA;;;;UC3DP,UAAA;EACf,KAAA,EAAO,MAAA;EACP,gBAAA,EAAkB,cAAA;AAAA;AAAA,UAGH,oBAAA;ENOD;EMLd,KAAA,EAAO,eAAA;ENEO;EMAd,KAAA,EAAO,UAAA;ENA0C;EMEjD,QAAA,GAAW,UAAA;ENDX;;;;;EMOA,UAAA;ENFe;;;;EMOf,SAAA;AAAA;AAAA,cAGW,kBAAA;AAAA,cACA,iBAAA;;;ANQb;;;;;;;;;;iBM8NgB,eAAA,CAAgB,KAAA,EAAO,oBAAA,GAAuB,eAAA;;iBAkC9C,uBAAA,CAAwB,IAAA,EAAM,eAAA"}
package/dist/index.mjs CHANGED
@@ -525,6 +525,7 @@ async function parseWxr(input, opts) {
525
525
  mime: guessMime(r.attachment_url ?? r.guid ?? ""),
526
526
  parent: r.parent,
527
527
  parent_resolved: r.parent ? byId.has(r.parent) : null,
528
+ link: !r.parent || byId.get(r.parent)?.status === "publish" && !byId.get(r.parent)?.password ? r.link || null : null,
528
529
  author: r.author,
529
530
  date: r.date,
530
531
  status: r.status,
@@ -1033,6 +1034,121 @@ function acfRows(value) {
1033
1034
  }))(r) : r) : value;
1034
1035
  }
1035
1036
  //#endregion
1037
+ //#region src/rest-redirects.ts
1038
+ /** Redirection's own defaults, under a site that states none. */
1039
+ const DEFAULTS = {
1040
+ flag_query: "exact",
1041
+ flag_case: false,
1042
+ flag_trailing: false
1043
+ };
1044
+ const MODULES = {
1045
+ 2: "apache",
1046
+ 3: "nginx"
1047
+ };
1048
+ /** Codes that say an address is gone: served as that answer, with no target. */
1049
+ const GONE = new Set([410, 451]);
1050
+ /** Redirection's query modes; `exactorder` (the same parameters in the same order) is read as `exact`. */
1051
+ const QUERY = {
1052
+ exact: "exact",
1053
+ exactorder: "exact",
1054
+ ignore: "ignore",
1055
+ pass: "pass"
1056
+ };
1057
+ /** The flags one layer states, as Redirection accepts them: a known query mode, a true boolean (not `"1"`). */
1058
+ const valid = (f) => ({
1059
+ ...typeof f?.flag_query === "string" && f.flag_query in QUERY ? { flag_query: f.flag_query } : {},
1060
+ ...typeof f?.flag_case === "boolean" ? { flag_case: f.flag_case } : {},
1061
+ ...typeof f?.flag_trailing === "boolean" ? { flag_trailing: f.flag_trailing } : {}
1062
+ });
1063
+ /** By id as bytes, as the Bridge sorts (`strcmp`). */
1064
+ const order = (a, b) => (a.id ?? "") < (b.id ?? "") ? -1 : (a.id ?? "") > (b.id ?? "") ? 1 : 0;
1065
+ const on = (x) => x.enabled ?? (x.status === void 0 || x.status === "enabled");
1066
+ /** Same-site targets become root-relative, like every `from`; other hosts stay absolute (as the Bridge does). */
1067
+ function relative(url, origin) {
1068
+ try {
1069
+ const home = new URL(origin);
1070
+ const u = new URL(url, `${origin}/`);
1071
+ if (u.host.toLowerCase() !== home.host.toLowerCase()) return url;
1072
+ return `${u.pathname}${u.search}${u.hash}`;
1073
+ } catch {
1074
+ return url;
1075
+ }
1076
+ }
1077
+ /**
1078
+ * Redirection's rules and groups → the rules the site serves and the ones it does not, sorted by id.
1079
+ * A rule's query, case and trailing-slash modes are what Redirection applies to it: its own flags, else the site's
1080
+ * (`options`), else the plugin's defaults. Without `options` (not readable) only a rule's own flags are stated.
1081
+ */
1082
+ function redirectionRules(items, groups, origin, options) {
1083
+ const site = options ? {
1084
+ ...DEFAULTS,
1085
+ ...valid(options)
1086
+ } : {};
1087
+ const byId = new Map(groups.map((g) => [g.id, g]));
1088
+ const redirects = [];
1089
+ const excluded = [];
1090
+ for (const item of items) {
1091
+ const group = item.group_id === void 0 ? void 0 : byId.get(item.group_id);
1092
+ const code = Number(item.action_code ?? 301);
1093
+ const data = item.action_data;
1094
+ const target = typeof data === "string" ? data : data?.url ?? "";
1095
+ const own = item.match_data?.source ?? {};
1096
+ const flags = {
1097
+ ...site,
1098
+ ...valid(own)
1099
+ };
1100
+ const rule = {
1101
+ id: `redirection:${item.id}`,
1102
+ from: item.url ?? "",
1103
+ source: "redirection",
1104
+ match: "url",
1105
+ regex: item.regex === true || own.flag_regex === true
1106
+ };
1107
+ const served = group?.module_id !== void 0 ? MODULES[group.module_id] : void 0;
1108
+ if (served) rule.served_by = served;
1109
+ if (rule.regex) rule.query = "exact";
1110
+ else if (typeof flags.flag_query === "string") rule.query = QUERY[flags.flag_query];
1111
+ if (typeof flags.flag_case === "boolean") rule.case_insensitive = flags.flag_case;
1112
+ if (typeof flags.flag_trailing === "boolean") rule.trailing_slash = flags.flag_trailing ? "ignore" : "exact";
1113
+ const matchType = item.match_type ?? "url";
1114
+ const actionType = item.action_type ?? "url";
1115
+ if (!on(item) || group && !on(group)) excluded.push({
1116
+ ...rule,
1117
+ reason: "disabled"
1118
+ });
1119
+ else if (actionType === "error" && GONE.has(code) && matchType === "url") redirects.push({
1120
+ ...rule,
1121
+ to: "",
1122
+ status: code
1123
+ });
1124
+ else if (actionType !== "url") excluded.push({
1125
+ ...rule,
1126
+ status: code,
1127
+ reason: `not-a-redirect:${actionType}`
1128
+ });
1129
+ else if (matchType !== "url") excluded.push({
1130
+ ...rule,
1131
+ match: matchType,
1132
+ status: code,
1133
+ ...matchType === "login" ? { condition: data ?? null } : {},
1134
+ reason: `conditional-match:${matchType}`
1135
+ });
1136
+ else {
1137
+ const move = code >= 300 && code < 400;
1138
+ redirects.push({
1139
+ ...rule,
1140
+ to: relative(target, origin),
1141
+ status: move ? code : 301,
1142
+ ...move ? {} : { status_note: `source status ${code} is not a redirect code; 301 assumed` }
1143
+ });
1144
+ }
1145
+ }
1146
+ return {
1147
+ redirects: redirects.toSorted(order),
1148
+ excluded: excluded.toSorted(order)
1149
+ };
1150
+ }
1151
+ //#endregion
1036
1152
  //#region src/rest-menus.ts
1037
1153
  const targetOf = (kind, object, id, url, ctx) => {
1038
1154
  const objectId = id && id > 0 ? id : null;
@@ -1071,6 +1187,12 @@ const targetOf = (kind, object, id, url, ctx) => {
1071
1187
  resolved: false
1072
1188
  };
1073
1189
  };
1190
+ /** The public address of the post or term a target names, when it is one this import proved public. */
1191
+ const publicLink = (target, ctx) => {
1192
+ if (!target.resolved || !("id" in target) || !target.id) return void 0;
1193
+ if (target.kind === "post") return ctx.postLink?.(target.id);
1194
+ if (target.kind === "term") return ctx.termLink?.(target.taxonomy, target.id);
1195
+ };
1074
1196
  /**
1075
1197
  * Items that are left out: a draft item, or one whose post target is not proven visible. Their children move up
1076
1198
  * to the nearest kept ancestor, as WordPress's walker shows them.
@@ -1091,15 +1213,17 @@ function hiddenItems(items, drop) {
1091
1213
  lift
1092
1214
  };
1093
1215
  }
1216
+ /** A host without case or a leading `www.`: example.com and www.example.com are one site (as the Bridge reads it). */
1217
+ const bareHost = (u) => u.hostname.toLowerCase().replace(/^www\./, "");
1094
1218
  /**
1095
- * The post a plain permalink names (`/?page_id=14`, `/?p=14`) on this site: a custom link typed by hand carries no
1096
- * post id, but it points at a post all the same, and its label is often that post's title.
1219
+ * The post a plain permalink names (`/?page_id=14`, `/?p=14`) on this site, `www.` or not: a custom link typed by
1220
+ * hand carries no post id, but it points at a post all the same, and its label is often that post's title.
1097
1221
  */
1098
1222
  function plainPostId(url, origin) {
1099
1223
  if (!url || !url.includes("?")) return void 0;
1100
1224
  try {
1101
1225
  const u = new URL(url, `${origin}/`);
1102
- if (u.origin !== new URL(origin).origin) return void 0;
1226
+ if (bareHost(u) !== bareHost(new URL(origin))) return void 0;
1103
1227
  const id = Number(u.searchParams.get("page_id") ?? u.searchParams.get("p"));
1104
1228
  return Number.isSafeInteger(id) && id > 0 ? id : void 0;
1105
1229
  } catch {
@@ -1126,13 +1250,15 @@ function classicMenus(menus, items, ctx, dropped = { count: 0 }) {
1126
1250
  slug: m.slug ?? "",
1127
1251
  name: strip(m.name ?? m.slug ?? ""),
1128
1252
  items: own.map((i) => {
1253
+ const target = targetOf(i.type, i.object, i.object_id, i.url, ctx);
1254
+ const typed = target.kind === "url" ? plainPostId(i.url, ctx.origin) : void 0;
1129
1255
  const item = {
1130
1256
  id: i.id,
1131
1257
  title: i.title?.raw || i.title?.rendered || "",
1132
1258
  order: i.menu_order ?? 0,
1133
1259
  parent: lift(i.parent || null),
1134
- url: i.url || null,
1135
- target: targetOf(i.type, i.object, i.object_id, i.url, ctx),
1260
+ url: publicLink(target, ctx) ?? (typed !== void 0 ? ctx.postLink?.(typed) : void 0) ?? (i.url || null),
1261
+ target,
1136
1262
  target_attr: i.target || null,
1137
1263
  classes: (i.classes ?? []).filter(Boolean),
1138
1264
  description: i.description ?? "",
@@ -1210,13 +1336,14 @@ function blockItems(tree, ctx, nextId, dropped) {
1210
1336
  const push = (b, parent) => {
1211
1337
  const id = nextId();
1212
1338
  const url = absolute(b.url, ctx.origin);
1339
+ const target = targetOf(b.kind, b.object, b.id, url, ctx);
1213
1340
  items.push({
1214
1341
  id,
1215
1342
  title: b.title,
1216
1343
  order: items.length + 1,
1217
1344
  parent,
1218
- url: url ?? null,
1219
- target: targetOf(b.kind, b.object, b.id, url, ctx),
1345
+ url: publicLink(target, ctx) ?? (b.typed !== void 0 ? ctx.postLink?.(b.typed) : void 0) ?? url ?? null,
1346
+ target,
1220
1347
  target_attr: b.newTab ? "_blank" : null,
1221
1348
  classes: (b.className ?? "").split(/\s+/).filter(Boolean),
1222
1349
  description: b.description ?? "",
@@ -1255,6 +1382,7 @@ function blockItems(tree, ctx, nextId, dropped) {
1255
1382
  kind,
1256
1383
  object,
1257
1384
  id: typeof a.id === "number" ? a.id : void 0,
1385
+ typed: kind === "custom" ? postId : void 0,
1258
1386
  newTab: a.opensInNewTab === true,
1259
1387
  className: str(a.className),
1260
1388
  description: str(a.description)
@@ -1625,19 +1753,24 @@ async function fetchRestRawIR(options) {
1625
1753
  const extraTerms = lists.slice(0, extraTaxonomies.length);
1626
1754
  const postLists = lists.slice(extraTaxonomies.length);
1627
1755
  const termsById = /* @__PURE__ */ new Map();
1756
+ /** A term's public address, for menu items that name it; RawTerm carries none. */
1757
+ const termLinks = /* @__PURE__ */ new Map();
1628
1758
  for (const [tax, list] of [
1629
1759
  ["category", categories],
1630
1760
  ["post_tag", tags],
1631
1761
  ...extraTaxonomies.map((t, i) => [t.taxonomy, extraTerms[i] ?? []])
1632
- ]) for (const t of list) termsById.set(t.id, {
1633
- id: t.id,
1634
- taxonomy: tax,
1635
- slug: t.slug,
1636
- name: strip(t.name),
1637
- parent: t.parent ? list.find((x) => x.id === t.parent)?.slug ?? null : null,
1638
- parent_resolved: t.parent ? list.some((x) => x.id === t.parent) : null,
1639
- description: t.description ?? ""
1640
- });
1762
+ ]) for (const t of list) {
1763
+ if (t.link) termLinks.set(t.id, t.link);
1764
+ termsById.set(t.id, {
1765
+ id: t.id,
1766
+ taxonomy: tax,
1767
+ slug: t.slug,
1768
+ name: strip(t.name),
1769
+ parent: t.parent ? list.find((x) => x.id === t.parent)?.slug ?? null : null,
1770
+ parent_resolved: t.parent ? list.some((x) => x.id === t.parent) : null,
1771
+ description: t.description ?? ""
1772
+ });
1773
+ }
1641
1774
  const userById = new Map(users.map((u) => [u.id, u]));
1642
1775
  const posts = [];
1643
1776
  const languagePairs = [];
@@ -1725,6 +1858,7 @@ async function fetchRestRawIR(options) {
1725
1858
  mime: m.mime_type ?? null,
1726
1859
  parent: m.post ?? null,
1727
1860
  parent_resolved: m.post ? posts.some((p) => p.id === m.post) : null,
1861
+ link: !m.post || posts.some((p) => p.id === m.post && visible(p)) ? m.link || null : null,
1728
1862
  date: iso(m.date_gmt)
1729
1863
  }));
1730
1864
  const gaps = [];
@@ -1789,6 +1923,7 @@ async function fetchRestRawIR(options) {
1789
1923
  ["navigation", navs]
1790
1924
  ]) if (!l.status || l.status >= 400 && !DENIED.has(l.status) && l.status !== 404) warnings.push(`${name}: HTTP ${l.status} — skipped`);
1791
1925
  const slugOf = new Map(posts.filter(visible).map((p) => [p.id, p.slug]));
1926
+ const linkOf = new Map(posts.filter(visible).flatMap((p) => p.link ? [[p.id, p.link]] : []));
1792
1927
  const ctx = {
1793
1928
  origin,
1794
1929
  postSlug: (id) => slugOf.get(id),
@@ -1796,6 +1931,8 @@ async function fetchRestRawIR(options) {
1796
1931
  const t = termsById.get(id);
1797
1932
  return t && t.taxonomy === taxonomy ? t.slug : void 0;
1798
1933
  },
1934
+ postLink: (id) => linkOf.get(id),
1935
+ termLink: (taxonomy, id) => termsById.get(id)?.taxonomy === taxonomy ? termLinks.get(id) : void 0,
1799
1936
  pages: posts.filter((p) => p.type === "page" && p.status === "publish" && !p.password).map((p) => ({
1800
1937
  id: p.id,
1801
1938
  parent: p.parent ?? null,
@@ -1829,6 +1966,62 @@ async function fetchRestRawIR(options) {
1829
1966
  user_id: c.author || null
1830
1967
  }));
1831
1968
  for (const c of comments) if (!postIds.has(c.post)) warnings.push(`comment ${c.id}: post ${c.post} not in fetched set`);
1969
+ let redirects = [];
1970
+ let redirectsExcluded = [];
1971
+ gaps.push("redirects_partial");
1972
+ const namespaces = Array.isArray(about?.namespaces) ? about.namespaces : void 0;
1973
+ const redirection = namespaces ? namespaces.includes("redirection/v1") : void 0;
1974
+ if (redirection && !authed) gaps.push("redirects_require_auth");
1975
+ else if (authed && redirection !== false) {
1976
+ const all = async (path) => {
1977
+ const items = [];
1978
+ const seen = /* @__PURE__ */ new Set();
1979
+ for (let page = 0; page < (maxPages ?? 500); page++) {
1980
+ const r = await schedule(() => doFetch(`${origin}/wp-json/redirection/v1/${path}?per_page=200&page=${page}`, { headers }));
1981
+ if (!r.ok) {
1982
+ await r.body?.cancel();
1983
+ return {
1984
+ items,
1985
+ status: r.status
1986
+ };
1987
+ }
1988
+ const body = await r.json();
1989
+ const fresh = (Array.isArray(body.items) ? body.items : []).filter((x) => !seen.has(x.id));
1990
+ for (const x of fresh) {
1991
+ seen.add(x.id);
1992
+ items.push(x);
1993
+ }
1994
+ if (!fresh.length || items.length >= Number(body.total ?? 0)) break;
1995
+ }
1996
+ return {
1997
+ items,
1998
+ status: 200
1999
+ };
2000
+ };
2001
+ const settings = async () => {
2002
+ try {
2003
+ const r = await schedule(() => doFetch(`${origin}/wp-json/redirection/v1/setting`, { headers }));
2004
+ if (!r.ok) {
2005
+ await r.body?.cancel();
2006
+ return;
2007
+ }
2008
+ const body = await r.json();
2009
+ return body?.settings && typeof body.settings === "object" ? body.settings : void 0;
2010
+ } catch {
2011
+ return;
2012
+ }
2013
+ };
2014
+ const [rules, groups, siteFlags] = await Promise.all([
2015
+ all("redirect"),
2016
+ all("group"),
2017
+ settings()
2018
+ ]);
2019
+ if (DENIED.has(rules.status)) {
2020
+ gaps.push("redirects_require_auth");
2021
+ warnings.push(`redirects: HTTP ${rules.status} with the credential — the user may not manage Redirection; its rules not read`);
2022
+ } else if (rules.status === 200) ({redirects, excluded: redirectsExcluded} = redirectionRules(rules.items, groups.status === 200 ? groups.items : [], origin, siteFlags));
2023
+ else if (redirection) warnings.push(`redirects: HTTP ${rules.status} — Redirection's rules not read`);
2024
+ }
1832
2025
  return {
1833
2026
  raw: {
1834
2027
  version: MIGRATION_CONTRACT_VERSION,
@@ -1856,6 +2049,8 @@ async function fetchRestRawIR(options) {
1856
2049
  attachments,
1857
2050
  comments,
1858
2051
  ...menus.length ? { menus } : {},
2052
+ ...redirects.length ? { redirects } : {},
2053
+ ...redirectsExcluded.length ? { redirects_excluded: redirectsExcluded } : {},
1859
2054
  ...languagePairs.length ? { language_pairs: languagePairs } : {}
1860
2055
  },
1861
2056
  warnings,