Adding a page type
A page type decides which tabs the page editor shows and how the public site renders the page.
Today there are six: info, posts, gallery, people, library (creatable in the admin) and
system (seeded: the home page and the contact page). Before adding a seventh, check whether an
existing type with a new tab would do.
1. The database: nothing to do
Section titled “1. The database: nothing to do”Migration 0005-page-types.sql added pages.type and pages.status with CHECK constraints
listing every value, which made a new type a rebuild of pages — a table eight others reference.
Migration 0019-pages-rebuild.sql (review D-01) rebuilt pages once, with the runner’s
-- @rebuild mode, without those two CHECKs. The database now accepts any text in both
columns; the lists live in one place, src/config/pageTypes.js:
PAGE_TYPES— every type, andPAGE_STATUSES—draft,published;assertPageTypeAndStatus({ type, status })— called by every write path that sets either column (src/admin/routes/pages/,src/lib/siteTemplate.js,src/admin/lib/convertSection.js); a value not in the lists throws. A new write path must call it too.
So adding the key to PAGE_TYPES (step 2) is the whole database side. test/dbRebuild.test.js
checks both halves: the table takes a new value, and the helper takes exactly what the config
lists.
2. Configuration: src/config/pageTypes.js
Section titled “2. Configuration: src/config/pageTypes.js”PAGE_TYPES— add{ label, hint, tabs }.labelandhintare Ukrainian (they are the cards on «Нова сторінка»);tabslists the editor tabs in order, usually ending with'settings'.CREATABLE_TYPES— add the key if admins may create such a page.TAB_LABELS— a Ukrainian label for any new tab.tabsFor(page)— only if the type needs a per-page exception (ascontacty, library sections and the home page have).RESERVED_SLUGS— if the type introduces a public path segment of its own.
3. Admin: the editor tab
Section titled “3. Admin: the editor tab”src/admin/views/pages/editor.ejsincludesviews/pages/tabs/<activeTab>.ejs; add a file for yours.src/admin/lib/pageEditor.js(createRenderEditor) loads the data a tab needs — load it only whenactiveTabis yours, as the existing tabs do. Add the tab toADMIN_ONLY_TABSif editors must not see it.- Writes go in a file of their own under
src/admin/routes/pages/(registered byroutes/pages/index.js, aspeople.jsis) or a router mounted under/pagesinsrc/admin/index.js(ascreateAlbumsRouteris), with their SQL in a store (src/admin/stores/). Verify the page’s type in every lookup (SELECT … WHERE id = ? AND deleted_at IS NULL AND type = '<yours>', as the albums and people stores do). - Permissions: add actions to
ACTIONSinsrc/admin/lib/permissions.jsand guard routes withrequireCan(); hide buttons withcan(). - A multipart form (any upload) is added with
postMultipart(router, path, multipartRoute(upload, { onInvalidFile }, handler))(src/admin/lib/multipart.js): the wrapper checks the CSRF token after multer, removes the temp files and turns a rejected file into your 400 re-render;postMultipartregisters the path — the only way the router-level CSRF check lets a multipart request through. - Redirect with
req.flash('success', …)after a successful write — that is what writes the «Журнал змін» entry.
4. Public site
Section titled “4. Public site”src/routes/site.js→typedPageOrNext(slug, next, types): add the type to the list of the/:slug(and, if the type has child pages,/:slug/:childSlug) route and add a branch. A type not handled there falls through to the documents catch-all and renders as a text page with its files, which is sometimes all you need.- A view in
src/views/pages/, rendered withres.renderPage(). Component CSS goes in a newpublic/css/components/*.cssfile added toSITE_STYLESHEETSinsrc/routes/assets.js, which joins it into/site.css(test/assets.test.jsfails for a component file that is not listed);head.ejsand the editor preview’sSITE_STYLESlink/site.cssalready. A new page-specific script is linked from its view, likeshare.js; one every page needs goes intoSITE_SCRIPTS. Every class the CSS defines must be used by a template or a script (test/cssUnused.test.js). - Read data through
SqliteContentRepository— add a method there rather than querying from the route.
5. Everything else that enumerates types or content
Section titled “5. Everything else that enumerates types or content”| Where | Why |
|---|---|
src/admin/stores/news.js → linkablePages |
Types a post may also be shown on («Показувати також на сторінках») |
SqliteContentRepository.getSitemapEntries() |
Whether the type’s child pages belong in sitemap.xml |
SqliteContentRepository.search() + migration triggers |
Whether the type’s content is searchable |
src/lib/fileUsages.js |
Every new column pointing at files, or the delete guard will let an in-use file go |
src/admin/lib/trash.js, purgeTrash.js |
A new soft-deletable entity (deleted_at/deleted_by) |
src/lib/structuredData.js, src/lib/canonical.js |
JSON-LD and the query parameters that are part of a canonical URL |
src/config/siteTemplates/*.json, src/lib/siteTemplate.js |
If the setup wizard’s templates should create such a page |
Backups need nothing: the manifest and the change hash enumerate tables from sqlite_master.
6. Tests and docs
Section titled “6. Tests and docs”- Admin: a test per route in
test/admin/(role checks included — an editor request must get 403 where it should). - Public: rendering, 404 for draft/hidden/recycled pages, CSP (no inline style or script).
- Update
CLAUDE.md(the data model and routing sections), the editor guide page that covers the type (docs/editor/) and add a bullet toCHANGELOG.mdunder «Не випущено».