Перейти до вмісту

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.

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, and PAGE_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.

  • PAGE_TYPES — add { label, hint, tabs }. label and hint are Ukrainian (they are the cards on «Нова сторінка»); tabs lists 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 (as contacty, library sections and the home page have).
  • RESERVED_SLUGS — if the type introduces a public path segment of its own.
  • src/admin/views/pages/editor.ejs includes views/pages/tabs/<activeTab>.ejs; add a file for yours.
  • src/admin/lib/pageEditor.js (createRenderEditor) loads the data a tab needs — load it only when activeTab is yours, as the existing tabs do. Add the tab to ADMIN_ONLY_TABS if editors must not see it.
  • Writes go in a file of their own under src/admin/routes/pages/ (registered by routes/pages/index.js, as people.js is) or a router mounted under /pages in src/admin/index.js (as createAlbumsRouter is), 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 ACTIONS in src/admin/lib/permissions.js and guard routes with requireCan(); hide buttons with can().
  • 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; postMultipart registers 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.
  • 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 with res.renderPage(). Component CSS goes in a new public/css/components/*.css file added to SITE_STYLESHEETS in src/routes/assets.js, which joins it into /site.css (test/assets.test.js fails for a component file that is not listed); head.ejs and the editor preview’s SITE_STYLES link /site.css already. A new page-specific script is linked from its view, like share.js; one every page needs goes into SITE_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.

  • 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 to CHANGELOG.md under «Не випущено».