feat: extensible bylines#1258
Conversation
…ation for admin API
🦋 Changeset detectedLatest commit: e6d45e9 The changes in this PR will be included in the next version bump. This PR includes changesets to release 14 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 |
Co-authored-by: ask-bonk[bot] <249159057+ask-bonk[bot]@users.noreply.github.com>
|
/review |
|
Actually wait — if the delete is the last sibling, and Still worth a comment maybe. But I'm getting into super-nit territory. Let me focus on finalizing my review. Given the prior LGTM! |
Overlapping PRsThis PR modifies files that are also changed by other open PRs:
This may cause merge conflicts or duplicated work. A maintainer will coordinate. |
| const defs = await new BylineSchemaRegistry(db).listFields(); | ||
| holder.cached = defs; |
There was a problem hiding this comment.
The jsdoc says that it caches the promise not the resolved value, but we're awaiting it before assigning. It's better to cache the promise because, as the comment says, it allows us to coalesce concurrent requests to use a single db query.
Merging emdash-cms#1298 (byline avatar storage key hydration) exposed a silent regression: getContentBylines/getContentBylinesMany reshape the joined rows into a minimal BylineRow before withCustomFields, which dropped the avatar_storage_key/avatar_alt columns the new media LEFT JOIN selects, so avatarStorageKey/avatarAlt always came back null. Carry both columns through the reshape (typed BylineRowWithAvatar). findByUserIds passes the joined rows directly and was already correct. Query-count snapshots regenerated for the merged tree.
|
Pushed a merge of Background: #1298 (byline avatar storage key) landed on The fix carries Note: the merged tree shows a uniform +1 query/route from the per-request |
What does this PR do?
Adds support for site-specific byline metadata — Twitter handle, pronouns,
company, localised job title, etc. — via a new
/byline-schemaadminscreen. Sites can register custom fields with their own type, validation,
and per-locale storage policy without editing code; values surface on
BylineSummary.customFieldsfor the frontend.Custom-field values can be set at both create and update time through
the same
customFieldsmap onPOSTandPUT /_emdash/api/admin/bylines.In the admin, registered fields render inline with Name, Bio, etc. — no
"Custom fields" section header — and are available in both the New byline
and Edit byline dialogs. The schema link sits at the top of the
Bylines page (admin-only) rather than the global sidebar so admins find
it in context.
Design choices worth flagging
_emdash_byline_fieldsis thesource of truth, with
options.byline_fields_versiondriving cacheinvalidation. Admins register fields through the admin UI; no code
edits required.
translatableflag. Translatable values are stored perlocale (one row per locale in the byline's
translation_group).Non-translatable values are stored once per
translation_groupandsurface on every locale variant. The flag is locked once values
reference the field — flipping would orphan rows in the wrong table.
urlscheme allowlist. URL fields requirehttp:orhttps:;javascript:/data:/mailto:/ftp:/file:reject. MirrorshttpUrlinapi/schemas/common.ts— custom URLs typically renderas
<a href>so this closes the XSS footgun.write and per-field writes roll back together on partial failure.
transactions, so a crash between the row insert and the field writes
leaves a partial byline that the API would otherwise refuse to
recover from. A retry POST is treated as completing the abandoned
create iff the full fixed-column payload, the translation-group
identity, and the existing custom-field subset all match the
incoming request. Anything else collapses to a standard
duplicate-slug
CONFLICT— recovery only fires when the retry isprovably the same request as the original.
options.byline_fields_versioncarries meaning in its parity — odd = mutation in flight (or
crashed), even = stable. The cache bypasses the global holder while
odd.
markVersionDirtyis parity-aware idempotent;markVersionCleanalways advances to a new even value so two concurrent mutators
can't collapse on the same key and pin a stale snapshot.
Idempotent-retry exits also run
markVersionClean— same code pathdoubles as crash recovery and false-clean recovery.
BylineSummarygains an optionalcustomFields: Record<string, CustomFieldValue>. Existingobject-literal consumers stay source-compatible — the property is
optional and runtime always returns
{}when no fields are registered.Builds on the bylines-i18n foundation from #1146.
What's deferred
requiredflag enforcement. The admin UI exposes a Requiredtoggle and the value persists on the field definition, but the
server accepts missing values and
nullfor any field. Needs adesign call on the enforcement model (reject on first set only?
reject on every save? coerce vs reject?). TODOs are in
BylineRepository.coerceFieldValueandBylineFieldEditor.parity-aware bookend has a residual race between two concurrent
markVersionCleancalls — bounded by the inter-clean duration(~ms). Schema mutations are admin-only and rare; acceptable for
this PR. A CAS-on-bump or dialect-specific row lock would close
it but raises dialect-divergence scope.
multiple conjunctive guards approximating "same request as before"
via state inspection. If similar D1 retry-safety problems recur on
other create endpoints, an explicit
Idempotency-Keyheader wouldreplace the apparatus with one lookup. Worth a Discussion before
the next D1 retry surface comes up.
translationOf-flavoured create recovery. The sibling-localeguard returns
CONFLICTbefore the recovery branch can fire ontranslation creates, so partial
translationOfretries fall throughto manual recovery via the edit flow. Uncommon enough that
reordering the guard wasn't justified for this PR.
Closes #1174
Closes #
Type of change
Checklist
pnpm typecheckpassespnpm lintpassespnpm testpasses (or targeted tests for my change)pnpm formathas been runmessages.pochanges except in translation PRs — a workflow extracts catalogs on merge tomain.AI-generated code disclosure
Screenshots / test output
Screen.Recording.2026-06-01.at.11.26.51.mov