You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
There is no way for a frontend to enumerate bylines. The public API offers only identifier-keyed lookups — getByline(id), getBylineBySlug(slug) — plus getEntriesByByline(), all of which require a byline you already have.
The only site-wide enumeration in the codebase is BylineRepository.findMany() (byline.ts:549), reachable solely via GET /_emdash/api/admin/bylines, which requires bylines:read, a session, and the CSRF header. BylineRepository is not exported from the package root or any subpath.
So the only thing site code can do today is query a content collection and dedupe entry.data.bylines. That drives the read from _emdash_content_bylines — and from a content-table query before it — to reach data that lives in _emdash_bylines. Splitting the repository's reads by driving table makes the shape of the gap clear:
Byline-driven:findById:506, findBySlug:519, findByUserId:532, findByTranslationGroup:620, findByUserIds:1187 — all identifier-keyed — and findMany:549, which is admin-gated
Every byline read available to a frontend that returns more than one row starts from the credit table.
Motivation
Author index pages, contributor lists, author pickers in site-side search, and sitemaps all want "the bylines on this site". Deriving that from content is wrong on three counts:
Wrong driving table. Reaching _emdash_bylines through a join on _emdash_content_bylines (itself reached through a content query) to read columns that sit on the byline row.
The result set is a function of which entries you fetched. It is bounded by the content query's limit/cursor, so the byline list is incomplete by construction and changes as you paginate.
It silently omits bylines with no resolvable credits — a guest author added ahead of publication, an author whose posts are all draft or scheduled, an author whose entries exist only in another locale (credit hydration is strict per-locale by design, bylines/index.ts:12).
Bylines are also the only content-adjacent system without a list function. Taxonomies have getTaxonomyTerms, menus getMenus, sections getSections, widgets getWidgetAreas.
Backed by a new byline-table-driven repository read:
SELECTb.id, b.slug, b.display_name, b.bio, b.avatar_media_id, b.website_url,
b.user_id, b.is_guest, b.locale, b.translation_group,
m.storage_key, m.alt, m.blurhash, m.dominant_colorFROM _emdash_bylines b
LEFT JOIN media m ONm.id=b.avatar_media_idWHEREb.locale= ?
ORDER BYb.display_nameASC, b.idASCLIMIT ? +1
Design points:
Media join included.avatar_media_id is an ID, not a URL, and avatarStorageKey is currently populated only on the content-credit path (documented at repositories/types.ts:62: "The plain byline finders … leave it null"). Without the join, every caller rendering avatars does a MediaRepository.findById per byline — an N+1 across exactly the page being built. This is the same LEFT JOIN mediagetContentBylinesMany already performs at :1079.
Strict locale, resolved like getTaxonomyTerms. Rows are per-locale, so filtering on the resolved locale yields one row per person and needs no translation-group dedupe. A per-row fallback chain would multiply queries and return mixed-locale output.
No custom-field hydration.findMany calls withCustomFields unconditionally (:301), costing 2–4 extra queries against the EAV tables from Discussion Custom fields on bylines #1174. A list of names and avatars does not need them. customFields comes back {}, matching the existing skipHydration behaviour. Adding includeCustomFields: true later is additive.
Alphabetical, cursor-paginated.findMany orders by created_at DESC, which is wrong for an author index. Cursor keyed on (display_name, id) via the existing encodeCursor/decodeCursor. Returns { items, nextCursor? } per the pagination convention.
Wrapped in requestCached, keyed on every argument.
Cost: one query. Opt-in, so no existing route's query count moves and the snapshots are unaffected.
Index behaviour
_emdash_bylines has single-column indexes on display_name and locale (migration 040), but no composite. EXPLAIN QUERY PLAN for the query above:
|--SEARCH b USING INDEX idx__emdash_bylines_locale (locale=?)
|--SEARCH m USING INDEX sqlite_autoindex_media_1 (id=?) LEFT-JOIN
`--USE TEMP B-TREE FOR ORDER BY
The locale filter is indexed; the ordering is not, so each page sorts the whole locale partition before LIMIT applies and the cursor is not a true seek. This is index-availability only — it does not depend on sqlite_stat1.
No migration proposed. At the byline counts real sites carry, sorting the partition is negligible, and a forward-only index migration is easy to add later if that stops being true. A (locale, display_name) composite removes the temp B-tree and makes the cursor seek-based; that belongs with #1532, which covers the same full-scan concern across taxonomy terms, bylines, users, and media.
Out of scope
No migration, no schema change, no change to existing behaviour or exports. Not a breaking change.
Alternatives considered
Export BylineRepository. Exposes create/update/delete to site code, and findMany's unconditional custom-field hydration plus missing media join make it the wrong shape regardless.
Document the getDb() escape hatch from emdash/runtime. Pushes locale resolution, translation-group semantics, and the media join onto every site, and puts internal table shapes into site code.
Keep deriving from content. The three problems under Motivation.
Deliberately not included
search and isGuest filters, an includeCustomFields flag, and adding the media join to findMany so the admin list stops N+1-ing avatars. Each is additive and none is needed by the use case that motivates this; the last is a separate change to an admin path and shouldn't ride along.
reacted with thumbs up emoji reacted with thumbs down emoji reacted with laugh emoji reacted with hooray emoji reacted with confused emoji reacted with heart emoji reacted with rocket emoji reacted with eyes emoji
Uh oh!
There was an error while loading. Please reload this page.
Problem
There is no way for a frontend to enumerate bylines. The public API offers only identifier-keyed lookups —
getByline(id),getBylineBySlug(slug)— plusgetEntriesByByline(), all of which require a byline you already have.The only site-wide enumeration in the codebase is
BylineRepository.findMany()(byline.ts:549), reachable solely viaGET /_emdash/api/admin/bylines, which requiresbylines:read, a session, and the CSRF header.BylineRepositoryis not exported from the package root or any subpath.So the only thing site code can do today is query a content collection and dedupe
entry.data.bylines. That drives the read from_emdash_content_bylines— and from a content-table query before it — to reach data that lives in_emdash_bylines. Splitting the repository's reads by driving table makes the shape of the gap clear:getContentBylines:938,getContentBylinesMany:1077,hasContentBylines(Many):1018/:1038,copyContentBylines:1255findById:506,findBySlug:519,findByUserId:532,findByTranslationGroup:620,findByUserIds:1187— all identifier-keyed — andfindMany:549, which is admin-gatedEvery byline read available to a frontend that returns more than one row starts from the credit table.
Motivation
Author index pages, contributor lists, author pickers in site-side search, and sitemaps all want "the bylines on this site". Deriving that from content is wrong on three counts:
_emdash_bylinesthrough a join on_emdash_content_bylines(itself reached through a content query) to read columns that sit on the byline row.limit/cursor, so the byline list is incomplete by construction and changes as you paginate.bylines/index.ts:12).Bylines are also the only content-adjacent system without a list function. Taxonomies have
getTaxonomyTerms, menusgetMenus, sectionsgetSections, widgetsgetWidgetAreas.Proposed solution
Export one function from
emdash:Backed by a new byline-table-driven repository read:
Design points:
avatar_media_idis an ID, not a URL, andavatarStorageKeyis currently populated only on the content-credit path (documented atrepositories/types.ts:62: "The plain byline finders … leave it null"). Without the join, every caller rendering avatars does aMediaRepository.findByIdper byline — an N+1 across exactly the page being built. This is the sameLEFT JOIN mediagetContentBylinesManyalready performs at:1079.getTaxonomyTerms. Rows are per-locale, so filtering on the resolved locale yields one row per person and needs no translation-group dedupe. A per-row fallback chain would multiply queries and return mixed-locale output.findManycallswithCustomFieldsunconditionally (:301), costing 2–4 extra queries against the EAV tables from Discussion Custom fields on bylines #1174. A list of names and avatars does not need them.customFieldscomes back{}, matching the existingskipHydrationbehaviour. AddingincludeCustomFields: truelater is additive.findManyorders bycreated_at DESC, which is wrong for an author index. Cursor keyed on(display_name, id)via the existingencodeCursor/decodeCursor. Returns{ items, nextCursor? }per the pagination convention.requestCached, keyed on every argument.Cost: one query. Opt-in, so no existing route's query count moves and the snapshots are unaffected.
Index behaviour
_emdash_bylineshas single-column indexes ondisplay_nameandlocale(migration040), but no composite.EXPLAIN QUERY PLANfor the query above:The locale filter is indexed; the ordering is not, so each page sorts the whole locale partition before
LIMITapplies and the cursor is not a true seek. This is index-availability only — it does not depend onsqlite_stat1.No migration proposed. At the byline counts real sites carry, sorting the partition is negligible, and a forward-only index migration is easy to add later if that stops being true. A
(locale, display_name)composite removes the temp B-tree and makes the cursor seek-based; that belongs with #1532, which covers the same full-scan concern across taxonomy terms, bylines, users, and media.Out of scope
No migration, no schema change, no change to existing behaviour or exports. Not a breaking change.
Alternatives considered
BylineRepository. Exposescreate/update/deleteto site code, andfindMany's unconditional custom-field hydration plus missing media join make it the wrong shape regardless.getDb()escape hatch fromemdash/runtime. Pushes locale resolution, translation-group semantics, and the media join onto every site, and puts internal table shapes into site code.Deliberately not included
searchandisGuestfilters, anincludeCustomFieldsflag, and adding the media join tofindManyso the admin list stops N+1-ing avatars. Each is additive and none is needed by the use case that motivates this; the last is a separate change to an admin path and shouldn't ride along.All reactions