feat(editor): page templates — live whole-page embed (MVP) #17
Reference in New Issue
Block a user
Delete Branch "feat/page-templates"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Implements
docs/page-templates-plan.md(MVP, Variant A — a separatepageEmbednode).What
Embed another page's live content into a host page — when the source changes, every embed updates (a live read-only view, not a static copy). A page can be flagged a template so it surfaces in the insert picker; any accessible page can be embedded.
How
Server
pages.is_template(+ partial index) andpage_template_references(whole-page back-refs).db.d.ts/entity types hand-merged (this repo curatesdb.d.ts).POST /pages/toggle-template(CASL Edit) flips the flag;is_templateis returned byfindByIdand the sidebar tree select so the tree menu label reflects state. Search suggestions gainonlyTemplatesfor the picker.POST /pages/template/lookup({sourcePageIds[]}, ≤50): returns each accessible source's{title, icon, slugId, content, sourceUpdatedAt}withcommentmarks stripped — using the same access path as block transclusion (filterViewerAccessiblePageIds: workspace + space membership + page permissions). Inaccessible →no_access, missing →not_found, error →not_found(never raw content).collectPageEmbedsFromPmJson+syncPageTemplateReferences) on the Yjs save hook;duplicatePageremapspageEmbed.sourcePageIdand inserts refs.Client
pageEmbednode (editor-ext), registered in both client and server schemas (or the server strips it). Read-only NodeView with a batching lookup;/Embed pageslash + a template picker (self-embed prevented);Make/Unset templatein the tree node menu.Reasoning / decisions
transclusionReference) — avoids polluting the working block-transclusion invariants/UNIQUE constraint with a whole-page sentinel; reuses its renderer/access helpers.is_template— the flag is for picker discovery only, so removing the flag later doesn't break existing embeds.Review findings & fixes
Review confirmed no access leak —
/pages/template/lookupuses the identical access checks as the trusted transclusion lookup (tried cross-space / restricted / cross-workspace ids — all filtered, workspace re-applied at the repo). Cycle guard, workspace-scoped refs, additive migrations all verified. Fixes applied:is_template, so the tree menu was stuck on "Make template" (couldn't un-template after reload) → addedisTemplateto the tree select +buildTree.not_found.slugId(added to the lookup response) viabuildPageUrlinstead of a raw UUID.Verification
pnpm --filter @docmost/editor-ext build+pnpm --filter server build+pnpm --filter client build— clean.page-embed.util.spec(collect/dedup + HTML↔JSON round-trip through the server schema),page-template-lookup.spec(access mapping: no_access/not_found/content+comment-strip) — pass.pageEmbedvia the picker into a host page → host showed the source's live content; editing the source then reloading the host showed the update (live, not a snapshot); an A↔B cycle rendered the "circular embed" placeholder with no hang/crash; self-embed not offered. No real app errors. Screenshots captured.🤖 Generated with Claude Code
Embed another page's LIVE content into a host page (it updates when the source changes, not a static copy). A page can be flagged a template for discovery in the picker; any accessible page can be embedded. Server: - migrations: pages.is_template (+ partial index) and page_template_references (whole-page back-refs); db.d.ts/entity types hand-merged (db.d.ts is curated). - POST /pages/toggle-template (CASL Edit) flips is_template; is_template is returned by findById + the sidebar tree select so the tree menu label reflects state. Search suggestions gain an onlyTemplates filter for the picker. - POST /pages/template/lookup ({sourcePageIds[]}, <=50): returns each accessible source's {title, icon, slugId, content, sourceUpdatedAt} with comment marks stripped (same access path as transclusion: filterViewerAccessiblePageIds; inaccessible -> no_access, missing -> not_found; error path -> not_found, never raw content). - reference sync (collectPageEmbedsFromPmJson + syncPageTemplateReferences) on the Yjs save hook; duplicatePage remaps pageEmbed.sourcePageId + inserts refs. Known MVP gap: REST content updates don't resync refs (lookup uses in-doc ids). Client: - pageEmbed node (editor-ext, registered in BOTH client + server schemas); read-only NodeView with a batching lookup; '/Embed page' slash + template picker (self-embed prevented); 'Make/Unset template' in the tree node menu. - Cycle guard: an ancestry-chain context + depth cap (5) render a 'circular embed' placeholder instead of recursing. - Public shares show a placeholder (no public lookup in MVP). MVP excludes (follow-ups): public-share lookup, unsync->static copy, server-side expansion for export/RAG, MCP schema mirror, point-in-time snapshots. Implements docs/page-templates-plan.md (MVP, variant A). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>