Skip to content

v2.5.0

Choose a tag to compare

@58bits 58bits released this 25 May 12:21
· 188 commits to main since this release

Highlights

  • @byline/core, @byline/richtext-lexical — Internal links inside Lexical rich-text fields are now canonicalised by a server-side write-time embed walker. On every save through document-lifecycle (createDocument, updateDocument, updateDocumentWithPatches, restoreDocumentVersion, duplicateDocument, copyToLocale), the walker refreshes each internal-link node's document.path against the live target document, sets document._resolved = false on links whose target has been deleted between picker and save, and refreshes document.title from the target's useAsTitle field. Link envelopes persisted in Lexical JSON now stay accurate as their targets change, instead of drifting until a manual edit triggers a re-save. Reads return the persisted envelope verbatim (snapshot mode is still the default), so the public site never pays per-read populate cost for the link case unless the host opts in explicitly via populateRelationsOnRead: true on the field. The walker is registered via the new lexicalEditorEmbedServer({ getClient }) factory on ServerConfig.fields.richText.embed, wired into the document-lifecycle pipeline between assignCounterValues and createDocumentVersion. See docs/RICHTEXT.md → Relations — embed and populate for the full strategy.

  • @byline/core — New optional buildDocumentPath hook on CollectionDefinition. Composes a renderable root-relative path for a document from its persisted slug plus any contextual fields (e.g. a pages collection might return /about/<slug> for documents with area: 'about' and /<slug> for documents at root). Called server-side by the richtext embed walker on every save and by the read-time populate visitor when populate is enabled. The signature is locale-agnostic — host renderers (LangLink, etc.) prepend the request locale at render time — and the hook returns null to signal "no path available" (brand-new draft, intentionally un-published, etc.). Falls back to /${collectionPath}/${slug} when omitted, so collections whose URL pattern matches the collection name (/news/<slug>, /docs/<slug>) need no per-collection wiring. CollectionAdminConfig.preview.url can delegate to it so the admin Preview button and the public renderer produce identical paths by construction (see apps/webapp/byline/collections/pages for the worked example).

  • @byline/richtext-lexical — Renderer fallback chain for internal-link nodes. The host serializer now reads document.path with a four-step preference: (1) _resolved: false → strip the <a> wrapper, render children as plain text; (2) leading slash → use as-is (canonical, walker has run); (3) bare slug + targetCollectionPath → generic compose /${collectionPath}/${slug} (heal-on-write fallback for legacy data); (4) nothing usable → strip wrapper. The public site never carries a broken <a href="">, deleted-target links render as plain text without losing the editor's link node (so re-linking is possible whenever the editor returns), and legacy data heals on every next save without a data migration. The two-factory shape (lexicalEditorEmbedServer + lexicalEditorPopulateServer) lets hosts wire write-time and read-time pipelines independently — same visitor under the hood, two trigger points.

  • @byline/richtext-lexical — The link serializer now distinguishes three href categories instead of two: router-prefix paths render through <LangLink> as before, in-page anchors (#section) and intra-app schemes (tel:, mailto:) render as a plain <a> without the external-link icon or forced target="_blank", and only true external URLs get the icon + new-tab treatment. The previous behaviour rendered #anchor links with the external-link icon, which was visually wrong for intra-page jumps. The new isLocalHref helper sits next to getHref and is reused by getAdditionalProps to keep the target="_blank" decision in sync with the render branch.

Migrations

  • @byline/richtext-lexical — Hosts wiring the richtext server adapter must switch from the single lexicalEditorServer({ getClient }) import to the two new factories lexicalEditorEmbedServer and lexicalEditorPopulateServer, registered as ServerConfig.fields.richText.{ embed, populate }. The bootstrap validator now fail-fasts at initBylineCore() time when any richtext field's effective embedRelationsOnSave: true (the default) lacks a registered embed adapter — so this is required for any host that ships richtext content. The error message includes the offending field path and points at the right factory. See apps/webapp/byline/server.config.ts for the canonical wiring.

  • @byline/core — Hosts whose collections have URLs that differ from the generic /${collectionPath}/${slug} pattern should declare buildDocumentPath on the CollectionDefinition. Collections whose URL already follows the generic pattern (/news/<slug>, /docs/<slug>) need no change — the embed walker uses the generic compose fallback. The reference installation's pages collection (URLs at root or under an area segment) is the worked example, and the preview.url block on pages/admin.tsx now delegates to it so the admin Preview button and the richtext embed walker produce identical paths.

Breaking Changes

  • @byline/richtext-lexicallexicalEditorServer is no longer exported. Replaced by lexicalEditorEmbedServer (write-time) and lexicalEditorPopulateServer (read-time). Hosts on the previous single-factory export must update their server.config.ts. See the Migrations section above for the worked example.

  • @byline/richtext-lexical — The client-side EditorSettings.embedRelationsOnSave flag has been removed from the editor-settings shape (EditorSettings, EditorSettingsOverride, DEFAULT_EDITOR_SETTINGS). The server-side RichTextField.embedRelationsOnSave flag on collection schemas is unchanged and now drives the new server walker. Hosts passing embedRelationsOnSave inside an EditorSettingsOverride (typically inside lexicalEditor((c) => ...)) will see a TypeScript error — remove the override; the field-level schema flag is the authoritative setting now.

  • @byline/richtext-lexicalInternalLinkAttributes (the persisted Lexical-node shape for internal links) is now one member of a discriminated union (LinkAttributes = CustomLinkAttributes | InternalLinkAttributes) rather than a single open interface. The legacy doc: { value, relationTo, data: { ... } } field is gone; internal links now carry the flattened DocumentRelation envelope (targetDocumentId, targetCollectionId, targetCollectionPath, document?: InternalLinkDocument). The new document payload pins { title?, path?, _resolved?: false }. Any host that wrote its own custom link serializer against the legacy shape must adopt the new envelope — the reference renderer at apps/webapp/src/ui/byline/components/link/link-lexical.tsx is the canonical pattern.

  • @byline/coreDocumentRelation is now a generic interface (DocumentRelation<D extends Record<string, any> = Record<string, any>>). Existing code using it without a type argument still compiles — the default for D preserves the previous Record<string, any> shape — but consumers can now pin a stricter document payload per node type. InternalLinkAttributes uses DocumentRelation<InternalLinkDocument>; the inline-image node continues with the loose default.

All other @byline/* packages bumped to 2.5.0 in lockstep with no behavioural changes this cycle.