fix(core): hydrate byline avatar LQIP placeholder fields (#1457)#1553
Conversation
…1457) Content byline credits fold in the avatar's storage key and alt via the media join, but dropped the blurhash/dominant_color LQIP columns, so author avatars couldn't render a low-quality placeholder despite the media row storing one. Carry m.blurhash/m.dominant_color through the same media join in getContentBylines, getContentBylinesMany, and findByUserIds, surface them as BylineSummary.avatarBlurhash/avatarDominantColor (and in the byline API response schema), and default to null on finders that don't join media. Purely additive, mirroring the existing avatarStorageKey pattern. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
🦋 Changeset detectedLatest commit: ae0fd77 The changes in this PR will be included in the next version bump. This PR includes changesets to release 16 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
There was a problem hiding this comment.
Approach: This is the right change, solving the right problem, in a way that fits EmDash. The byline avatar media LEFT JOIN already folded storage_key/alt into BylineSummary but dropped the LQIP columns (blurhash/dominant_color) that migration 024 added to media. The fix carries those two columns through the same existing joins in all three byline hydration paths (getContentBylines, getContentBylinesMany, findByUserIds), surfaces them as optional BylineSummary.avatarBlurhash/avatarDominantColor, exposes them in the byline API response schema, and defaults them to null on the plain finders — mirroring the established avatarStorageKey/avatarAlt semantics. It's purely additive: no new join, no extra round-trip, no new query, optional fields + nullish() schema + patch bump = backwards-compatible. The changeset is well-written and describes the observable effect for an upgrader. Good taste.
What I checked:
- Migration
024_media_placeholders.tsaddsblurhash/dominant_colortomedia, anddatabase/types.tsdeclares them — so them.blurhash/m.dominant_colorselects typecheck and run on both SQLite and PG. - All three avatar-media joins in
byline.ts(lines ~933, ~1072, ~1181) are updated identically;rgconfirms no other avatar-media joins exist elsewhere, so there's nothing left inconsistent. ContentItem.byline/entry.data.byline(content handleritem.byline = explicit[0]?.bylineandquery.tsdata.byline = credits[0]?.byline) both derive from these same repository paths, so the LQIP fields propagate to every byline consumer — there is no separate primary-byline media join that this PR would have missed.rowToBylineusesrow.avatar_blurhash ?? null, and the plain finders useselectAll()on_emdash_bylines(no media join), so the new fields come back asnullthere — the "null on plain finders" contract holds.- The new selects are static string literals (no interpolation) — SQL-safe; no content-table (
ec_*) queries are added, so no locale-filter concern; no new indexes needed. - Tests cover the single-entry, batch, and
findByUserIdshydration paths asserting the LQIP values are present, plus null-when-no-avatar on the join paths. TheskipHydrationpaths share the same select list and both route throughrowToByline, so the default-path coverage implies the skip path for these fields.
Conclusion: Implementation is correct, complete, and consistent. The only thing I'd touch is a minor test-coverage nicety: the plain-finder null-contract test was extended for avatarStorageKey but not for the two new fields it now also applies to. Not blocking.
Findings
-
[suggestion]
packages/core/tests/unit/database/repositories/byline.test.ts:279This test locks in the contract that plain finders (
findById,findBySlug, …) don't joinmediaand returnnullfor avatar-derived fields. This PR extends that exact contract toavatarBlurhash/avatarDominantColor— androwToByline's?? nulldefault guarantees it, sincefindByIdusesselectAll()on_emdash_bylineswith no media join (soavatar_blurhash/avatar_dominant_colorareundefinedon the row). But the assertion here was only extended to coveravatarStorageKey, not the two new fields. The join-path tests above cover presence; this is the one place that covers absence for the plain finders, so it's worth extending for parity.expect(byId?.avatarStorageKey).toBeNull(); expect(byId?.avatarBlurhash).toBeNull(); expect(byId?.avatarDominantColor).toBeNull();
@emdash-cms/admin
@emdash-cms/auth
@emdash-cms/auth-atproto
@emdash-cms/blocks
@emdash-cms/cloudflare
@emdash-cms/contentful-to-portable-text
emdash
create-emdash
@emdash-cms/gutenberg-to-portable-text
@emdash-cms/plugin-cli
@emdash-cms/plugin-types
@emdash-cms/registry-client
@emdash-cms/registry-lexicons
@emdash-cms/sandbox-workerd
@emdash-cms/x402
@emdash-cms/plugin-ai-moderation
@emdash-cms/plugin-atproto
@emdash-cms/plugin-audit-log
@emdash-cms/plugin-color
@emdash-cms/plugin-embeds
@emdash-cms/plugin-field-kit
@emdash-cms/plugin-forms
@emdash-cms/plugin-webhook-notifier
commit: |
What does this PR do?
Content byline credits fold the avatar's storage key and alt into the result via the
mediaLEFT JOIN, but dropped the LQIP placeholder columns (blurhash/dominant_color) that themediatable stores (migration 024). So an author avatar couldn't render a low-quality image placeholder while loading, even though every other media consumer can.This carries
m.blurhash/m.dominant_colorthrough the same media join ingetContentBylines,getContentBylinesMany, andfindByUserIds, surfaces them asBylineSummary.avatarBlurhash/avatarDominantColor(and in the byline API response schema), and defaults them tonullon finders that don't join media — mirroring the existingavatarStorageKey/avatarAltsemantics. Purely additive: no new join, no extra round-trip, existingBylineSummaryliterals stay source-compatible.Closes #1457
Type of change
Checklist
pnpm typecheckpassespnpm lintpassespnpm testpasses (targeted:byline.test.ts+ full integration suite)pnpm formathas been runAI-generated code disclosure
Screenshots / test output
byline.test.ts: 64 passed. Targeted unit + integration suites green (only pre-existing Windows-only baseline failures unrelated to this change).