Skip to content

Commit b9c74d6

Browse files
authored
fix(cms): stop auto-creating post type templates (CoreBunch#88)
1 parent 100c12d commit b9c74d6

17 files changed

Lines changed: 154 additions & 430 deletions

File tree

docs/features/content-storage.md

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -166,7 +166,6 @@ This is the single source of truth for slug derivation used by all admin write p
166166
| `server/repositories/data/rows/mapper.ts` | Internal: hydrated SELECT builder + `DataRowRow → DataRow` mapper (not part of the public barrel) |
167167
| `server/repositories/data/rows/index.ts` | Barrel for the `rows/` directory |
168168
| `server/repositories/data/publish.ts` | Publish persistence (`persistDataRowPublish` writes `data_row_versions`) + public-route lookups; the orchestration (lock, artefacts, cache bump) is `server/publish/publishRow.ts` |
169-
| `server/publish/templateSeeding.ts` | Seed default entry template for new postType tables (publish layer — it publishes a page row) |
170169
| `server/repositories/data/shared.ts` | Shared helpers: `userRefAt` (typed accessor per prefix — unknown prefix is a compile error), `userRefColumns` / `userRefJoin` (SQL fragment builders — the single source for the four `<prefix>_*` user-ref join columns and LEFT JOIN clauses, spliced verbatim by both `rows/mapper.ts` and `publish.ts`), `UserJoinColumns` (interface for all four `<prefix>_*` column groups — always present via LEFT JOIN, `null` when no user matched) |
171170
| `server/repositories/data/index.ts` | Barrel for the whole `data/` directory |
172171

@@ -228,9 +227,7 @@ For postType rows, `publishDataRow` does the same but incrementally: writes the
228227

229228
See [docs/features/publisher.md](publisher.md) for the full pipeline.
230229

231-
For **post-types**, each `data_table` has a **default entry template** (a `pages` row with `template.target = { kind: 'postTypes', tableSlugs: [table.slug] }`). When you publish a post, the renderer resolves the template chain for the entry route (`resolveTemplateChain`) and renders the merged tree with the post row pushed onto the entry stack as `currentEntry`. Dynamic bindings on the template nodes resolve `currentEntry.title`, `currentEntry.body`, etc.
232-
233-
The entry template is seeded automatically when a new postType `data_table` is created (`backfillDefaultEntryTemplates(db)` at boot for tables that predate the template system). See [docs/features/templates.md](templates.md) for the full template model.
230+
For **post-types**, public row routes require an explicitly authored entry template (a `pages` row with `template.target = { kind: 'postTypes', tableSlugs: [table.slug] }`). When you publish a post, the renderer resolves the template chain for the entry route (`resolveTemplateChain`) and renders the merged tree with the post row pushed onto the entry stack as `currentEntry`. Dynamic bindings on the template nodes resolve `currentEntry.title`, `currentEntry.body`, etc. Without a matching entry template, the published row exists in the CMS but its public detail URL returns 404. See [docs/features/templates.md](templates.md) for the full template model.
234231

235232
### Scheduled publishing
236233

@@ -252,9 +249,10 @@ The field appears in the postType's edit form and is queryable from loops.
252249

253250
1. Open the Data workspace.
254251
2. Create a new `data_table` with `kind: 'postType'`.
255-
3. The system seeds the built-in fields (`title`, `slug`, `body`, `featuredMedia`, `seoTitle`, `seoDescription`) and a default entry template.
252+
3. The system seeds the built-in fields (`title`, `slug`, `body`, `featuredMedia`, `seoTitle`, `seoDescription`).
256253
4. Add custom fields as needed.
257254
5. Add posts via the Content workspace.
255+
6. Create a post-type template in the Site workspace if the collection needs public detail pages.
258256

259257
### Create a "data" table (no workflow)
260258

@@ -351,7 +349,7 @@ Events are emitted from `server/publish/contentEvents.ts`, which also exports `a
351349
- `server/repositories/data/``tables.ts`, `rows/` (split by responsibility), `publish.ts`, `shared.ts`
352350
- `server/handlers/cms/data/` — generic data endpoints
353351
- `server/handlers/cms/pages.ts`, `components.ts`, `layouts.ts` — typed endpoints for the system tables
354-
- `server/publish/publishRow.ts` — per-row publish orchestration; `server/publish/templateSeeding.ts` — default entry-template seeding
352+
- `server/publish/publishRow.ts` — per-row publish orchestration
355353
- `server/publish/publishScheduler.ts` — scheduled-publish tick
356354
- Gate tests:
357355
- `src/__tests__/architecture/data-tables-system-flag.test.ts`

docs/features/templates.md

Lines changed: 10 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,6 @@ src/core/templates/
3232
└── tokenInterpolation.ts — parseTokenString, interpolateTokens, walkFieldPath
3333
3434
src/modules/base/outlet/ — base.outlet module (Content Outlet)
35-
server/publish/templateSeeding.ts — seed + backfill for default entry templates
3635
server/publish/publicRouter.ts — isTemplatePage guard on direct slug routing
3736
server/publish/publicRenderer.ts — chain-aware render paths
3837
```
@@ -318,14 +317,14 @@ Store action: `convertTemplateToPage(pageId)` in `siteSlice`.
318317

319318
---
320319

321-
## Seeding — default entry templates
320+
## Entry templates are explicit
322321

323-
When a postType `data_table` is created, `ensureDefaultEntryTemplate(db, table)` in `server/publish/templateSeeding.ts` inserts a default template page (idempotent — it no-ops if one already targets the table):
322+
Creating a postType `data_table` does not create a page. Entry templates are ordinary pages that users create, convert, and delete through the Site workspace. A published row detail URL (`/<route-base>/<row-slug>`) renders only when a matching template exists:
324323

325324
- `templateEnabled: true`, `templateTarget: { kind: 'postTypes', tableSlugs: [table.slug] }`, `templatePriority: 0`
326-
- Page tree: `base.body` > `base.text` (`<h1>` bound to `currentEntry.title` via token interpolation) + `base.outlet` (bound to `currentEntry.body` via `html` format)
325+
- Page tree: any authored layout with a `base.outlet` where the row body should flow
327326

328-
`backfillDefaultEntryTemplates(db)` at boot covers postType tables created before the template system was added.
327+
Without a matching entry template, the row remains publishable CMS content but its public detail URL returns 404.
329328

330329
---
331330

@@ -340,12 +339,11 @@ When a postType `data_table` is created, `ensureDefaultEntryTemplate(db, table)`
340339

341340
### Add an entry template for a postType
342341

343-
When a postType is created, the system seeds a default entry template automatically. To customize:
344-
345-
1. Open the template page in the visual editor.
346-
2. Edit it like any page — bind nodes to `currentEntry.<field>` via the Properties panel.
347-
3. Add `base.outlet` anywhere you want the post body to flow.
348-
4. Publish.
342+
1. Create a new page or choose an existing page in the visual editor.
343+
2. Open Template settings and choose **Post types**.
344+
3. Select the target collection slug(s).
345+
4. Build the layout — bind nodes to `currentEntry.<field>` via the Properties panel, and add `base.outlet` where the post body should flow.
346+
5. Publish.
349347

350348
### Share a layout across post types
351349

@@ -384,7 +382,7 @@ node.props.text = 'Posted by {currentEntry.author.displayName} on {currentEntry.
384382
|---------|------------|
385383
| Reading `currentEntry` from a module's `render` without bindings | Set `dynamicBindings` on the node — keeps the schema honest |
386384
| Hardcoding a template's slug in server handlers | Use `resolveTemplateChain(site, ctx)` |
387-
| Creating a template page via raw `INSERT INTO pages` | Use `ensureDefaultEntryTemplate(...)` or the admin dialog |
385+
| Creating a template page via raw `INSERT INTO pages` | Use the Site workspace template dialog |
388386
| Walking a deep binding path with `JSON.parse(JSON.stringify(...))` | Use `walkFieldPath(frame, 'a.b.c')` |
389387
| Expecting to visit a template page at its own slug | Template pages are never directly routable — the live router and bake loop both skip them |
390388
| Inlining `page.template?.target.kind === 'everywhere' ? … : …` in UI code | Use `templateTargetLabel(page)` from `@core/templates` |
@@ -411,7 +409,6 @@ node.props.text = 'Posted by {currentEntry.author.displayName} on {currentEntry.
411409
- `src/admin/pages/site/hooks/useTemplatePreviewContext.ts` — synthetic preview context for the canvas
412410
- `src/admin/pages/site/hooks/useActiveLivePath.ts` — resolves the toolbar "Open live page" path for templates
413411
- `src/core/templates/templatePreviewData.ts``buildPreviewCells`, `dataTablePreviewToLoopItem`
414-
- `server/publish/templateSeeding.ts` — default-template seeding
415412
- `server/publish/publicRenderer.ts` — chain-aware render paths
416413
- `src/admin/pages/site/hooks/useInsertModule.ts` — hook-level outlet guard (toast + null return)
417414
- `src/admin/pages/site/store/slices/site/nodeActions.ts` — store-level outlet backstop in `insertNode`

docs/server.md

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,6 @@ server/index.ts
3535
│ (selects migrations-pg.ts OR migrations-sqlite.ts based on dialect)
3636
3737
├─→ syncSystemRoles(db) ← force-resets Owner capabilities every boot
38-
├─→ backfillDefaultEntryTemplates(db) ← ensures every postType table has a default entry tpl
3938
├─→ mediaStorageRegistry.configureLocalDisk({ uploadsDir }) ← register local-disk media adapter
4039
├─→ activateInstalledServerPlugins(db, uploadsDir) ← run plugin lifecycle: activate
4140
@@ -511,7 +510,6 @@ Server-side publishing helpers live in `server/publish/`:
511510
| `holeRuntime.ts` | Layer C client-side runtime (~668 B). Exports `runInstaticHoleRuntime` (TS source) and `HOLE_RUNTIME_JS` (IIFE-serialized for browser delivery). |
512511
| `publishSite.ts` | Full-site publish orchestrator (`publishDraftSite`): phase-1 builds, the short `persistSitePublish` transaction, Layer A bake + slot swap, Layer B bump. |
513512
| `publishRow.ts` | Per-row publish orchestrator (`publishDataRow`) + `removeDataRowArtefact`: persist via the data repository, in-place artefact update, Layer B bump. |
514-
| `templateSeeding.ts` | Default entry-template seeding for postType tables (creates + publishes a page row; called by table-creation entry points, boot, and post-import backfill). |
515513
| `publicRenderer.ts` | `renderPublishedSnapshot`, `renderPublishedDataRowTemplate` — snapshot-aware wrappers around `publishPage`. |
516514
| `publishedHtmlPipeline.ts` | Plugin frontend-asset injection + `publish.html` filter chain. Runs at publish time for every baked page (complete doc or hole shell); also runs in the Layer B factory for query-string / live renders (cached). |
517515
| `siteCssBundle.ts` | Per-site reset / framework / style CSS bundles (hashed filenames). |

server/handlers/cms/data/tables.ts

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,6 @@ import {
3535
createDataRow,
3636
listDataRows,
3737
} from '../../../repositories/data'
38-
import { ensureDefaultEntryTemplate } from '../../../publish/templateSeeding'
3938
import { normalizeDataTableFields } from '@core/data/fields'
4039
import { slugForTable } from '@core/data/cells'
4140
import { slugFromTitle } from '@core/utils/slug'
@@ -221,9 +220,6 @@ async function handleTablesCollection(req: Request, db: DbClient): Promise<Respo
221220
createdByUserId: user.id,
222221
updatedByUserId: user.id,
223222
})
224-
// postType tables need a default entry template so the public route
225-
// `/<route-base>/<row-slug>` resolves immediately (no-op for `data` kind).
226-
await ensureDefaultEntryTemplate(db, table, user.id)
227223
await recordTableAuditEvent(db, user, req, 'data.table.create', table)
228224
return jsonResponse({ table }, { status: 201 })
229225
}

server/handlers/cms/import.ts

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,6 @@ import {
5252
importDataRowRedirect,
5353
} from '../../repositories/data/publish'
5454
import { deleteAllMediaFolders, importMediaFolder } from '../../repositories/mediaFolders'
55-
import { backfillDefaultEntryTemplates } from '../../publish/templateSeeding'
5655
import { jsonResponse, readValidatedBody } from '../../http'
5756
import { parseValue } from '@core/utils/typeboxHelpers'
5857
import {
@@ -361,16 +360,6 @@ export async function handleImportRoute(
361360
})
362361
}
363362

364-
// ---------------------------------------------------------------------------
365-
// Default entry templates — AFTER the transaction commits
366-
// ---------------------------------------------------------------------------
367-
// Seeding publishes a page row (publish lock + cache bump), so it must not
368-
// run inside the import transaction. Running the idempotent backfill after
369-
// the rows have landed also means a bundle that carries its own entry
370-
// templates is left alone — only postType tables that arrived without one
371-
// get the default seed.
372-
await backfillDefaultEntryTemplates(db)
373-
374363
// ---------------------------------------------------------------------------
375364
// Media — outside the DB transaction (filesystem writes)
376365
// ---------------------------------------------------------------------------

server/index.ts

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import { createDbClient } from './db'
22
import { runMigrations } from './db/runMigrations'
33
import { syncSystemRoles } from './repositories/roles'
4-
import { backfillDefaultEntryTemplates } from './publish/templateSeeding'
54
import { readServerConfig } from './config'
65
import { DEV_ORIGIN_ALLOWLIST, configurePublicOrigins, configureTrustedProxyCidrs, stampSocketIp } from './auth/security'
76
import { startConversationPurgeTick } from './ai/boot'
@@ -21,12 +20,6 @@ await runMigrations(db, migrations)
2120
// installations don't strand owners on a stale grant list when new
2221
// capabilities are added in code. See `syncSystemRoles` for the policy.
2322
await syncSystemRoles(db)
24-
// Every postType data table needs a default entry template so the public
25-
// route `/<route-base>/<row-slug>` resolves. Table-creation entry points
26-
// seed one right after `createDataTable`; this backfill covers the
27-
// baseline-seeded `posts` table and any postType row that pre-dates the
28-
// seeding feature.
29-
await backfillDefaultEntryTemplates(db)
3023
// Wire the built-in local-disk media adapter BEFORE plugins activate —
3124
// plugin adapters register through the same registry but local-disk is
3225
// always the fallback for unset roles. See `mediaStorageRegistry.ts`.

server/plugins/host/handlers/content.ts

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,6 @@ import {
4343
scheduleDataRowPublish,
4444
} from '../../../repositories/data'
4545
import { publishDataRow } from '../../../publish/publishRow'
46-
import { ensureDefaultEntryTemplate } from '../../../publish/templateSeeding'
4746
import { republishAllPages } from '../../../publish/republish'
4847
import { bumpPublishVersionSerialized } from '../../../publish/publishState'
4948
import { applyContentEntryCellsFilter } from '../../../publish/contentEvents'
@@ -181,9 +180,6 @@ export async function handleContentTablesCreate(
181180
primaryFieldId: input.primaryFieldId ?? 'title',
182181
fields,
183182
})
184-
// postType tables need a default entry template so the public route
185-
// `/<route-base>/<row-slug>` resolves immediately (no-op for `data` kind).
186-
await ensureDefaultEntryTemplate(db, created, null)
187183
const slugLookup = await buildTableSlugLookup(db)
188184
replyApiOk(msg.pluginId, msg.correlationId, tableSchema(created, 0, slugLookup))
189185
}

server/publish/__tests__/templateSeeding.test.ts

Lines changed: 0 additions & 27 deletions
This file was deleted.

server/publish/publicRouter.ts

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -140,9 +140,9 @@ type PublicRouteResolution =
140140
* matches the pre-unification routing order (`tryServePublishedPage`
141141
* ran before `tryServeContentRoute` in the dispatcher).
142142
*
143-
* The row path also needs the site snapshot to find the entry
144-
* template; when there isn't one (corrupt install / nothing published),
145-
* we return `not-found` rather than inventing a fallback document.
143+
* The row path also needs the site snapshot to find explicitly authored entry
144+
* templates; when there isn't one, we return `not-found` rather than inventing
145+
* a fallback document.
146146
*/
147147
async function resolvePublicRoute(
148148
db: DbClient,
@@ -166,12 +166,11 @@ async function resolvePublicRoute(
166166

167167
const row = await getPublishedDataRowByRoute(db, route.tableRouteBase, route.rowSlug)
168168
if (row) {
169-
// Every postType table has a default entry template auto-seeded
170-
// into the `pages` table on creation (and the boot backfill catches
171-
// any pre-existing table that's missing one). So a missing
172-
// siteSnapshot here means a corrupt install — surface that as
173-
// not-found rather than half-rendering. The snapshot is memoised per
174-
// publish version, so warm row requests skip the full-site parse.
169+
// Row routes render through explicitly authored entry templates. A missing
170+
// site snapshot means there is no published template surface to consult, so
171+
// surface that as not-found rather than inventing a fallback document. The
172+
// snapshot is memoised per publish version, so warm row requests skip the
173+
// full-site parse.
175174
const siteSnapshot = await getLatestSnapshotForVersion(db, getPublishVersion())
176175
if (!siteSnapshot) return { kind: 'not-found' }
177176
return { kind: 'row', snapshot: siteSnapshot, row }

0 commit comments

Comments
 (0)