Repository navigation
Conversation
…moment Versions are numbered per locale, so "version 3" names a different state in every language. Nothing identifies one moment across locales, which a printed carrier needs: the passport state a product was placed on the market with is one moment, not one per language. Add `publication_releases`: one row per minted version, with a `sequence` that is monotonic per publication, minted inside the same lock as the version number. `publication_versions.release_id` points at it. Existing versions are backfilled, one release each in publish order. `PublicationRelease::versionsAsOf()` resolves the state as of a release: for every locale, the most recent version minted at or before it. That is the read a per-release public route needs and nothing else; this change alters no public behaviour. Releases are immutable like versions (update/delete throw), and `release_id` is sealed on the version.
… out of shared caches `Publisher::redactAll()` redacted only the `is_current` versions before flipping the publication to Redacted. Every superseded version kept its sealed payload readable in the database, so a GDPR Art. 17 erasure held only for as long as nothing ever read history. Now every not-yet-redacted version of the publication is redacted; versions already redacted on their own are left untouched. The public tombstone changes with it. A redacted passport answers `410 Gone` (the state is irreversible), while a withdrawn one stays `200` because it can be reinstated. Both tombstones are now `private, no-store` and `noindex`: the package has no cache purge hook, so a shared cache must never hold a tombstone that a reinstatement would have to displace, and the tombstone must not be indexed regardless of the channel's `indexable` setting. Tests: redactAll nulls superseded payloads and stamps the reason on them; a version redacted individually earlier keeps its own reason; the redacted route is 410 + no-store + noindex; the withdrawn route is 200 + no-store + noindex.
Adds `GET /{prefix}/{uuid}/r/{sequence}/{locale}`: the state of the
passport as of one release, for one locale, resolved through
`PublicationRelease::versionsAsOf()`. This is the URL a printed carrier
can be bound to: it names one moment across every language and keeps
resolving to exactly that state after the passport moves on.
Semantics:
- strict locale, no Accept-Language negotiation; a locale that had no
version yet at that release is 404 there
- 200 with a banner naming the release and whether it is still current,
linking to the live page; the locale switcher stays inside the release
- never indexed (`noindex, noarchive, nofollow`, a `<link rel=canonical>`
and a `Link` header pointing at the live page); no JSON-LD negotiation,
since a historical payload's stamped identity is the publication URL
- a version redacted individually renders the tombstone with 410 and
`no-store`, even while the publication itself stays Published; this
also closes the same gap on the live route
- the ETag now covers release, currency and redaction state, because
the banner and the tombstone change the HTML without changing the
checksum; TEMPLATE_VERSION bumped
- release pages are not counted as views
`show()` and `showRelease()` share one `render()`. The four new strings
live in the Publication package and are present in all 33 locales, in
English where no translation exists yet, so the translation audit passes.
midego1
force-pushed
the
feat/publication-release-route
branch
from
September 4, 2026 08:29
4a4809f to
d80bf18
Compare
This was referenced Sep 4, 2026
Contributor
Author
|
Context, motivation and suggested review order for this and the related PRs: #683 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Stacked on #680 and #679
This PR shows their commits until they land. Only the last commit (
feat(publication): public per-release route) is new here.Problem
Every public passport URL resolves to the current state. A QR code printed into a manual two years ago therefore shows today's data. The regulation asks for the opposite: the state the product was placed on the market with must stay reachable, and one printed reference must keep meaning that one state. #680 gave that state a name (a release, spanning all locales); nothing serves it yet.
Change
Route
GET /{prefix}/{uuid}/r/{sequence}/{locale}in the per-type group,where('sequence', '[1-9][0-9]{0,9}'). Four segments with a literalr, so it cannot shadow the existing two-segment routes. Resolved viaPublicationRelease::versionsAsOf(): for the requested locale, the most recent version minted at or before that release.Semantics
Accept-Languagenegotiation on an explicit historical URL: a reference must not resolve to a different document per reader. A locale that had no version yet at that release is 404 there.X-Robots-Tag: noindex, noarchive, nofollow, a<link rel="canonical">and aLink: <…>; rel="canonical"header pointing at the live page. No JSON-LD negotiation on release pages: a historical payload's stamped identity is the publication URL, which would misdescribe it.410andno-store, even while the publication itself stays Published. The same check now applies on the live route, which previously handed the template a null payload while telling it the page was not withdrawn.ETag now also covers the release sequence, whether the version is still current, and whether it is redacted: the banner text and the tombstone change the HTML without changing the payload checksum.
TEMPLATE_VERSIONbumped to invalidate cached live pages.Refactor:
show()and the newshowRelease()share one privaterender(); behaviour of the live route is unchanged apart from the redacted-version case above.Strings: four new keys under
publication::app.public.release, present in all 33 locales (English placeholders where no translation exists yet).unopim:translations:checkpasses.Tests
Stub-template route tests (
PublicPassportRouteTest):Link, no indexing, and the earlier payload rather than the newer one; latest release marked current0and non-numeric are 404no-storewhile the live page still serves the newer stateReal template (
PassportPageRenderTest): banner,<link rel="canonical">,noindex, switcher links inside the release, and no JSON-LD on a release page.Related
Follows #677, #678, #679 and #680. Next on this track: an issuance record binding a printed carrier to the release it was issued against, and GS1 Digital Link qualifiers (
/01/{gtin}/10/{lot}).