WEBVTT

00:00:00.000 --> 00:00:02.960
An imported page showed the letter E where an

00:00:03.040 --> 00:00:04.400
icon should have been.

00:00:04.720 --> 00:00:07.800
Pages I hadn't touched were restyling themselves.

00:00:08.080 --> 00:00:10.800
Nothing errored. The pages were just wrong.

00:00:11.067 --> 00:00:13.866
This was the first WordPress importer I built for

00:00:13.947 --> 00:00:17.386
a multi-site content management system, and it took the

00:00:17.547 --> 00:00:20.226
route that looks correct on a whiteboard.

00:00:20.267 --> 00:00:22.626
Extract each page's content.

00:00:22.667 --> 00:00:24.719
Rebuild a clean shell.

00:00:24.854 --> 00:00:27.506
Pool all the CSS site-wide.

00:00:27.547 --> 00:00:30.506
It produced silently broken pages.

00:00:30.700 --> 00:00:33.340
To find the restyling I pulled up two source

00:00:33.420 --> 00:00:36.820
pages and diffed the rules for the same selector.

00:00:36.860 --> 00:00:40.940
They disagreed. The same class name carried completely different

00:00:41.020 --> 00:00:43.660
declarations depending on the page.

00:00:43.780 --> 00:00:46.220
That one diff was the diagnosis.

00:00:46.434 --> 00:00:48.833
Then I measured the blast radius.

00:00:48.914 --> 00:00:54.353
71 colliding selectors across 34 per-page stylesheets.

00:00:54.434 --> 00:00:56.993
Every one a place where two pages had used

00:00:57.074 --> 00:01:00.273
the same class name to mean something different.

00:01:07.467 --> 00:01:09.826
These class names are positional.

00:01:10.107 --> 00:01:13.826
.section_0 just means the first section on this page.

00:01:14.107 --> 00:01:17.706
.column_4 is an index into that page's own layout

00:01:17.827 --> 00:01:18.906
and nothing more.

00:01:19.227 --> 00:01:21.786
The strings are stable, but the first section of

00:01:21.867 --> 00:01:24.586
one page and the first section of another share

00:01:24.827 --> 00:01:26.026
nothing beyond the name.

00:01:26.300 --> 00:01:29.820
Pooling those stylesheets into one bundle makes the collision

00:01:29.900 --> 00:01:34.140
mechanical. Both rules carry the same specificity, so the

00:01:34.460 --> 00:01:36.980
cascade falls back to source order.

00:01:37.020 --> 00:01:41.740
Whichever page's stylesheet loaded last wins, globally, and silently

00:01:41.940 --> 00:01:44.220
restyles every other page.

00:01:44.434 --> 00:01:46.513
This isn't a builder bug.

00:01:46.594 --> 00:01:50.673
The builder's CSS assumes exactly one page's stylesheet is

00:01:50.874 --> 00:01:53.473
present at a time, and for that contract it

00:01:53.554 --> 00:01:56.860
is correct. It isn't a parse bug either.

00:01:56.887 --> 00:01:58.886
The extraction was fine.

00:01:58.941 --> 00:02:01.433
The defect lives in the merge.

00:02:01.474 --> 00:02:05.473
Any tool that pools per-page builder stylesheets is structurally

00:02:05.874 --> 00:02:08.473
broken, however clean its extraction.

00:02:08.514 --> 00:02:11.793
So the fix could not be a cleverer merge.

00:02:12.000 --> 00:02:15.520
The letter E was a different failure entirely, one

00:02:15.680 --> 00:02:17.640
layer down at the assets.

00:02:17.680 --> 00:02:20.880
The theme's icons are characters shaped by an icon

00:02:20.960 --> 00:02:24.640
font. After capture the font's files were missing from

00:02:24.720 --> 00:02:28.240
disk, and the browser's cross-origin fallback to the original

00:02:28.320 --> 00:02:30.080
host was blocked.

00:02:30.200 --> 00:02:32.880
No font on the page, so the glyph degraded

00:02:32.960 --> 00:02:34.520
to a bare letter.

00:02:34.560 --> 00:02:37.320
Two failures, no shared mechanism.

00:02:37.360 --> 00:02:40.320
Just the same assumption underneath, that the thing worth

00:02:40.400 --> 00:02:42.320
capturing was the site.

00:02:49.534 --> 00:02:52.493
So I threw the reconstruction away and built an

00:02:52.734 --> 00:02:56.853
archive-fidelity import. Each page is stored verbatim.

00:02:57.054 --> 00:02:59.613
Its own head, its own body, its own body

00:02:59.774 --> 00:03:03.293
classes, in document order, as the snapshot it was

00:03:03.374 --> 00:03:04.373
on the source.

00:03:04.654 --> 00:03:07.453
No shell, no template, no pooling.

00:03:07.694 --> 00:03:10.413
The page that loads is the page that existed.

00:03:10.700 --> 00:03:13.527
The assets travel with the page.

00:03:13.553 --> 00:03:18.140
The importer walks each page's CSS, scripts, images and

00:03:18.500 --> 00:03:23.180
fonts, following URL and import references recursively, and stores

00:03:23.260 --> 00:03:26.220
every asset by the hash of its bytes, served

00:03:26.300 --> 00:03:29.020
from a path that is the hash.

00:03:29.140 --> 00:03:31.300
That bought two things at once.

00:03:31.340 --> 00:03:35.580
Far-future immutable caching, because a URL's contents can never

00:03:36.020 --> 00:03:39.580
change, and no runtime dependency on the source host

00:03:39.660 --> 00:03:41.380
for stored assets.

00:03:41.420 --> 00:03:43.820
The icon font is now served from the site's

00:03:43.900 --> 00:03:48.140
own origin, so the cross-origin block disappears with it.

00:03:48.334 --> 00:03:51.613
At this point the pages were faithful and completely

00:03:52.134 --> 00:03:55.533
dead. A verbatim archive cannot show a contact form

00:03:55.614 --> 00:03:58.973
that submits, and it cannot show articles written after

00:03:59.054 --> 00:04:02.413
the import. The temptation is to conclude that the

00:04:02.494 --> 00:04:05.613
pages need to be dynamic after all, and rebuild

00:04:05.694 --> 00:04:08.813
them properly. That is the trap I had just

00:04:08.894 --> 00:04:10.306
climbed out of.

00:04:10.361 --> 00:04:13.186
The pages did not need to be dynamic.

00:04:13.401 --> 00:04:15.053
Specific regions of them did.

00:04:20.600 --> 00:04:23.720
Live behaviour comes back as a chain of render-time

00:04:23.800 --> 00:04:26.840
transforms applied to the archived HTML on the way

00:04:26.920 --> 00:04:29.720
out, never as edits to the snapshot.

00:04:30.360 --> 00:04:33.320
Each transform is a no-op unless the page contains

00:04:33.400 --> 00:04:37.000
its target markup, so every page without that markup

00:04:37.080 --> 00:04:39.720
passes through the whole chain untouched.

00:04:40.040 --> 00:04:44.280
And the reducer rescues per transform, passing the pre-transform

00:04:44.400 --> 00:04:48.520
HTML forward, so when one transform fails the reader

00:04:48.640 --> 00:04:51.587
gets the frozen archive for that region, never a

00:04:51.640 --> 00:04:52.680
broken page.

00:04:52.967 --> 00:04:55.766
The scraped contact form is swapped for a native

00:04:55.847 --> 00:05:00.126
one, rendered into the archived markup's own class structure,

00:05:00.167 --> 00:05:04.206
so the page's captured CSS styles it identically.

00:05:04.247 --> 00:05:08.766
The blog index reuses every imported entry's captured markup,

00:05:08.807 --> 00:05:11.526
so each one looks exactly as it did, and

00:05:11.607 --> 00:05:14.566
clones an entry only for records created after the

00:05:14.647 --> 00:05:19.206
import. And a brand-new article renders inside an archived

00:05:19.287 --> 00:05:22.686
post shell by targeted node substitution.

00:05:22.727 --> 00:05:25.206
The theme dresses it exactly as it dressed the

00:05:25.327 --> 00:05:26.406
old ones.

00:05:26.600 --> 00:05:28.760
I didn't want to trust my own eyes on

00:05:28.920 --> 00:05:30.813
whether any of this matched.

00:05:30.867 --> 00:05:35.080
A fleet of headless-browser agents captured full-page screenshots of

00:05:35.160 --> 00:05:37.720
every page on both the live source and the

00:05:37.800 --> 00:05:41.320
import, and judged them section by section.

00:05:41.400 --> 00:05:44.440
That pass caught one stale capture, and on the

00:05:44.520 --> 00:05:48.760
first full run confirmed 15 of 16 pages visually

00:05:48.840 --> 00:05:50.040
identical.

00:05:50.234 --> 00:05:52.793
The reconstruction was never lazy.

00:05:52.834 --> 00:05:55.553
It was the more sophisticated design.

00:05:55.594 --> 00:05:59.713
Extract, template, pool the assets site-wide.

00:05:59.754 --> 00:06:02.553
And every instinct that made it attractive was the

00:06:02.634 --> 00:06:04.873
same instinct that broke it.

00:06:04.954 --> 00:06:07.673
Page-builder output is a set of documents that each

00:06:07.754 --> 00:06:10.473
assume they are the only one present, and you

00:06:10.554 --> 00:06:13.713
cannot pool what was authored to stand alone.

00:06:13.754 --> 00:06:17.273
Fidelity comes from preserving, not reconstructing.

