v2.5.0
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 throughdocument-lifecycle(createDocument,updateDocument,updateDocumentWithPatches,restoreDocumentVersion,duplicateDocument,copyToLocale), the walker refreshes each internal-link node'sdocument.pathagainst the live target document, setsdocument._resolved = falseon links whose target has been deleted between picker and save, and refreshesdocument.titlefrom the target'suseAsTitlefield. 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 viapopulateRelationsOnRead: trueon the field. The walker is registered via the newlexicalEditorEmbedServer({ getClient })factory onServerConfig.fields.richText.embed, wired into the document-lifecycle pipeline betweenassignCounterValuesandcreateDocumentVersion. Seedocs/RICHTEXT.md → Relations — embed and populatefor the full strategy. -
@byline/core— New optionalbuildDocumentPathhook onCollectionDefinition. Composes a renderable root-relative path for a document from its persisted slug plus any contextual fields (e.g. apagescollection might return/about/<slug>for documents witharea: '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 returnsnullto 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.urlcan delegate to it so the admin Preview button and the public renderer produce identical paths by construction (seeapps/webapp/byline/collections/pagesfor the worked example). -
@byline/richtext-lexical— Renderer fallback chain for internal-link nodes. The host serializer now readsdocument.pathwith 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 forcedtarget="_blank", and only true external URLs get the icon + new-tab treatment. The previous behaviour rendered#anchorlinks with the external-link icon, which was visually wrong for intra-page jumps. The newisLocalHrefhelper sits next togetHrefand is reused bygetAdditionalPropsto keep thetarget="_blank"decision in sync with the render branch.
Migrations
-
@byline/richtext-lexical— Hosts wiring the richtext server adapter must switch from the singlelexicalEditorServer({ getClient })import to the two new factorieslexicalEditorEmbedServerandlexicalEditorPopulateServer, registered asServerConfig.fields.richText.{ embed, populate }. The bootstrap validator now fail-fasts atinitBylineCore()time when any richtext field's effectiveembedRelationsOnSave: 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. Seeapps/webapp/byline/server.config.tsfor the canonical wiring. -
@byline/core— Hosts whose collections have URLs that differ from the generic/${collectionPath}/${slug}pattern should declarebuildDocumentPathon theCollectionDefinition. 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'spagescollection (URLs at root or under anareasegment) is the worked example, and thepreview.urlblock onpages/admin.tsxnow delegates to it so the admin Preview button and the richtext embed walker produce identical paths.
Breaking Changes
-
@byline/richtext-lexical—lexicalEditorServeris no longer exported. Replaced bylexicalEditorEmbedServer(write-time) andlexicalEditorPopulateServer(read-time). Hosts on the previous single-factory export must update theirserver.config.ts. See the Migrations section above for the worked example. -
@byline/richtext-lexical— The client-sideEditorSettings.embedRelationsOnSaveflag has been removed from the editor-settings shape (EditorSettings,EditorSettingsOverride,DEFAULT_EDITOR_SETTINGS). The server-sideRichTextField.embedRelationsOnSaveflag on collection schemas is unchanged and now drives the new server walker. Hosts passingembedRelationsOnSaveinside anEditorSettingsOverride(typically insidelexicalEditor((c) => ...)) will see a TypeScript error — remove the override; the field-level schema flag is the authoritative setting now. -
@byline/richtext-lexical—InternalLinkAttributes(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 legacydoc: { value, relationTo, data: { ... } }field is gone; internal links now carry the flattenedDocumentRelationenvelope (targetDocumentId,targetCollectionId,targetCollectionPath,document?: InternalLinkDocument). The newdocumentpayload 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 atapps/webapp/src/ui/byline/components/link/link-lexical.tsxis the canonical pattern. -
@byline/core—DocumentRelationis 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 forDpreserves the previousRecord<string, any>shape — but consumers can now pin a stricterdocumentpayload per node type.InternalLinkAttributesusesDocumentRelation<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.