The gate list for 1.0.0 had eight items under "bugs the audit turned up". Two of them were not tidying: the home page had two URLs with the canonical naming the copy, and any URL typed in the panel went into an href without a scheme check. Both were an hour, and both were about to be frozen behind a version number.
1.0.0 was going to be a small release. Drag to reorder had landed, the editor could move between pages, the API needed naming. Tag it.
Before tagging I reread Docs/tasks/14-v1-release.md, the file that lists what has to be true before the number changes. Under "the bugs the audit turned up", written on 3 September and not looked at since, two entries stopped me.
One page, two URLs, canonical on the wrong one
The home page is the page whose slug is home, and the controller serves it at /. Fine.
Page::url() had no case for it:
return $locale === array_key_first(config('atelier.locales'))
? url($slug) // "home" -> /home
: url("{$locale}/{$slug}");
So the canonical tag said /home. The hreflang alternates said /home. The sitemap entry said /home. Every menu item a client built by picking the home page said /home. Meanwhile /home served the same content, through the catch-all, with a 200.
Duplicate content, with the canonical pointing at the duplicate, on a package whose main argument is that it is better for SEO than a client-side page builder.
The fix is four lines in url() plus a 301 from the named URL to the root. What made it worth the interruption is that the wrong URLs were being written into menus and sitemaps as data, so shipping 1.0.0 first would have meant every site built on it accumulating them.
Two existing tests changed, which is the honest signal that behaviour moved rather than a typo being corrected.
A javascript: link in an href
Every block that renders a client-supplied URL did this:
<a href="{{ $attributes['cta_url'] ?? '#' }}">
Blade escapes the value, so it cannot break out of the attribute. It does nothing at all about the scheme. Type javascript:alert(document.cookie) into a call to action's button link and every visitor to that page runs it.
The threat model is narrower than it sounds, since it needs panel access, and the person with panel access owns the site. It is still stored XSS: a client pastes a tracking link somebody emailed them, or an agency hands the panel to a junior, or the panel has ten users and one of them is annoyed. And the mitigation is an allowlist:
public const SCHEMES = ['http', 'https', 'mailto', 'tel', 'sms'];
Allowlist rather than blocklist, so data:, vbscript: and whatever the next one turns out to be are refused by not being on it. The fiddly part is deciding what counts as a scheme at all: /a/b:c is a path, #a:b is an anchor, and only a colon before any slash, question mark or hash introduces a scheme.
Why they had been sitting there
Both were written down. Both had a paragraph explaining exactly what was wrong. Neither was fixed, for eleven days, because they arrived during a simplification pass while I was working on something else, and "write it in the gate file" felt like dealing with it.
It is not nothing: they were still there to find. But a list of known bugs is a decision deferred, not a decision made, and the deferral had no expiry on it. The thing that forced it was the version number, because 1.0.0 promises the API will not move and a bug in a public method is much more expensive to fix after that promise than before it.
The one I did defer, and said so
The same list has six other items: no revisions UI, an unchecked file_get_contents, six public methods with no callers, a duplicated FAQ schema builder. Those are in the release notes as deferred with the reason, rather than quietly left.
The difference is not size. It is whether leaving it writes bad data into someone's database, which the canonical bug was doing every time a menu item was created.
Where it stands
Both fixed and shipped in v1.0.0, with tests, including one that asserts a hostile scheme never reaches the rendered page.
The lesson I want to keep: a checklist is only worth writing if something eventually forces you to read it. A release is that thing. Without one, the file is a museum of intentions.