Message keepsakes and reveal links
Of the 10 presets in the registry behind this site's occasion pages — one preset per page — 5 encode a message rather than a link or a contact card: baby reveal, gender reveal, valentine, gift tag and treasure hunt. A message is harder for a code to carry than a link, not for capacity but for what a phone does once it has read one. This guide works through the two modes the engine offers, the fragment format a reveal link uses, and the file that keeps a message readable without this site. Every figure below was computed at build time by the engine itself.
What a phone does with bare text
A QR code containing bare text carries no scheme, so a phone has nothing to act on. Apple's Camera and Google's ML Kit both recognise links, phone numbers, email addresses, contact cards, calendar events and Wi-Fi joins — and no plain-text type. Faced with bare text, an iPhone offers to copy or search it and Google Lens simply runs a web search, so the message is the one thing the reader never sees. Samsung phones and dedicated scanner apps do display it, but a keepsake cannot depend on which phone opens it.
That is a dispatch problem rather than an encoding one: the code is read correctly; what differs is what the phone then does with text. The methodology page records the same statement.
Two honest modes
The engine offers two modes. In both, on a framed keepsake, the message is also printed under the code when no caption of your own is typed, so the words can be read by eye whatever the phone does — as far as the caption area allows: the frame sets up to 200 characters and 4 lines of it, and a message past that prints clipped, ending in an ellipsis so a cut caption never reads as the whole message. A reveal message at the 300-character limit is therefore longer than its printed fallback.
-
A reveal link encodes an ordinary URL to a page on this site, with the message
carried in the URL's fragment — the part after the
#. A link is a type the recognisers above know, and the message is decoded entirely on the reader's device. - An offline note writes the words into the code itself. It needs no connection, ever, and it accepts the display caveat above in full.
Neither mode hides the other's cost: a reveal link depends on this site answering; an offline note depends on the phone showing text.
The fragment format, worked through
The format is published on the methodology page so the codes stay readable by anyone, with or
without this site: #, then a single flag character (A for UTF-8
bytes, B for raw-deflate compressed bytes), then the payload in unpadded
uppercase base32 (RFC 4648), then one base32 checksum character over the payload. Here it is on a
fictional message of 33 characters — the engine accepts up to
300 before encoding:
Ten summers, one porch, still us.
encodeRevealFragment produced this fragment, 55 characters long:
AKRSW4IDTOVWW2ZLSOMWCA33OMUQHA33SMNUCYIDTORUWY3BAOVZS4Y
| Part | Value | Meaning |
|---|---|---|
| Flag | A | the raw form — the payload is the message's UTF-8 bytes as they are |
| Payload | KRSW4IDTOVWW2ZLSOMWCA33OMUQHA33SMNUCYIDTORUWY3BAOVZS4 | 53 base32 characters, uppercase, unpadded |
| Checksum | Y | one base32 character computed over the payload |
buildRevealPayload then places the fragment on the reveal page's address — the origin
https://KEEPSAKECODES.NET, host in capitals, the path /m/, which the
site also serves as /M/ — giving the 84-character URL the code actually encodes:
https://KEEPSAKECODES.NET/M/#AKRSW4IDTOVWW2ZLSOMWCA33OMUQHA33SMNUCYIDTORUWY3BAOVZS4Y
Hosts are case-insensitive (RFC 3986 §3.2.2), which is why the host and path are written in
capitals: with the fragment already in base32, every character of the host, the path and the
fragment sits in the QR alphanumeric set, while the scheme stays lowercase and the #
is not in that set at all. Beside the URL, buildRevealPayload returns a plan of
4 runs for a mixed-mode encoder — which stretches could take the denser
alphanumeric mode and which would have to stay in byte mode:
| Mode | Text |
|---|---|
| byte | https:// |
| alphanumeric | KEEPSAKECODES.NET/M/ |
| byte | # |
| alphanumeric | AKRSW4IDTOVWW2ZLSOMWCA33OMUQHA33SMNUCYIDTORUWY3BAOVZS4Y |
That plan describes the URL; it is not how the code on this site is encoded.
encodeQr hands the whole URL to its encoder as one text, and that encoder chooses a single
mode for the entire text — numeric when every character is a digit, alphanumeric when every character
is in that set, byte mode otherwise. isQrAlphanumeric reports the fragment as alphanumeric,
https:// as not alphanumeric and
# as not alphanumeric, so the URL as a whole is not alphanumeric and the symbol is encoded in byte mode from end to end. Every version and module figure on this
page is a byte-mode figure; the segment plan records what a mixed-mode encoder could do with the same
URL, and nothing in the figures below relies on it.
Encoded at level H — the level every style that redraws modules requires, and the level the figures on this page use — the URL becomes a version 8 symbol, 49×49 modules, 57 across with the 4-module quiet zone on each side. The same 33 characters as an offline note encode to version 4, 33×33: a reveal link pays for its address in modules.
A frame leaves the code untouched on its panel, so a framed code without heart modules may also carry level Q when the generator's opt-in smaller-code setting is chosen; the engine rejects anything below Q, and a render at either level must still pass decode verification before it can be downloaded. At level Q the same URL is version 7, 45×45 modules, 53 across: 1 version smaller for the same link.
Why base32, and what the checksum catches
Base32 is used rather than base64 because some scanners change the case of what they read, and
base32 survives that; its alphabet is also a subset of the QR alphanumeric character set, which
encodes at 5.5 bits per character instead of 8. Lowercasing the fragment and handing it to decodeRevealFragment returned:
Ten summers, one porch, still us.
The decoder simply re-uppercases. Cutting the last 4 characters off the fragment and handing
it to parseRevealFragment returned no message at all, only this:
This keepsake link was altered in transit.
The checksum is one base32 character and is not cryptographic; it exists so that a truncated or altered link produces that sentence rather than gibberish.
Compression, when it pays
The encoder takes a raw-deflate compressor as an optional argument. Given one,
encodeRevealFragment computes both encodings and keeps whichever base32 payload is shorter
— which is what the flag is for, because compression reliably loses on short messages, where the deflate
header and table cost more than the redundancy they remove. Handed a compressor when this page was
built, the engine kept the raw form for the
33-character message: the fragment above, character for character. A longer
fictional message of 150 characters tips the comparison:
Happy anniversary. Happy anniversary to the porch, the dog, the burnt toast and the twenty years we never counted. Happy anniversary, again and again.
Raw, its fragment runs to 242 characters; compressed, 148. Given the compressor, the engine kept the deflate form, flag B. This page was built with a raw-deflate compressor in Node to show the comparison the format
allows; the generators on this site do not pass a compressor today, so every reveal link they
produce carries flag A, and the reveal page inflates a flag-B
payload with the platform if one ever arrives.
So the link a generator on this site makes for the long message is the raw one — flag A, a 271-character URL — and at level H it encodes to version
17, 85×85 modules: 93 across
against the engine's comfortable ceiling of 65, which it exceeds. The compressed form the generators do not emit would be a 177-character URL at version 13, 69×69 — 77 across, still past the ceiling, 4 versions smaller than the raw link. As an offline note the same words are version 12, 65×65. Past that ceiling the engine's guidance is a shorter message rather than a larger print; the
printing and scanning guide covers what a
module count means on paper.
Where the message goes — and where it never does
Per RFC 3986, a fragment is stripped before a request is made: it never reaches this site's
servers, a CDN, or any log, and the reveal page decodes it entirely on the reader's device. A
query string would carry the same characters, but it is part of the request, so every message
would land in a server log — which is why the engine does not use one. The reveal page is served
at /m/ and at /M/ as a rewrite rather
than a redirect, because a redirect round-trip is exactly where a fragment can be lost, and the fragment
is the message.
The offline note and the inscription fold
An offline note hands the message to the encoder as it is — buildTextPayload
checks only that it is not empty and changes nothing — so the code carries the UTF-8 bytes of the
words. For the short message that is a version 4 symbol at level H, 33×33 modules. The caveat stands in full: the phones named above that do not display
text will not display this either, which is why, on a framed keepsake, the message is also printed
under the code when no caption of your own is typed, within the caption limits above.
There is one more text form in the engine, built for capacity rather than display.
toInscription folds text into the QR alphanumeric mode — uppercase, accents flattened,
a few meaning-preserving substitutions, anything else removed — because that mode packs more characters
into the same modules than byte mode does, which is what raises a version-4 photo-woven
symbol from 34 characters to
50. On the short message it returns TEN SUMMERS ONE PORCH STILL US. and reports the text as altered, since the 2 commas in the original became spaces. Still, an inscription is a
bare-text payload with the same display caveat as any offline note — a capacity feature, not a fix;
the
photo halftone guide covers that style.
The file that outlives the site
A reveal link works only while keepsakecodes.net is answering. The engine's answer is
buildStandaloneReveal, whose own description calls it an escape hatch from this
site's own mortality: a single self-contained HTML file that decodes the same fragment format
with no network, no dependencies and no origin, so it runs from a USB stick, an email
attachment, or a file:// address. For the short message the file is 2491 bytes — 2.4 KB — in its default presentation. It carries its own copy of the base32 decoder,
which is why the test suite runs the emitted file against the real encoder, so a duplicated decoder
cannot silently drift.
The message is baked into the page as a data attribute and as the visible text, so the file
shows it when opened with no fragment at all; and a fragment is still honoured when one is
present, so the same file opens any other keepsake link. Its decoder reads flag A fragments only; a flag B
fragment is not decoded by the file, which carries no decompressor — its baked-in message never needs
one.
What verification proves for a message keepsake
Whichever mode is chosen, the render is decode-verified before download: the browser rasterises the finished artwork at 800, 1200, 1600 px, runs jsQR on each, and compares the decoded bytes with the exact payload — the full URL for a reveal link, the exact text for an offline note. The badge is earned when at least 2 of the 3 rasters match; anything else is a warning with the downloads locked. For a reveal link that means the fragment, checksum included, came back byte for byte from the artwork; the decode verification guide explains why 3 sizes rather than one.
What it cannot prove is anything after the decode. It cannot see your printer, your paper, or the phone that will do the scanning, and it cannot make a phone display bare text. Print at least 2 × 2 cm for arm's-length scanning, keep the 4-module quiet zone uncropped, and scan a test print with a real phone before the moment that matters.
Common misreadings
- Cropping the quiet zone. The 4-module margin is part of the symbol; the engine always draws it, and on a framed keepsake the code sits with its quiet zone inside the clean panel, which frame art never covers. The panel edge is therefore safe to crop to; trimming the white inside the panel down toward the dark modules removes a mandatory part of the code.
- Expecting an offline note to display. On the phones named above that do not show bare text, it will not; on a framed keepsake, the words printed under the code are the fallback by design.
- Editing the link. The checksum covers the payload and is meant to produce "This keepsake link was altered in transit." rather than a wrong message — cutting 4 characters from this page's fragment did exactly that. Retyping a fragment in lowercase is fine; shortening it is not.
- Low-contrast heart colours. The valentine preset starts on the heart-module style, where foreground and background must measure at least 4.5:1 and the engine rejects weaker pairs rather than warning; its default heart colour on white measured 8.02:1 on the short message's render. The heart-module guide shows pairs the gate accepts and refuses.
- One code for a whole hunt. A treasure hunt is plural; the treasure hunt chains guide covers how a clue list becomes a numbered chain.
Message keepsake FAQ
How long can a reveal-link message be?
A reveal message is at most 300 characters before encoding; past that the engine refuses with "A reveal message is at most 300 characters." The 33-character example on this page encodes to a 55-character fragment on a URL 84 characters long.
Is the message sent to KeepsakeCodes when someone scans the code?
No. The message travels in the URL fragment, the part after the #, and per RFC 3986 a fragment is stripped before a request is made — it never reaches this site's servers, a CDN, or any log. The reveal page decodes it entirely on the reader's device.
What if a scanner changes the case of what it reads?
That is why the payload is base32 rather than base64: base32 survives a case change because the decoder simply re-uppercases. Lowercasing this page's example fragment and decoding it returned "Ten summers, one porch, still us." unchanged.
What happens to a reveal link if this site goes away?
The link stops opening, which is why every message keepsake can also be downloaded as a self-contained HTML file that decodes the same fragment format with no network and no website at all — 2.4 KB for the example here in its default presentation. An offline note never depended on the site.
Reveal link or offline note — what is the actual trade?
A reveal link is a normal URL, so the phones the methodology page names recognise it, but it needs this site to answer. An offline note needs nothing, ever, but accepts that an iPhone offers to copy or search bare text and Google Lens runs a web search instead of showing it. On a framed keepsake with no caption of your own, both modes also print the words under the code — up to 200 characters and 4 lines of them, a longer message ending in an ellipsis.
Every render is decode-verified in your browser before it can be downloaded. The rules this guide describes, and the measurements behind them, are documented on the methodology page; the other guides take the remaining styles one at a time.