@bygghammar/cms-platform
Shared CMS platform code for every site in this workspace (Voyara, barskola and future sites). Code only — no content, no data, no secrets, no theme.
What lives here
src/blocks/types.ts— block prop types and the JSON contract for block propssrc/blocks/icons.ts— the icon set blocks may referencesrc/blocks/block-props-context.tsx— block prop plumbing for inline editingsrc/i18n/*— the localization core (locales, types, resolver, context)src/backup/*— archive/media safety, browser-side large-media export and provider-neutral verification helperssrc/trips/*— reusable trips data contract, editor and legacy course-data compatibility. Each host supplies its own terminology (Courses, Cruises or Trips).src/navigation/*— public and backoffice navigation contracts plus shared active-state helpers. Menu content and visual headers remain per-site.src/theme.css— the design-token contract + neutral starter theme
Starting a new site
docs/NEW-SITE.md is the supported starter path: install, assert the contract,
apply the canonical schema with bunx cms-platform-migrations, import
theme.css, register host components / platform data / translations, wrap the
server cores and mount the shared backoffice views.
Editing locks
docs/EDIT-LOCKS.md is what a host wires so two administrators cannot
overwrite each other: run the migration, expose one server endpoint through
editLockActionCore, mount EditLockProvider with createEditLockApi, and
forward editSessionId to the saves. Until that is done every screen behaves
exactly as it did before locks existed.
Feature guides
| Guide | What it covers |
|---|---|
docs/PAGES.md | Draft/published pages, the publish and unpublish confirmations, duplication |
docs/SCHEDULING.md | VisibilitySchedule, site-timezone editing, scheduled menu and footer items |
docs/LAYOUT-BLOCKS.md | The columns block, the nested block tree and its move rules, the Icon Cards mobile carousel |
docs/IMAGE-CARDS.md | The Image Card Gallery block: its props, defaults, overlay and colour rules, links, the phone slider and the editor |
docs/MEDIA-FOLDERS.md | Automatic month folders, custom upload folders and their sanitising, library browsing |
docs/GOOGLE-SHEETS.md | Service-account form delivery, the connection test, and the server variables it needs |
docs/TYPOGRAPHY.md | Independent Google Fonts for the menu and the footer |
docs/PUBLICATION-REVIEW.md | The cms-review procedure for upgrading a live site to contract v3 |
Server variables
Beyond the existing provider secrets, Google Sheets delivery reads either
GOOGLE_SERVICE_ACCOUNT_JSON, or GOOGLE_SERVICE_ACCOUNT_EMAIL plus
GOOGLE_SERVICE_ACCOUNT_PRIVATE_KEY. All three are server-only; nothing Google
related is ever read in the browser, and the font picker needs no API key. See
docs/GOOGLE-SHEETS.md.
Compatibility policy
-
CHANGELOG.mdrecords every release; read it before upgrading. -
Pre-1.0, a minor release may break shared views, adapters or cores; patch releases are always compatible.
-
PLATFORM_CONTRACT_VERSION(platform/contract) is bumped when a shared screen starts requiring schema or adapter capabilities an older site lacks. Sites pin it in a test. It is v3: page saves, publishing, trashing and restoring go through database functions (v2), and a release records the five facts it used to assert with one sentence — migration applied, schema capability, publication scan, decisions applied, routes activated — as five separate records the later of which refuse to be written without the earlier (v3). Upgrading straight from the last pre-v2 published package,0.90.0-m7.12, means applying all five new canonical migrations, in chronological order:20260904090000_c54603f8-a155-40f8-a86c-54434aa5c0ae.sql20260904140000_9f2a7c41-06d5-4d6b-8b4e-1c7a5d3e9b20.sql20260904160000_0d565000-2839-4738-aacc-537d7090398f.sql20260905120000_da2db0b2-6048-4aa8-8e4f-13ee33e9cc24.sql20261030120000_46a4febc-227e-42ec-bbe3-a0ae9c97feaa.sql
Files 2 and 3 are the pair that establishes contract-v2 publication behaviour, but the first is canonical schema too, so applying only the pair still leaves
missingMigrationsandcompareSchemareporting drift. File 4 adds contract v3: the release ledger, the review model, the route claims, one lock order every page mutation follows, and duplication as a single transaction. It replaces five publication function bodies at the signatures they already have, without changing what any of them decides, so nothing a visitor sees today changes. File 5 belongs to no contract version: it givesblock_templatesits own timestamp trigger in place of the pages trigger it was created with, which assigned anupdated_bycolumn that table does not have and so failed every update of a template. Apply them before the v3 package is deployed, then verify separately:missingMigrations(applied)reports nothing missing, andassertPlatformContract(3)passes. The second only compares version numbers in the site's own code and never reads the database, so it is no substitute for the first.docs/NEW-SITE.mdgives the full order. -
migrations/is schema only. Site content, SEO copy and seeds never belong there.
What must never live here
- colours, fonts or spacing values used by a specific site (see
theme.css) - page content, SEO copy, media, CMS page JSON
- Supabase generated types, auth users, secrets, API keys
- route files and per-site deployment config
- actual navigation links, public slugs, brand-specific headers or menu styling
- site table lists, import order, database clients, authentication and RLS policies; each host supplies those adapters to the shared backup core
Backup core
The package owns the CMS-wide, provider-neutral parts of backup and restore:
- media signature/path validation and secret sanitising
- streaming large media files into an archive in the authenticated browser
- archive progress and manifest contracts
- row-count, user/role and media verification helpers
The browser media exporter accepts a host storage adapter with only list()
and download() methods plus the host's expected archive format/schema
versions. Keep the dynamic import behind an import.meta.env.SSR guard in SSR
applications so the browser-only ZIP code cannot enter the optimized server
chunk graph.
Individual media downloads are retried before an export is stopped. Public media-bucket adapters should also fall back to the object's public URL when an authenticated Storage download fails, so one transient large-file request cannot discard an otherwise valid archive.
The consuming site remains responsible for its dataset groups, table import order, Supabase clients, superadmin authorization, HTTP routes, audit records and database migrations. The package never contains site data or credentials.
Per-site wiring
Translation data is per-site content, so each site registers its own maps once at startup:
import { registerTranslations } from "@bygghammar/cms-platform/i18n";
registerTranslations({ generated: TRANSLATIONS, overrides: TRANSLATION_OVERRIDES });
Host components must participate in a value the application actually uses.
Do not rely only on import "./register-host" because an optimized build may
remove that module when the application's package.json declares
"sideEffects": false.
import { initializeHostComponents, type HostComponents } from "@bygghammar/cms-platform/host";
const HOST_COMPONENTS = {
FocalImage,
LazyVideo,
FocusPicker,
RichTextEditor,
mediaLibraryQueryOptions,
useMediaUpload,
} satisfies Partial<HostComponents>;
export const BLOCK_REGISTRY = initializeHostComponents(HOST_COMPONENTS, () =>
createBlockRegistry(),
);
Keep any existing registration module in the consuming application's
sideEffects allowlist as defence in depth until every entry point uses the
explicit contract. CSS must also remain in that allowlist.
Repository, development and releases
The canonical source of truth is https://github.com/byggis79/cms-platform (private). This repository is standalone — it is no longer a directory inside a site repository, and it must never be copied back into a consuming site.
bun install
bun run check # typecheck + tests + package verification
The package ships TypeScript sources; there is no build output. Every change
goes through a pull request, and every release is triggered by pushing a
vX.Y.Z tag that matches package.json. That tag runs
.github/workflows/publish.yml, which publishes to npm using Trusted
Publishing: GitHub mints a short-lived OIDC token scoped to that single
workflow run, and npm exchanges it for publish rights through the package's
Trusted Publisher configuration. No long-lived npm credential takes part — no
NPM_TOKEN, no NODE_AUTH_TOKEN, no OTP. RELEASING.md is
what the workflow checks and the npm settings it has to match. Never publish
manually.
See CONTRIBUTING.md for the full workflow and changelog
conventions. Consumers install an explicit released version such as
@bygghammar/[email protected].