cornhsu-polymigrate 2.2.1 → 2.3.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/README.md CHANGED
@@ -13,7 +13,7 @@ PolyMigrate turns legacy dynamic sites (old PHP sites and the like) into clean,
13
13
  Markdown — and it treats multilingual content as a first-class concern, not an afterthought.
14
14
  Config-driven, fully offline-rerunnable, built on .NET.
15
15
 
16
- **Status: 2.2.** The extraction pipeline, pairing, verification, thumbnails and orphan-page
16
+ **Status: 2.3.** The extraction pipeline, pairing, verification, thumbnails and orphan-page
17
17
  recovery are complete and validated against a real full-site migration (see below). The **CLI
18
18
  surface and the Phase output contracts remain stable** (unchanged since 1.0): new features bump
19
19
  the minor version, fixes bump the patch. 2.0 was an engineering release — it narrowed the
@@ -21,8 +21,9 @@ the minor version, fixes bump the patch. 2.0 was an engineering release — it n
21
21
  config fields; 2.1 fixed up the release pipeline (arm64 npm packages, symbol packages).
22
22
  2.2 closed a blind spot in `verify` — pages nothing links to were reported as fine, because a
23
23
  link-following audit never reaches them — and added `slugs` for keeping up with a source site
24
- that is still being updated. **Upgrading to 2.2 can turn a passing `verify` into exit 1**;
25
- see the [CHANGELOG](CHANGELOG.md) for that and the other migration notes.
24
+ that is still being updated. **Upgrading to 2.2 can turn a passing `verify` into exit 1**.
25
+ 2.3 reads the source site's own `hreflang` declarations (see below) and reports them in a new
26
+ `hreflang_map.csv`. See the [CHANGELOG](CHANGELOG.md) for the migration notes.
26
27
 
27
28
  ## Why
28
29
 
@@ -33,14 +34,48 @@ the pairing to you. PolyMigrate:
33
34
 
34
35
  - **pairs automatically** where filenames are symmetric (`/ch/news/x` ↔ `/en/news/x` share a
35
36
  `translation_key`),
36
- - **suggests pairs heuristically** where they are not — shared photo albums, normalized dates
37
- hidden in slugs (`20240121` vs `01212024`), title similarity,
37
+ - **reads the site's own `hreflang` declarations** — `<link rel="alternate" hreflang>` is the one
38
+ pairing signal the site's authors *stated* rather than something a tool inferred, so it is
39
+ tried first and can pair across sections (`/ch/news/` ↔ `/en/press/`),
40
+ - **suggests pairs heuristically** where neither applies — slugs that differ only in separators
41
+ (`2025-light-offering` vs `2025_light_offering`), shared photo albums, normalized dates hidden
42
+ in slugs (`20240121` vs `01212024`), title similarity,
38
43
  - **honestly reports what it cannot pair**, producing a review-ready gap inventory instead of
39
44
  guessing wrong.
40
45
 
46
+ Note what PolyMigrate deliberately does *not* do: match language versions by URL string
47
+ similarity. `/products/`, `/produkte/` and `/產品/` have a similarity of zero and are the same
48
+ page; symmetric-path pairing is an exact match after the language prefix is stripped, never a
49
+ fuzzy one.
50
+
41
51
  Languages are not limited to two: declare any number in `lang_map` and every output
42
52
  (frontmatter, inventories, pairing) expands accordingly. All locale output is standard BCP-47.
43
53
 
54
+ ### About `hreflang`
55
+
56
+ Legacy sites are exactly the sites whose `hreflang` is missing or wrong — usually bolted on by
57
+ an SEO contractor years later. So PolyMigrate treats it as strong evidence, not as gospel.
58
+ A declaration is only used for pairing if it:
59
+
60
+ - isn't `x-default` (that's "where to go when nothing matches", not a language version),
61
+ - doesn't point at the page itself,
62
+ - resolves to a page actually in your mirror — **same host included**, since plenty of sites put
63
+ the English version on `en.example.org`, where a path-only comparison would make a page its
64
+ own translation,
65
+ - and **isn't one of many same-language pages claiming the same target**. This is the classic
66
+ failure: one `<link>` pasted into the site-wide template, so every Chinese page declares the
67
+ English homepage as its English version. One page cannot be the translation of three others,
68
+ so all of those claims are dropped rather than turned into a confident-looking wrong pair.
69
+
70
+ Everything declared — including the rejected ones — is written to `hreflang_map.csv` with
71
+ `in_mirror` / `reciprocal` / `usable` and a `reject_reason` naming which rule it failed, because
72
+ "can I trust this site's hreflang?" is a question you want answered with data, not a total.
73
+
74
+ Pairs are still only ever *suggested*: `hreflang` raises the evidence quality, it does not
75
+ switch the tool into merging pages on its own. That is a deliberate call, not an unfinished
76
+ one — the measurement behind it, and what would have to change to revisit it, are recorded in
77
+ [docs/hreflang_量測與決策.md](docs/hreflang_量測與決策.md).
78
+
44
79
  ## Battle-tested defaults
45
80
 
46
81
  The extraction pipeline bakes in fixes for real problems found during a real migration —
@@ -55,6 +90,7 @@ things generic tools and LLM extractors silently get wrong:
55
90
  | `<title>` polluted with dates and site name | body-first title extraction + configurable cleanup |
56
91
  | Mixed date formats (`YYYYMMDD` / `MMDDYYYY` / `DDMMYYYY`) | all recognized and normalized |
57
92
  | Old articles removed from indexes but still served | orphan probing (per-day URL candidates + suffix variants) |
93
+ | `hreflang` pointing at dead URLs, other hosts, or itself | validated against the mirror; unusable ones recorded, not applied |
58
94
  | Broken images on the source site | detected, recorded, never blocks the run |
59
95
  | Bot protection (JS cookie challenge → 409) | declarative cookie workaround in config |
60
96
  | Legacy encodings (Big5, GB2312, …) | declared or defaulted per site |
@@ -91,7 +127,7 @@ dotnet tool install -g Cornhsu.PolyMigrate # .NET
91
127
  ```
92
128
 
93
129
  > The npm build ships a self-contained native binary and only downloads the one
94
- > matching your platform (win32-x64 / linux-x64 / darwin-x64 / darwin-arm64).
130
+ > matching your platform (win32 / linux / darwin, each in x64 and arm64).
95
131
  > Migrating a site is usually a one-off job — installing a whole SDK for a single
96
132
  > run is friction most people won't accept, so the tool meets you where you are.
97
133
 
@@ -128,7 +164,7 @@ url_pattern:
128
164
  extract:
129
165
  content: "section[id]:not(#header):not(#footer)"
130
166
  pairing:
131
- fallback: [shared_media, date, title_similarity]
167
+ fallback: [hreflang, slug_normalized, shared_media, date, title_similarity]
132
168
  ```
133
169
 
134
170
  ## Use as a library
@@ -159,6 +195,7 @@ Console.WriteLine($"{verify.Errors} errors, {verify.Warnings} warnings");
159
195
  | `src/PolyMigrate.Cli` | the `polymigrate` CLI (NuGet tool package: `Cornhsu.PolyMigrate`) |
160
196
  | `tests/` | unit/integration tests + an offline fixture site with golden-file baselines |
161
197
  | `docs/contracts.md` | file-format contracts between pipeline phases |
198
+ | `docs/hreflang_量測與決策.md` | why `hreflang` only ever *suggests* — the measurement behind that call |
162
199
  | `docs/搬遷工具_評估與規劃書.md` | the original design/planning doc (the `§X.Y` references throughout the source point here) |
163
200
 
164
201
  ## Development
@@ -169,6 +206,14 @@ dotnet test
169
206
  dotnet run --project src/PolyMigrate.Cli -- --help
170
207
  ```
171
208
 
209
+ ## See also
210
+
211
+ [**多語言網站搬遷最容易搬丟的東西**](https://cornhsu.com/articles/multilingual-site-migration-pairing)
212
+ (in Traditional Chinese) — background on the category rather than this tool: the five pairing
213
+ signals in priority order, why "just read `hreflang`" often misses on real legacy sites
214
+ (measured on one: 528 pages, **0** declarations, sitemap 404), the five gates a declaration has to
215
+ pass before it is usable at all, and why URL string similarity is the wrong tool for this job.
216
+
172
217
  License: [MIT](LICENSE). All dependencies are MIT/BSD/Apache-2.0
173
218
  (imaging via **Magick.NET**; ImageSharp was dropped when its 4.x line began requiring a
174
219
  license key at build time). See [THIRD-PARTY-NOTICES](THIRD-PARTY-NOTICES.md).
@@ -19,9 +19,14 @@ const { spawnSync } = require("node:child_process");
19
19
  const path = require("node:path");
20
20
 
21
21
  // 平台 → 子套件名 / 執行檔名
22
+ // ⚠ 這份清單要與 npm/prepare.mjs 的 TARGETS 一致。
23
+ // 兩邊漂開的話症狀很隱蔽:平台包會被 npm 正確裝好,啟動時卻說「沒有預先建置版本」。
24
+ // (2.1.0 加了 win32-arm64 / linux-arm64,但漏改這裡,2.1.0~2.3.0 的 arm64 使用者都中。)
22
25
  const TARGETS = {
23
26
  "win32 x64": { pkg: "@cornhsu/polymigrate-win32-x64", bin: "polymigrate.exe" },
27
+ "win32 arm64": { pkg: "@cornhsu/polymigrate-win32-arm64", bin: "polymigrate.exe" },
24
28
  "linux x64": { pkg: "@cornhsu/polymigrate-linux-x64", bin: "polymigrate" },
29
+ "linux arm64": { pkg: "@cornhsu/polymigrate-linux-arm64", bin: "polymigrate" },
25
30
  "darwin x64": { pkg: "@cornhsu/polymigrate-darwin-x64", bin: "polymigrate" },
26
31
  "darwin arm64": { pkg: "@cornhsu/polymigrate-darwin-arm64", bin: "polymigrate" },
27
32
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cornhsu-polymigrate",
3
- "version": "2.2.1",
3
+ "version": "2.3.1",
4
4
  "description": "The i18n-first static-site migrator — the only one that pairs your multilingual pages automatically. Mirrors a legacy site into frontmatter Markdown, auto-matches language versions, and honestly lists what it could not pair.",
5
5
  "keywords": [
6
6
  "i18n",
@@ -36,11 +36,11 @@
36
36
  "node": ">=18"
37
37
  },
38
38
  "optionalDependencies": {
39
- "@cornhsu/polymigrate-win32-x64": "2.2.1",
40
- "@cornhsu/polymigrate-win32-arm64": "2.2.1",
41
- "@cornhsu/polymigrate-linux-x64": "2.2.1",
42
- "@cornhsu/polymigrate-linux-arm64": "2.2.1",
43
- "@cornhsu/polymigrate-darwin-x64": "2.2.1",
44
- "@cornhsu/polymigrate-darwin-arm64": "2.2.1"
39
+ "@cornhsu/polymigrate-win32-x64": "2.3.1",
40
+ "@cornhsu/polymigrate-win32-arm64": "2.3.1",
41
+ "@cornhsu/polymigrate-linux-x64": "2.3.1",
42
+ "@cornhsu/polymigrate-linux-arm64": "2.3.1",
43
+ "@cornhsu/polymigrate-darwin-x64": "2.3.1",
44
+ "@cornhsu/polymigrate-darwin-arm64": "2.3.1"
45
45
  }
46
46
  }