Skip to content

Restore gallery captions and items the conversion flattened - #55

Merged
ptr727 merged 5 commits into
developfrom
restore-gallery-captions-and-items
Aug 8, 2026
Merged

Restore gallery captions and items the conversion flattened#55
ptr727 merged 5 commits into
developfrom
restore-gallery-captions-and-items

Conversation

@ptr727

Copy link
Copy Markdown
Owner

Closes#52.

What was wrong

A gallery is a flex row whose column widths come from .gallery-cols-N figure. Anything inside it that is not a figure gets no width from that rule and is laid out as one more item in the row. Three variants of that survived the WordPress conversion, and no gate could see any of them: every media check reasons about a URL, and content misplaced inside a gallery resolves and is linked exactly as correct content is.

Shape in the markdownCountWhat the old platform had
Caption text trailing the last figure shortcode's }}11<figcaption class="blocks-gallery-caption"> — a caption for the set
A bare ![](…) image2<li class="blocks-gallery-item"><figure>
A linked [![](…)](…) image8the same, with the <a>inside the figure

Why the capture was consulted rather than the markup

The first shape is why this is not a mechanical rewrite. Reading only the source, the obvious fix is to move the trailing text into that figure's caption parameter — which is what the review on #51 suggested, three times, and what #52 recorded as an open question.

The captured live site settles it: all eleven were blocks-gallery-caption elements, and the repo's eleven trailing texts match them exactly, one for one, with nothing on either side unaccounted for. They are captions for the set. Moving one into the last figure would have attributed a caption describing four images to whichever image sorted last, and it would have looked correct.

So the captions move to a caption on the gallery, and the container becomes a <figure>, because figcaption is only valid as a figure's child. The reset already sets figure { margin: 0 } and the theme already styles figure > figcaption, so a set caption needed exactly one rule of its own: a full-width flex basis, without which it packs onto the end of the last row and reads as a caption for whichever image it lands beside.

The other two shapes become figure shortcodes, the linked ones through the link parameter the theme already has, which puts the anchor back inside the figure where the original had it.

The gate

check-url-parity.py gains the check that would have caught all of this. It parses the built pages rather than matching text, because nested <figure> elements defeat a regex, and it fails on any direct child of a gallery that is not a figure or the gallery's own figcaption.

Demonstrated failing before being trusted. Against the pre-fix content it reports 27 stray nodes and exits 1. It distinguishes the shapes rather than counting them, naming bare text and each stray element type separately, so a future finding says what kind it is.

The gate found more than the review did. The linked-image variant is on moving-from-unraid-to-proxmox-ve, a post neither the Copilot review nor #52 mentions — it surfaced only by running the check across all 19 pages that carry a gallery. #52's own measurement of "12 across 2 pages" was low for the same reason: it looked for stray text, and eight of these are stray elements.

Verification

gallery: 19 pages with galleries, 0 stray nodes inside one
PASS - the built site honors the URL contract

Released through make-release.sh to the local mirror and checked against the running server: PASS - 1245 URLs honored, rules confirmed from the release just installed.

assets stays at 1049 and orphans stays at 98, both unmoved, which is the evidence that the rewrite lost no media and orphaned none. Build clean under hugo --gc --minify --panicOnWarning.

markdownlint-cli2 and editorconfig-checker clean on every file this touches. The MD012 warnings inside the edited post are pre-existing conversion artifacts, unchanged in count at 9 before and after, and content/** is excluded from the CI markdown glob by design.

🤖 Generated with Claude Code

Closes#52.
A gallery is a flex row whose column widths come from `.gallery-cols-N figure`,
so anything inside it that is not a figure gets no width from that rule and is
laid out as one more item in the row. Three variants of that survived the
conversion, and no gate could see any of them: every media check reasons about a
URL, and misplaced content resolves and is linked exactly as correct content is.
Each was verified against the captured live site before being changed rather than
inferred from the markup, and the first is why that matters. Eleven captions in
the FLAIR post were written as text trailing the last figure shortcode. Reading
only the source, the obvious fix is to move the text into that figure's caption
parameter, which is what the review suggested. The capture shows all eleven were
blocks-gallery-caption elements, captions for the set, so that fix would have
attributed a caption describing four images to whichever one sorted last and
would have looked right. They move to a caption on the gallery instead.
The other two were gallery items the conversion emitted as plain markdown, two
bare images and eight linked ones, against li.blocks-gallery-item > figure in the
original. Both become figure shortcodes, the linked ones through the parameter
the theme already has, which puts the anchor back inside the figure where it was.
The container becomes a figure, because figcaption is only valid as a figure's
child. The reset already sets figure { margin: 0 } and the theme already styles
figure > figcaption, so a set caption needed one rule: a full-width flex basis,
without which it packs onto the end of the last row and reads as a caption for
whichever image it lands beside.
check-url-parity.py gains the gate that would have caught all of this, parsing
the built pages rather than matching text, since nested figures defeat a regex.
Demonstrated failing before being trusted: against the pre-fix content it reports
27 stray nodes and exits 1, and it distinguishes the shapes rather than counting
them, naming bare text and each stray element type.
The gate found more than the review did. The linked-image variant is on a post
neither the review nor the issue mentioned, and it was found only by running the
check across all 19 pages that carry a gallery.
Verified through make-release.sh and against the local mirror: PASS - 1245 URLs
honored, with assets at 1049 and orphans at 98, both unmoved, so no media was
lost or newly orphaned by the rewrite.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
CopilotAI lite review requested due to automatic review settings August 8, 2026 04:29

CopilotAI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Restores WordPress-conversion-flattened gallery structure (set-level captions and non-figure gallery items) and adds a gate to prevent future regressions by validating the built HTML structure of galleries.

Changes:

  • Update the gallery shortcode to render as a <figure> with an optional gallery-level <figcaption>.
  • Convert affected posts’ stray gallery children (trailing caption text, bare images, linked images) into proper gallery/figure shortcode structure.
  • Extend check-url-parity.py with an HTML-structure check that flags any non-figure/figcaption direct children inside a gallery, plus supporting docs and CSS.

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 2 comments.

Show a summary per file
FileDescription
layouts/shortcodes/gallery.htmlSwitch gallery wrapper to <figure> and add optional gallery-level caption rendering.
assets/css/extended/custom.cssEnsure gallery-level captions take full width in the flex layout.
checks/check-url-parity.pyAdd gallery structure validation by parsing built HTML and reporting stray nodes.
checks/README.mdDocument the new gallery structural check and the conversion artifact shapes it detects.
content/posts/2022/10/30/installing-flair-smart-vents-to-keep-room-temperatures-balanced.mdMove gallery captions from trailing text into gallery shortcode caption.
content/posts/2020/06/21/moving-from-unraid-to-proxmox-ve.mdConvert linked markdown images inside a gallery into figure shortcodes with link=.
content/posts/2020/02/02/recovering-the-firmware-on-a-supermicro-bpn-sas3-846el1-backplane.mdConvert bare markdown images inside a gallery into figure shortcodes.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment threadlayouts/shortcodes/gallery.html Outdated
Comment threadchecks/check-url-parity.py Outdated
Both from review on #55.
The cheap reject read `class="gallery`, which assumes the quotes survive and that
the token sorts first. Minification drops the quotes around a value that does not
need one and says nothing about class order, so `class=gallery` and
`class="foo gallery"` both skipped the parser entirely and the gate passed
vacuously on a page whose markup was merely spelled differently. It matches the
bare word now, which cannot skip a page the parser would find, since the parser
requires the class token and a page carrying it always contains the string.
Matching a page that only mentions the word costs one parse.
The printed count is now taken from what the parser found rather than from the
reject, so the broader test does not turn "pages carrying a gallery" into "pages
the word appears on".
The shortcode comment claimed a set caption needed no rule of its own, while this
branch adds one. It does need it, and for a reason worth keeping: the gallery is
a flex container, so without a full-width basis the caption packs onto the end of
the last row and reads as a caption for whichever image it lands beside. The
comment now says so and says not to remove the rule as redundant.
Verified on all three spellings, quoted, unquoted, and reordered, each carrying a
known stray node: all three are now detected, and the first two were not before.
The site itself is unchanged at 19 pages with galleries and 0 stray nodes.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
CopilotAI review requested due to automatic review settings August 8, 2026 04:35

CopilotAI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 7 out of 7 changed files in this pull request and generated no new comments.

Suppressed comments (1)

checks/check-url-parity.py:200

  • The GalleryScan docstring says this logic catches “both conversion artifacts”, but checks/README.md (and the PR description) describe three distinct markdown shapes (trailing caption text, bare ![](...), and linked [![](...)](...)). Consider updating this docstring to match that 3-shape framing (or explicitly explain that the scanner sees two HTML-level shapes but they originate from three markdown patterns) so future readers aren’t confused about what the check is intended to detect.
 A gallery is a flex row of figures, so its column widths are set by `.gallery-cols-N figure`.
Anything else landing in there is not laid out by that rule and is rendered as one more item
in the row, which is how both conversion artifacts this catches presented: a set caption
written as trailing text after a figure shortcode became a bare text node, and a gallery item
the conversion emitted as a plain markdown image became a `<p>` wrapping two images.

From a suppressed finding on #55, which was right: the docstring said "both
conversion artifacts" while the README and the description say three. It was
written after two were known and never revised when the check itself found the
third.
Rewritten to say what it actually tests. Three source patterns reached it and
they arrive as three different HTML shapes, so an enumeration is a list to extend
every time the conversion surprises us again. The invariant is that a gallery
holds figures and its own caption, and that is what covers the shape nobody has
thought of yet, which is how the third was found after the first two.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
CopilotAI review requested due to automatic review settings August 8, 2026 04:40
@ptr727

Copy link
Copy Markdown
OwnerAuthor

Answering the suppressed finding from the round on c97e7a8, which carried no thread to reply on.

The GalleryScan docstring says this logic catches "both conversion artifacts", but checks/README.md (and the PR description) describe three distinct markdown shapes

Correct, and it was stale rather than a different framing: I wrote that docstring when two shapes were known, and never revised it when the check itself found the third on a post neither the review nor #52 had mentioned.

Fixed in 76144bd, taking the second of the two options you offered rather than the first. Enumerating source patterns makes the docstring a list to extend every time the conversion surprises us again, and the surprise already happened once inside this branch. It now names the invariant instead — a gallery holds figures and its own caption — and says explicitly that the three markdown patterns arrive here as three different HTML shapes. That is what covers the shape nobody has thought of yet, which is precisely how the third one was found.

CopilotAI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 7 out of 7 changed files in this pull request and generated no new comments.

Suppressed comments (1)

checks/check-url-parity.py:243

  • GalleryScan doesn’t implement HTMLParser.handle_startendtag, so XHTML-style void tags like <br/> or <img/> won’t be reported as stray direct children (and won’t participate in depth tracking). That can let a gallery-structure regression slip through if the HTML serializer emits self-closing void tags.
 def handle_starttag(self, tag, attrs):
classes = dict(attrs).get("class", "").split()
if self.depth is None:
if tag == "figure" and "gallery" in classes:
self.depth = 0

From a suppressed finding on #55, which reasoned correctly from the code and
reached the wrong conclusion about the base class. HTMLParser forwards a
self-closing tag to handle_starttag and then handle_endtag, so `<br/>`, `<br />`
and `<img/>` are already reported as stray children and already leave the depth
balanced. Verified on those three spellings and on a self-closing non-void
`<figure/>`, which enters and exits correctly and lets the `<p>` after it be seen.
Adding the override the finding asks for is what would break this, by counting a
pair the base class already splits. That is worth a comment rather than a reply
nobody reading the file later will find.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
CopilotAI review requested due to automatic review settings August 8, 2026 04:45
@ptr727

Copy link
Copy Markdown
OwnerAuthor

Answering the suppressed finding from the round on 76144bd, which carried no thread.

GalleryScan doesn't implement HTMLParser.handle_startendtag, so XHTML-style void tags like <br/> or <img/> won't be reported as stray direct children

Refuted, by running it rather than reading it.HTMLParser.handle_startendtag is not abstract — its own implementation forwards to handle_starttag and then handle_endtag, so the self-closing spellings already arrive at the code that reports them:

Input inside a galleryReported
<br><br> as a direct child
<br/><br> as a direct child
<br /><br> as a direct child
<img/><img> as a direct child
<figure/> then <p>after</p><p> as a direct child — the self-closing non-void case enters and exits correctly, so what follows it is still seen
a clean gallerynothing

Adding the override is in fact what would break it: handle_startendtag firing and the base class's split would count the pair twice, desynchronizing the depth for every gallery after it — the same failure the VOID set exists to prevent.

The reasoning was sound and only the base class's behaviour was wrong, which is worth more than a reply nobody reading the file later will find, so a7c8e70 records it as a comment at the class: no handle_startendtag override, deliberately, and here is what happens if you add one.

CopilotAI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 7 out of 7 changed files in this pull request and generated no new comments.

Suppressed comments (1)

checks/check-url-parity.py:304

  • The new gallery check returns a list of "stray nodes", but the shared failure summary prints FAIL gallery: N missing, which is misleading when this check fails (the items aren’t missing). Consider special-casing the summary noun for the gallery label so failures read correctly without changing other checks’ wording.
 ("media", check_media(public)),
("assets", check_assets(public, refs)),
("orphans", check_orphans(public, refs)),
("gallery", check_galleries(public)),
):

From a suppressed finding on #55. The shared summary printed
`FAIL gallery: N missing` for a check whose findings are nodes that are present,
which is the entire complaint about them.
Each label can now name its own noun, defaulting to "missing" so the four older
checks are unchanged and a check added later reads the way they do unless it says
otherwise. Verified by forcing a failure: `FAIL gallery: 15 stray nodes`.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
CopilotAI review requested due to automatic review settings August 8, 2026 04:51
@ptr727

Copy link
Copy Markdown
OwnerAuthor

Answering the suppressed finding from the round on a7c8e70, which carried no thread.

the shared failure summary prints FAIL gallery: N missing, which is misleading when this check fails (the items aren't missing)

Correct, and fixed in c8f177c. The noun was wrong in the way that matters: the findings are nodes that are present, which is the entire complaint about them, so the summary described the opposite of the defect.

Taking the special-case shape you suggested. Each label can name its own noun and the default stays missing, so the four older checks are byte-identical in output and a check added later reads the way they do unless it says otherwise.

Verified by forcing a failure rather than reading the format string:

FAIL gallery: 15 stray nodes

CopilotAI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 7 out of 7 changed files in this pull request and generated no new comments.

@ptr727
ptr727 merged commit 1aae98a into developAug 8, 2026
5 checks passed
@ptr727
ptr727 deleted the restore-gallery-captions-and-items branch August 8, 2026 05:00
ptr727 added a commit that referenced this pull request Aug 8, 2026
The gallery caption entry under Open decisions is gone: #55 answered the question
it posed, against the captured live site, and closed#52 with it.
Two findings from the VPS agent's §23.3 and §24.3 replace it.
The robots.txt entry described a gap on the interim name and missed that the
cutover makes it a loss. The live .com blog serves a robots.txt today carrying a
Sitemap: line, and this site emits none, so M7b moves a site that has had crawl
directives for years to having none and takes the sitemap pointer with them. That
side found it from the outside, will not put a file in this bundle, and has made
it block step 1 of the M7b checklist rather than be discovered after it. Recorded
as deliberately undecided with the three options, and Open decisions points at it
rather than restating it, so the two cannot drift.
Nothing checks that media survived the trip to the server. A 3,095-request gate
run fetched no image at all, which prompted the question of whether the legacy
media list is wired in. It is, at build time, against files on disk. The live
check requests pages and redirects and never an image, so a media tree lost
between the build and the server is caught by neither, the build having passed
before the loss and the live gate never asking.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
ptr727 added a commit that referenced this pull request Aug 8, 2026
* Record operations as recurring work, not as a finished milestone
The backlog listed the log review and the off-host log pull among the
one-off migration tasks, so a reader finishing that list would read the
migration as done while neither duty had ever run on its cadence. Both
move to a Recurring operations section that says outright that everything
above it ends and this does not, and the State table gains an Operations
row so the summary stops implying the pipeline is the whole system.
The open decision behind them is that the tooling has no home in git.
vps-backup-pull, its systemd units, and the variables naming both ends of
the copy exist only on the Proxmox host, which is the host the backup runs
from, so losing it loses both the copies and the means of making them.
Two candidate homes are recorded without choosing between them.
The same question covers the fourteen migration scripts in the capture
directory, and one of them already shows the cost of leaving it open:
build-redirects.py exists in three places, the two capture copies
identical to each other at 115 lines and this repo's maintained copy at
225. The capture is not a git repository, so nothing detects that.
Two open questions are closed with measurements rather than assumptions.
The production environment's HUGO_BASEURL held the live WordPress address,
which the deploy workflow both builds with and points the live check at,
and it is now the interim name. And the first production deploy does not
fix the robots.txt 404 the VPS agent flagged in its 22.10: hugo.yaml sets
no enableRobotsTXT so the site emits none at all, while sitemap.xml is
emitted and becomes fetchable at that same deploy.
The VPS production row said the resource was deliberately disabled, which
its own file contradicted twenty lines later.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Record the robots.txt cutover loss and the media gate gap
The gallery caption entry under Open decisions is gone: #55 answered the question
it posed, against the captured live site, and closed#52 with it.
Two findings from the VPS agent's §23.3 and §24.3 replace it.
The robots.txt entry described a gap on the interim name and missed that the
cutover makes it a loss. The live .com blog serves a robots.txt today carrying a
Sitemap: line, and this site emits none, so M7b moves a site that has had crawl
directives for years to having none and takes the sitemap pointer with them. That
side found it from the outside, will not put a file in this bundle, and has made
it block step 1 of the M7b checklist rather than be discovered after it. Recorded
as deliberately undecided with the three options, and Open decisions points at it
rather than restating it, so the two cannot drift.
Nothing checks that media survived the trip to the server. A 3,095-request gate
run fetched no image at all, which prompted the question of whether the legacy
media list is wired in. It is, at build time, against files on disk. The live
check requests pages and redirects and never an image, so a media tree lost
between the build and the server is caught by neither, the build having passed
before the loss and the live gate never asking.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
ptr727 added a commit that referenced this pull request Aug 8, 2026
The table said nothing about whether the conversion lost anything, which is
the one question a migration's record has to answer and the one a reader
cannot check for themselves.
Rebased onto develop and reconciled with two things that landed while this sat
open. The volatile counts it originally carried in the Content and media row
are gone, because develop deliberately replaced counts that content additions
move with what they represent, and this row would have reintroduced 514 pages
against that decision. And the claim is narrowed from "closed" to closed for
the files themselves: the image loss was 19 and is restored, but #55 later
found the conversion had also flattened how galleries present media, in three
shapes across three posts. That was not a file loss and no file was missing,
which is exactly why nothing saw it for so long, and it is now gated.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
ptr727 added a commit that referenced this pull request Aug 8, 2026
The table said nothing about whether the conversion lost anything, which is
the one question a migration's record has to answer and the one a reader
cannot check for themselves.
Rebased onto develop and reconciled with two things that landed while this sat
open. The volatile counts it originally carried in the Content and media row
are gone, because develop deliberately replaced counts that content additions
move with what they represent, and this row would have reintroduced 514 pages
against that decision. And the claim is narrowed from "closed" to closed for
the files themselves: the image loss was 19 and is restored, but #55 later
found the conversion had also flattened how galleries present media, in three
shapes across three posts. That was not a file loss and no file was missing,
which is exactly why nothing saw it for so long, and it is now gated.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
ptr727 added a commit that referenced this pull request Aug 8, 2026
The table said nothing about whether the conversion lost anything, which is
the one question a migration's record has to answer and the one a reader
cannot check for themselves.
Rebased onto develop and reconciled with two things that landed while this sat
open. The volatile counts it originally carried in the Content and media row
are gone, because develop deliberately replaced counts that content additions
move with what they represent, and this row would have reintroduced 514 pages
against that decision. And the claim is narrowed from "closed" to closed for
the files themselves: the image loss was 19 and is restored, but #55 later
found the conversion had also flattened how galleries present media, in three
shapes across three posts. That was not a file loss and no file was missing,
which is exactly why nothing saw it for so long, and it is now gated.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
ptr727 added a commit that referenced this pull request Aug 8, 2026
The table said nothing about whether the conversion lost anything, which is
the one question a migration's record has to answer and the one a reader
cannot check for themselves.
Rebased onto develop and reconciled with two things that landed while this sat
open. The volatile counts it originally carried in the Content and media row
are gone, because develop deliberately replaced counts that content additions
move with what they represent, and this row would have reintroduced 514 pages
against that decision. And the claim is narrowed from "closed" to closed for
the files themselves: the image loss was 19 and is restored, but #55 later
found the conversion had also flattened how galleries present media, in three
shapes across three posts. That was not a file loss and no file was missing,
which is exactly why nothing saw it for so long, and it is now gated.
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@ptr727