cornhsu-polymigrate 2.2.0 → 2.3.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.
Files changed (2) hide show
  1. package/README.md +49 -6
  2. package/package.json +7 -7
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 |
@@ -105,6 +141,12 @@ polymigrate fetch-orphans site.yaml --section news
105
141
  polymigrate slugs . --section news # slugs already mirrored under ./raw (pipeable)
106
142
  ```
107
143
 
144
+ > **`raw/` is the one thing you cannot regenerate.** Re-crawling a site takes hours at a
145
+ > polite request rate, and the source site may be switched off the moment you finish
146
+ > migrating — at which point your mirror is the only copy left. No PolyMigrate command
147
+ > ever deletes or overwrites `raw/` or `media/`; keep them until the migration is signed
148
+ > off, and back them up if the source is going away.
149
+
108
150
  > No .NET? Every command above also runs with **zero install** via npm — just prefix it:
109
151
  > `npx cornhsu-polymigrate extract site.yaml`, `npx cornhsu-polymigrate verify out/`, and so on.
110
152
 
@@ -122,7 +164,7 @@ url_pattern:
122
164
  extract:
123
165
  content: "section[id]:not(#header):not(#footer)"
124
166
  pairing:
125
- fallback: [shared_media, date, title_similarity]
167
+ fallback: [hreflang, slug_normalized, shared_media, date, title_similarity]
126
168
  ```
127
169
 
128
170
  ## Use as a library
@@ -153,6 +195,7 @@ Console.WriteLine($"{verify.Errors} errors, {verify.Warnings} warnings");
153
195
  | `src/PolyMigrate.Cli` | the `polymigrate` CLI (NuGet tool package: `Cornhsu.PolyMigrate`) |
154
196
  | `tests/` | unit/integration tests + an offline fixture site with golden-file baselines |
155
197
  | `docs/contracts.md` | file-format contracts between pipeline phases |
198
+ | `docs/hreflang_量測與決策.md` | why `hreflang` only ever *suggests* — the measurement behind that call |
156
199
  | `docs/搬遷工具_評估與規劃書.md` | the original design/planning doc (the `§X.Y` references throughout the source point here) |
157
200
 
158
201
  ## Development
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cornhsu-polymigrate",
3
- "version": "2.2.0",
3
+ "version": "2.3.0",
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.0",
40
- "@cornhsu/polymigrate-win32-arm64": "2.2.0",
41
- "@cornhsu/polymigrate-linux-x64": "2.2.0",
42
- "@cornhsu/polymigrate-linux-arm64": "2.2.0",
43
- "@cornhsu/polymigrate-darwin-x64": "2.2.0",
44
- "@cornhsu/polymigrate-darwin-arm64": "2.2.0"
39
+ "@cornhsu/polymigrate-win32-x64": "2.3.0",
40
+ "@cornhsu/polymigrate-win32-arm64": "2.3.0",
41
+ "@cornhsu/polymigrate-linux-x64": "2.3.0",
42
+ "@cornhsu/polymigrate-linux-arm64": "2.3.0",
43
+ "@cornhsu/polymigrate-darwin-x64": "2.3.0",
44
+ "@cornhsu/polymigrate-darwin-arm64": "2.3.0"
45
45
  }
46
46
  }