Skip to content

Social preview images for the public pages - #155

Merged
asm0dey merged 25 commits into
mainfrom
feat/social-preview-images
Aug 26, 2026
Merged

Social preview images for the public pages#155
asm0dey merged 25 commits into
mainfrom
feat/social-preview-images

Conversation

@asm0dey

@asm0dey asm0dey commented Aug 26, 2026

Copy link
Copy Markdown
Owner

Adds og:/twitter: metadata to every shareable page and a rendered card image
per meeting type — and, first, stops the capability URLs from unfurling at all.

Why the token pages come first

/booking/{manageToken}/* and /guest/{declineToken}/* are reachable by
anyone holding the link. An unfurl paints the invitee's name, the meeting and
the time into whatever chat the token was pasted into, and some clients
prefetch previews server-side. These pages now emit noindex,nofollow and no
og: tags — by default, not by flag: base.html only renders the tags for
pages that pass an OgCard, so a page that doesn't build one gets nothing to
leak.

Card endpoints

Three routes render a PNG on demand:

  • /og.png — the generic product card
  • /og/{user}.png — an owner's card
  • /og/{user}/{slug}.png — a specific meeting type's card (owner, meeting
    name, duration(s))

Caching is an ETag computed over the inputs that affect the render
(owner name, type name, allowed durations, location kind) plus
Cache-Control: public, max-age=3600 — no invalidation step needed, because a
changed input produces a changed ETag automatically. Verified with a
rename-changes-the-ETag test, not just stability-across-two-requests.

Every degenerate case — unknown user/slug, an inactive or secret meeting
type, a disabled owner — falls back to the generic product card at HTTP
200
, never a 404. A 404 unfurls as a broken image in chat clients, which is
worse than a slightly-wrong preview.

Security fix found in review

A disabled owner's card originally leaked their real name and meeting details
at HTTP 200, while the booking page correctly 404'd the same account —
reopening the enumeration oracle calit-h8mb had already closed (a prober
could distinguish "disabled" from "never existed"). Card resolution now
applies the same enabled guard as the booking path and degrades to the
product card. Fixed and tested in-branch (commit 637df6f).

Both container images previously could not render

  • The native image could not even compile: CardRenderer's
    static final Color constants get folded into the native-image heap
    because CardRenderer is a CDI @ApplicationScoped bean and Quarkus
    initializes bean classes at build time, and GraalVM refuses run-time-init
    types (java.awt.Color) there. Confirmed pre-existing on bare HEAD before
    any change on this branch. Fixed with
    --initialize-at-build-time=java.awt.Color in Dockerfile.native, decided
    over the alternative of forcing run-time init on CardRenderer itself
    (would fight Quarkus's own build-time bean initialization, untested, to
    guard a static field nobody has added yet).
  • The JVM image lacked libfreetype/fontconfig/any font on disk, so
    card rendering failed at request time.

Every failure in this class is build-green and request-red — the build
succeeds, the request 500s. The CI image smoke test now requests /og.png and
/og/admin.png and checks PNG magic bytes plus non-identity between the two, so
this class of failure fails the build instead of shipping an image.

Note on where that check runs: the image build/smoke job is gated on
github.event_name == 'push' to main or a tag, so it does not run on this
PR
— it fires on the merge commit, before any image is published. The
guarantee for this branch therefore rests on local verification, which was done
rather than assumed: both images were built, booted against Postgres, and asked
for /og.png and /og/admin.png; both returned distinct real PNG image data, 1200 x 630, and the JVM and native outputs were byte-for-byte SHA-256
identical.

JVM image moved to a hardened distroless base

Advancing part of calit-gabg: the JVM production image now runs on
bellsoft/hardened-liberica-runtime-container:jre-distroless-muslno
shell, no package manager
. The font stack (freetype, fontconfig,
font-dejavu-core, pre-warmed fontconfig cache) is built in a separate donor
stage and COPYed in by SONAME only. Dockerfile.native is unchanged
no hardened bare JRE base currently exists, so a native binary would land
in a much larger regular JRE image, which would be a real regression.

Sizes: JVM 214 MB → 211 MB (smaller, despite gaining font rendering);
native ~161 MB.

New to this repo: @QuarkusIntegrationTest + failsafe

OgImageResourceIT is a black-box HTTP test that boots the packaged app as a
separate process and asserts /og.png (PNG magic, 1200×630, content-type,
Cache-Control). It cannot seed data via QuarkusTransaction the way the
unit-style OgImageResourceTest does, so DB-backed cards stay covered by the
CI container smoke's /og/admin.png probe instead. maven-failsafe-plugin
binds to verify, so mvn verify now packages and boots the app —
mvn test is unaffected (test phase runs before verify).

og:locale

Always en_US. Unfurl bots send no Accept-Language, so there is nothing to
key a per-locale render off of — this is the one deliberate, documented
exception to the translate-everything rule; the card and preview strings are
fixed English literals.

Still outstanding

  • The real-client unfurl check has NOT been done. It needs a publicly
    reachable deployment and a human pasting a link into an actual chat client
    (Slack/Telegram/WhatsApp/iMessage) to confirm the card renders and that a
    token URL does not unfurl. Everything short of that has been verified:
    HTML output, served PNG bytes/dimensions/determinism, and the container
    smoke test.
  • The docs-site changelog fix is committed but not pushed (commit
    dae0608 on docs-site, corrected image-size numbers and split the dense
    entry per the 1.21.0/1.20.2 precedent). Needs a manual git push on that
    branch — outside this repo's remote, and per this repo's norm shared
    branches aren't pushed without explicit authorization.

Follow-ups filed

  • calit-ss7l — co-hosted meeting types unfurl with the generic card, not
    their own (the page resolves via resolveForAlias, the card via
    findBySlug; degraded, not broken — no leak, no crash).
  • calit-fanm — brand-lockup chip box-shadow (not drawn) and proportion
    ratios vs the live site's .lp-brand (chip corner radius and wordmark
    tracking were real bugs, fixed in this branch already).

Spec

docs/superpowers/specs/2026-08-26-social-preview-images-design.md

🤖 Generated with Claude Code

https://claude.ai/code/session_01MrnPiNwAQhe2RtmYTteNcj

asm0dey and others added 19 commits August 26, 2026 14:00
Design for og:/twitter: metadata on every shareable page, a per-meeting-type
card image, and — first — suppressing previews on the capability URLs that
leak invitee details into any chat the token is pasted into.

Rendering rests on spike calit-xu12: AWT works in the native musl image, but
only with runtime-image changes the build never warns about. Includes the
spike bean.
Rendering in-process forecloses a scratch/distroless final layer (AWT is
dlopen-based; Chainguard and distroless are glibc), and adds freetype +
fontconfig to what must be kept patched. /tmp must stay writable because
Font.createFont(InputStream) spills to a temp file.

Hardening work itself is calit-gabg, filed alongside.
The JVM runtime cannot render either — liberica-runtime-container:jre-26-musl
is missing libfreetype.so.6, so this was a defect, not a risk.

BellSoft does publish hardened images (separate repos I had missed). Their
distroless musl JRE has no apk and no shell, so the font stack has to be
COPYed from a builder stage — verified working, and at 134.2 MB it lands
3.8 MB smaller than today's base that cannot render at all.

Corrects the earlier claim that distroless was foreclosed: that applies to a
static binary, not to a distroless layer. Base choice stays calit-gabg's call.
Seven tasks, leak fix first. Records that quarkus-awt's own requirement
(freetype + fontconfig) is necessary but not sufficient: without an actual font
file the JDK font manager will not initialise, which breaks Font.createFont even
for an embedded TTF.
base.html emits og:/twitter: only when a page passes an OgCard; absent means
noindex,nofollow. The token pages pass nothing, so they cannot unfurl an
invitee's name, meeting and time into a chat — by default rather than by flag.

Secret meeting types get the product card: naming a hidden meeting in a preview
would defeat the flag.
Rubik covers Latin, Cyrillic, Hebrew and Arabic in one file; the site's Hanken
Grotesk is Latin-only, so it stays the wordmark. AWT does no automatic fallback
for createFont-loaded fonts, hence the run splitting.

All faces are static instances baked with fonttools: AWT ignores fvar axes and
loads whatever the default instance is.
Regenerate all seven faces with fontTools' --update-name-table so
getFontName() reports the pinned weight instead of the source variable
font's stale default-instance name (e.g. both Rubik weights previously
read "Rubik Light"). Fraunces needs an extra step: its STAT table only
registers exact opsz nominals at 9/72/144, so --update-name-table can't
be pinned directly at opsz=14; derive the name from a throwaway opsz=9
probe (same "9pt" bucket) and transplant it onto the real opsz=14
shape.

semiboldChainUsesSemiboldFallback previously asserted only
contains("Noto"), which passed identically whether chain(true) used
notoSemiBold or notoRegular. With accurate per-weight names it now
asserts the resolved font is specifically "Noto Sans SemiBold" -
verified this fails with the right message when notoRegular is
substituted in, then restored the correct wiring. Tightened two more
assertions (latin/Greek runs) now that exact names are meaningful.
Centred composition keeps everything inside the central 630x630 square, so a
square-cropping client still shows the whole card — a left-aligned draft became
a fragment of one word with no logo. Centring also removes any need to mirror
for RTL.

Fixed a fit-ladder gap while adding ellipsize test coverage: an unbreakable
single word (or a leading word alone) longer than the safe square skipped the
wrap stage without ever reaching ellipsize, so it overflowed past x=915
untouched. fitHeadline now checks the constructed first line's width before
returning it and falls through to ellipsize when wrapping can't help.

Verified render()/product() are byte-for-byte deterministic: identical SHA-256
across repeated calls in one JVM and across separate JVM launches, and the PNG
carries no tIME chunk. Task 4's ETag and identical-bytes assertions depend on
this.
Rendered per request behind an ETag over the inputs, so a rename invalidates
nothing. Unknown, inactive and secret targets degrade to the product card with
200 — a 404 unfurls as a broken image.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MrnPiNwAQhe2RtmYTteNcj
OgImageResource.findOwner had no `enabled` check, so a disabled owner's real
name and meeting-type detail still rendered (200) via /og/{user}.png and
/og/{user}/{slug}.png even though their actual booking page 404s
(PublicResource.resolveOwner, calit-h8mb). Both routes now fall back to the
product card for a disabled owner, same as an unknown one -- never a 404,
since a 404 unfurls as a broken image.

Collapsed findOwner into the existing AppUser.findByUsername (byte-identical
lookup) and applied the enabled check at both call sites instead of keeping a
duplicate query path in a security-relevant file.

Also closes two test gaps flagged in review: the inactive-type fallback (a
spec-named miss path with no prior test) and the ETag's duration/location
axes (previously only the name axis was proven input-sensitive).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MrnPiNwAQhe2RtmYTteNcj
RoundRectangle2D's arc arguments are diameters, not radii, so the chip's
corners were drawn at half the site's 30% radius. The wordmark advanced by
fm.charWidth(), an integer-rounded advance that also drops kerning; a font
derived with TextAttribute.TRACKING draws "calit" in a single drawString
call, honouring real kerning and staying byte-for-byte deterministic.

Measured: chip corner inset 7px -> 15px (target ~14px). "calit" ink width
stays at 60px with the shipped static Hanken Grotesk instance -- the
mechanism is correct and verified monotonic/deterministic, but this specific
font asset's natural advance already sits at 63px untracked, so applying the
site's own -0.02em overshoots below the browser's 63.2px reference (measured
against the live Google-hosted font, not our baked static copy). Left the
-0.02 constant alone rather than retuning it to force a match; see
task-3b-report.md for the full investigation.
Neither could. The native runtime needs AWT's dlopen'd JDK .so files beside the
binary; both need freetype and at least one font on disk, because an empty
fontconfig makes the JDK font manager fail to initialise and that breaks
Font.createFont even for an embedded TTF. The native build also failed outright
(before any request could even happen) on java.awt.Color being embedded in the
image heap by CardRenderer's static Color constants; fixed via
--initialize-at-build-time=java.awt.Color rather than touching CardRenderer.

CI now requests /og.png AND /og/admin.png and checks PNG magic bytes on both,
for both the jvm and native matrix legs: every failure of this kind passes the
build and dies on the first request.

A hardened/distroless base under calit-gabg requires switching both runtime
stages from apk to a copy-from-builder stack: libfreetype.so.6,
libfontconfig.so.1, libexpat, libbz2, libpng16, libbrotlidec, libbrotlicommon,
/etc/fonts, the font files, and /var/cache/fontconfig with fc-cache run in the
builder stage. Font.createFont(InputStream) also spills to a temp file, so a
read-only root filesystem needs a tmpfs mount at /tmp regardless of base image.

Also adds a JVM-mode @QuarkusIntegrationTest (OgImageResourceIT) as a packaging
check, wired via a new maven-failsafe-plugin bound to verify.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MrnPiNwAQhe2RtmYTteNcj
Code review flagged the --initialize-at-build-time=java.awt.Color comment as
incomplete: it said "safe" without naming the scope risk (the flag names
Color specifically, so a future AWT-typed static field on CardRenderer -- a
Font, a GradientPaint, a BasicStroke -- reproduces the identical fatal build
error) or the durable alternative and why it wasn't taken
(--initialize-at-run-time=CardRenderer would cover any future field, but
fights Quarkus's build-time CDI bean init untested, to guard against nothing
written yet). No code changed; comment-only.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MrnPiNwAQhe2RtmYTteNcj
Point 1 of the Dockerfile.native comment claimed java.awt.Color defaults to
run-time init via a JDK static initializer calling Toolkit.loadLibraries()
plus a conditional initIDs(). That mechanism does not exist on Color (no
static initializer at all, per JDK 26 src.zip) and belongs to other AWT
classes that need a native display connection.

Corrected: quarkus-awt's AwtProcessor build step classifies java.awt.Color
into the ICC colour-management surface (ColorSpace/ICC_Profile/...) because
Color carries a private ColorSpace cs field (Color.java:255), which is what
actually needs the native CMM at runtime. Our Color constants use the
plain-int sRGB constructor, so cs stays null -- the real reason build-time
init is safe here. Points 2-4 of the comment (safety evidence, scope risk,
rejected alternative) were already accurate and are unchanged.

Comment-only; no code changed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MrnPiNwAQhe2RtmYTteNcj
Final-review fix wave for calit-o89d (social preview images):

- CardRenderer.renderable() now rejects a blank type name -- fitHeadline
  produces an empty run list for "", and render() indexed into it
  unconditionally (line.getFirst()), crashing /og/{user}/{slug}.png with a
  500 for any type whose name is blank (reachable: AdminResource has no
  server-side blank check, and Slugs.uniqueMeetingTypeSlug turns a blank
  base into "meeting"). Falling back to the product card reuses the same
  path every other unrenderable input already takes.
- Documented why renderable()'s font-coverage check against chain(false)
  also covers what the headline draws with chain(true).
- CI's /og/admin.png smoke probe now asserts the owner card differs from
  the product card, so a silent fallback can no longer pass unnoticed.
- Dockerfile: install the AWT font stack before the app COPYs so a
  source-only change doesn't bust that layer's cache.
- CLAUDE.md: note that *IT classes run under `mvn verify` via failsafe,
  not `mvn test`.
- ADR-0009: note the Cache-Control: public / auth-cookie interaction on
  the rendered card endpoints.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MrnPiNwAQhe2RtmYTteNcj
…stack

The spec always intended the production JVM image to run on BellSoft's
hardened/distroless runtime base rather than the plain musl JRE image with
apk-installed freetype/fontconfig -- that apk install was only ever meant to
ship long enough to be immediately replaced (calit-gabg). Doing the move now
means the apk-based Dockerfile never ships at all.

The hardened distroless base has no shell and no package manager, so the
font stack AWT card rendering needs (libfreetype, libfontconfig, their
transitive libs, /etc/fonts, the font files, and a pre-warmed
/var/cache/fontconfig) can no longer be `apk add`ed in the runtime stage.
Instead, a new `fontstack` donor stage reuses the jdk-26-musl image the
build stage already pulls (no new image enters the build), installs the
same apk packages there, and the runtime stage COPY --from=fontstack's the
resolved library files.

Verified by booting the built image against Postgres with the CI smoke
test's exact env vars, completing /setup, and confirming /og.png and
/og/admin.png both return distinct real 1200x630 PNGs (not a silent
fallback to the generic card) -- "it builds" doesn't prove the font/AWT
path still works on a base with no shell to poke at. Image size dropped
slightly (220 MB -> 213 MB) and the old "Fontconfig error: No writable
cache directories" boot-time log line is gone now that the cache is
pre-populated as root in the donor stage instead of built at runtime by
UID 1001. Dockerfile.native is untouched -- there is no hardened bare JRE
base, so moving it would regress its ~8.2 MB Alpaquita final layer to a
130 MB JRE image.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MrnPiNwAQhe2RtmYTteNcj
Review found the prior COPY's justification for shipping both the versioned
real file and the unversioned symlink was wrong: BuildKit's COPY --from=
dereferences a symlink source into a full regular file at the destination,
it never leaves a dangling link. readelf -d on the hardened base's
libfontmanager.so confirms its NEEDED entries reference only the unversioned
SONAME (libfreetype.so.6, etc.), never the versioned filename.

Drop the versioned copies -- they were dead weight, and the exact thing a
Renovate digest bump of the fontstack donor image could break, since
renovate.json pins docker digests. Only the SONAME symlink names are copied
now; BuildKit resolves each into the real file the dynamic linker needs.
Rewrote the adjacent comment to state the correct reasoning instead of
leaving the disproven dangling-symlink claim next to corrected code.

Re-ran the full boot-and-request acceptance sequence (not just a rebuild):
/setup -> 302, /og.png and /og/admin.png both real distinct 1200x630 PNGs,
no Fontconfig cache-directory warning, no reflection/init errors. Image
size dropped again, 213 MB -> 211 MB, from no longer shipping each lib
twice.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MrnPiNwAQhe2RtmYTteNcj
…out calit-o89d todos

Pinning the base image by digest doesn't pin the apk packages inside it -- apk
add resolves against the live repo at build time regardless. Document why that
is left deliberate (CVE-rich C font parsers benefit from picking up patches on
every rebuild; Trivy + the /og.png smoke test already gate both failure modes;
Renovate can't manage an apk pin here anyway).

Tick calit-o89d's per-locale-card todo with an explicit "not shipped --
deliberate" annotation instead of leaving it dangling forever (og:locale is
always en_US since unfurl bots send no Accept-Language). The real-client
unfurl-verification todo stays unticked; it needs a human and a public
deployment.

Also commits the calit-ss7l bean (co-hosted meeting types unfurl with the
generic card) filed during the final whole-branch review but not yet staged.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MrnPiNwAQhe2RtmYTteNcj
@asm0dey asm0dey closed this Aug 26, 2026
@asm0dey asm0dey reopened this Aug 26, 2026
asm0dey added a commit that referenced this pull request Aug 26, 2026
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MrnPiNwAQhe2RtmYTteNcj
asm0dey and others added 6 commits August 26, 2026 18:31
FirstRunRedirectFilter redirects every request to /setup while no AppUser
exists, but its allow-list never covered /og.png or /og/*.png. This branch
made "/", "/privacy" and "/terms" advertise those routes as their og:image
on the theory that they're already exempt for exactly this reason (e.g. an
unfurl bot on a fresh instance) -- but the image URL itself 302'd to /setup,
so a crawler following it got text/html instead of a PNG. OgImageResourceIT
caught this in CI: it boots the packaged jar against a brand-new database,
so it always hits the zero-user state that local runs mask via the reused
Dev Services Postgres.

Exempt /og.png and the /og/ prefix. Safe pre-bootstrap: with zero AppUsers
there is no tenant data to leak, so every card degrades to the generic
product card by construction, and "og" is already reserved in
Usernames.RESERVED so the prefix can never shadow a real user.

Added FirstRunOgImageTest (mirrors FirstRunLegalPagesTest's real-DB pattern)
to catch this without needing the slower IT packaging round-trip; confirmed
it fails pre-fix and passes post-fix, and re-ran the full mvn verify both
with the reused Dev Services Postgres and with a forced-fresh one
(-Dquarkus.datasource.devservices.reuse=false).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MrnPiNwAQhe2RtmYTteNcj
CardRenderer.drawLockup computed the chip+wordmark centring offset with
integer division (lockWidth / 2), truncating it by half a pixel whenever
the tracked wordmark width was odd -- a real defect, not just Sonar's
S2184 cast nit, since the card's whole crop-safety design leans on this
lockup being exactly centred. Made the width/offset arithmetic float end
to end; the S2184 warning disappears as a consequence. Verified
render()/product() stay byte-for-byte deterministic across repeated
calls after the change.

Raised new-code coverage on CardRenderer/CardFonts/TextRuns/
OgImageResource (77.0% -> comfortably over 80% estimated) by adding
tests for the headline fit ladder's three stages, renderable()'s
boundary conditions, CardFonts' accessors and chain(), and
location()/meta() branches. Discovered along the way that
quarkus-jacoco only instruments code reached through a booted
@QuarkusTest -- plain-JUnit tests exercising these AWT rendering
classes directly are invisible to the coverage report regardless of
how real their assertions are, so the new coverage-targeted tests are
@QuarkusTest classes with CDI-injected beans instead.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MrnPiNwAQhe2RtmYTteNcj
A code review confirmed quarkus-jacoco never activates without a booted
@QuarkusTest in the run: plain-JUnit tests contribute nothing to the
coverage report no matter how thorough their assertions are. This cost
real time on this branch (CardRenderer showed 64% new-code coverage
despite CardRendererTest exercising it thoroughly) so it's worth being
explicit that a low Sonar number here can mean "wrong kind of test",
not "no test".
…n allow-list

Found reviewing the social-preview card fix (2cb5af1). FirstRunRedirectFilter
matches its allow-list against rc.request().path() -- the raw, undecoded path --
while Vert.x dispatches on normalizedPath(), which percent-decodes and collapses
dot segments. So /og/%2e%2e/me satisfies startsWith("/og/") on the raw string
while actually routing to /me.

Pre-existing and broader than the /og entries that surfaced it: the same
mismatch applies to /, /img/, /privacy, /terms, /calit.css and /q/. Fixing it
changes matching semantics for every exempt path on a filter that runs before
security, so it needs its own test pass rather than a branch-tail one-liner.

Impact is bounded -- the window exists only pre-bootstrap, the filter runs
before security rather than instead of it, and the /og endpoints are safe
however they are reached (owner == null is checked first, and ON DELETE CASCADE
makes an orphaned MeetingType impossible).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MrnPiNwAQhe2RtmYTteNcj
lockWidth (tile + 14 + wordWidth) is genuinely integral: fractional
metrics are never enabled on the Graphics2D, so FontMetrics.stringWidth
already equals the true drawn width. Declaring lockWidth as float
asserted sub-pixel precision that doesn't exist, and the int+int+int
addition still ran in int before the float widened it on assignment,
which is exactly the java:S2184 pattern (cast one operand of an
addition, not the whole result afterwards). Switching lockWidth back to
var/int and keeping the float only on `lockWidth / 2f` removes the
pattern honestly instead of relocating it, and leaves the genuine
half-pixel centring fix from the previous commit untouched.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MrnPiNwAQhe2RtmYTteNcj
Sonar flags the loop counter being assigned inside the body. That is deliberate:
i advances by Character.charCount(cp), which is 2 for a surrogate pair and 1
otherwise, so a fixed i++ would split surrogate pairs and corrupt astral-plane
text -- exactly what this run splitter exists to avoid.

Suppressed by rule id rather than a bare // NOSONAR, which would silence every
rule on that line including any future genuine finding. The quality gate already
passed without this; the annotation just stops a known false positive recurring.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MrnPiNwAQhe2RtmYTteNcj
@sonarqubecloud

Copy link
Copy Markdown

@asm0dey
asm0dey merged commit 1d77346 into main Aug 26, 2026
11 checks passed
@asm0dey
asm0dey deleted the feat/social-preview-images branch August 26, 2026 18:13
Sign up for free to 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.

1 participant