Vai al contenuto
Spedizione in 24/48h in tutta Italia
Vai al contenuto
Navigazione documentazione

Questa pagina non è ancora disponibile nella lingua scelta. È mostrata la versione EN.

Independent static sites with Site Publish

Site Publish lets a tenant edit its independent site in k0smos and explicitly publish a static release. The supplied DM3 landing is the first profile; the generic content profile publishes public Page, Blog, Menu and Media data for another independent frontend through the same pipeline. The DM3 site serves prerendered HTML, React hydration, local fonts, images and MP4 clips; another profile's independent repository owns its rendering stack. Opening a built site needs no PHP, tenant database, Higgsfield Worker or public content API.

The initial profile preserves the existing six material chapters and single scroll-controlled film. It publishes Italian at /; English administration does not imply an English public page. The site checkout has its own Git repository, dependency lock and tooling. It is not a k0smos theme.

Ownership and operator access

site_publish is optional and disabled by default. Enable it explicitly with its Media dependency after installing the module migrations. Page is optional for dm3 legal content and required when selecting content; Blog is optional inside content and must be active when selected.

Owner Canonical data
Site Publish Target binding, bounded profile configuration, immutable document revisions and publication ledger
Media Original bytes, publication state, trusted paths, MP4 validation and image metadata
Company settings Public company identity and explicitly allowlisted social links
Page Selected DM3 legal documents or exact-locale generic Page inventory; drafts and email templates are excluded
Independent site Components, CSS, projection schema, static builder and release activation

/admin/site-publish requires site_publish.view. Editing, publication, retry, rollback and recovery require site_publish.manage; cookie-authenticated mutations require the site_publish.admin CSRF token. The Media picker retains media.view. Import additionally requires media.upload and media.edit only when the selected profile imports source assets (including dm3); the generic content profile imports no Media because it reads already-published owner data. Site Publish never grants those permissions itself.

The Default/Sober editor includes header links and logo, scene copy, desktop and mobile video/poster pairs, all six chapters, materials and manufacturer source links, contact action, SEO and up to four legal-page selections. Company values are read-only in this panel and remain editable through their existing owner. Async validation preserves browser input; a stale revision returns HTTP 409. The module editor is server-rendered and this profile addition requires no theme bundle build. Theme asset builds remain part of the maintainer's normal release procedure when theme-owned sources change.

Video replacement requires its paired exact-frame poster, with matching dimensions. Desktop MP4s allow 32 MiB and mobile MP4s 16 MiB. Media validates the self-contained H.264 container before trusted storage. The website contact link remains the default; WhatsApp needs a validated international + number and generates an ordinary outbound link. There is no lead submission backend.

Generic public content

Select "profile": "content" for an independent frontend that consumes the tenant's current public content rather than a bespoke landing document. Its one revisioned configuration record contains:

  • Page enabled as the required owner and Blog as an explicit optional owner;
  • one published home Page slug for every i18n.supported_locales entry;
  • front.primary and/or front.footer published menu placements; and
  • up to 50 internal redirects whose target exists in the same generation.

The default locale is unprefixed. Other locales use /<locale>/. Page exports published ordinary exact-locale rows; Blog uses published base rows for the default locale and requires an exact translation row for every additional locale. Missing active owner modules, home Pages, duplicate routes/documents, redirect collisions and oversized inventories fail the complete publication. No partial generation is emitted.

Page and HTML-format Blog bodies cross a semantic sanitizer. It preserves ordinary headings, paragraphs, lists, quotes, code, tables, safe links and public Media images while excluding scripts, forms, embeds, SVG/MathML, inline styles, editor classes and unknown images. Plain Blog bodies are escaped. Page CSS and project JSON, Blog editor metadata, k0smos templates and credentials are never exported. The independent frontend owns all presentation and CSS.

Published anonymous menus contain localized labels, nested children and only links that resolve to this generation or a safe HTTPS, mailto or fragment target. Referenced public images are copied by checksum with their already- generated responsive WebP derivatives; publication does not generate variants. Portable payloads contain no tenant Media IDs or private paths.

Deployment configuration

Keep the tenant's root database block unchanged. Only the independent checkout location, HTTPS public origin and optional content profile belong in its deployment-only site block:

{
  "site": {
    "root": "site/dm3",
    "public_url": "https://k0smos.example.com"
  }
}

The resolved tenant is the entry point for everything below. Its declared folder name is the published site code (site/dm3 publishes dm3), and the content profile defaults to that code. Set profile when the checkout name and contract differ; for example, a generic site may declare:

{
  "site": {
    "root": "site/showroom",
    "public_url": "https://k0smos.example.com",
    "profile": "content"
  }
}

dm3 and content are both registered. The resolved code and profile are frozen in the module's target row at first import, so a later rename or profile change resolves to a different site or fails closed rather than republishing this one under a new name.

Editing depends on the declaration only, never on the checkout being present, so content work continues while a site folder is unmounted or replaced. Publication fails closed until the checkout resolves, and the editor panel, site:status and the CLI report the exact deployment error.

Optional Supabase/PostgreSQL projection

site.sqlite remains the canonical, sufficient generation database. A deployment that wants a Supabase-backed site reader may add a separate server-side PostgreSQL target without changing the tenant's root database:

{
  "site": {
    "root": "site/showroom",
    "public_url": "https://k0smos.example.com",
    "profile": "content",
    "database": {
      "driver": "supabase",
      "url": "${K0SMOS_SITE_SUPABASE_DATABASE_URL}"
    }
  }
}

The environment value is a PostgreSQL connection URL for a server-side writer, not an anon browser key. Remote Supabase connections require sslmode=require. Literal credentials must not be committed to tenant JSON and never enter AppSettings, the public manifest, build receipts, logs or generated assets.

Before enabling the target, apply modules/SitePublish/resources/schema/v1.pgsql.sql with the deployment's qualified schema owner. Runtime publication performs no DDL. The schema marker must remain version 1; every generation data table is namespaced by exact site and generation, and RLS has no public policy by default. A direct writer needs the deployment-approved protected-table role. If PostgREST/browser reads are later enabled, add narrowly reviewed site-specific SELECT policies and never expose a service credential in the browser.

After the local manifest is finalized, Site Publish verifies its ledger identity and SQLite digest, then inserts all portable rows into PostgreSQL in one transaction before the independent-site validation/build. Repeating the same manifest digest is safe; a conflicting generation ID, sequence, schema or digest fails closed. A PostgreSQL outage fails that publication attempt without modifying the immutable SQLite generation. Omitting site.database keeps the current SQLite-only path unchanged. A live Supabase account and executable SQLite/PostgreSQL parity test are not prerequisites at this stage.

Relative roots resolve below the k0smos checkout. By default, a target must be a strict descendant of its site/ directory. For a separately located checkout, set K0SMOS_SITE_ALLOWED_ROOTS to approved absolute parent directories, separated by the platform's path separator. Canonical-path checks reject source/public overwrites, a target containing the tenant database, unsafe links and path escapes. The root must already contain the site's app, contracts and tooling directories.

Locale policy, revisions and status are module-owned database state. The target stores an opaque source identity separately from its internal tenant binding. A private site-side binding must match before publication. Bun must be available to the worker; the adapter uses fixed arguments, app/ as its working directory, bounded execution time/output and an allowlisted process environment. Credentials and arbitrary editable shell commands are not passed to the build.

Choose the document root deliberately

The two public surfaces remain independent even when they share one hostname:

Web-server target Result for /
k0smos public/ Runs public/index.php and renders the tenant's configured theme.front
site/<site>/current Serves the activated independent static release

The site.root declaration tells Site Publish where to prepare and activate the independent checkout. It does not change k0smos routing and does not make the independent site a theme. Pointing a virtual host at the k0smos repository root is unsupported; the PHP application document root is always its public/ directory.

Do not use site/<site>/app/public as the independent production document root. That directory is owned by the site's source application and supplies bounded import assets. A build writes an immutable releases/<generation>/public directory, and activation atomically moves current to that release. The web server must also map /assets/ and /site-media/ to their matching directories under published-assets/; serving current alone loses the retained-asset contract used by old browser tabs and rollback.

Bun is a build-time dependency of the queue worker and explicit site:build command. Once a generation is active, Apache, Caddy or another qualified static server reads ordinary files; no Node, Deno or Bun HTTP process is required.

First import and backup

Use the existing target tenant. The examples use the canonical placeholder hostname; substitute the tenant's configured host or numeric ID. --actor=7 illustrates an existing authorized user and must be replaced with the real operator ID. Module commands are available only when the module is active.

php bin/console site:import --host=k0smos.example.com --actor=7 --dry-run
php bin/console site:import --host=k0smos.example.com --actor=7 --confirm-checksum=PREVIEW_SHA256

For dm3, preview checks the supplied document and all seven original assets. A content checkout instead supplies contracts/content.import.json with an empty assets collection and a bounded configuration document. Preview reports the exact combined checksum, counts and occupied document status. Apply requires that unchanged checksum. The current SQLite adapter creates a consistent backup under private var/backups/site-publish/ before first import; backups are not included in customer handoffs. Other tenant database platforms require a qualified backup adapter before import. Archive the preview and backup according to the deployment's retention policy.

Import uses Media's owned ingestion path and rebinds portable asset references. It inserts missing target/document records only. Repeating it preserves operator edits and occupied keys. This operation imports the supplied site; it is not a demo fixture and does not activate the site.

Save, publish, build and recover

The panel reports four distinct facts: saved editorial revision, exported revision, recorded live generation and independently verified live generation. Saving does not update the public website. Publishing records a complete- generation request and queues its identity on the logical default queue. The worker captures the owner-consistent SQLite/media snapshot, builds it and activates the verified release under the target lock. Frontend compilation and activation are never a synchronous HTTP fallback. Keep the ordinary tenant queue worker running; see Queue operations.

php bin/console site:publish --host=k0smos.example.com --actor=7
php bin/console site:status --host=k0smos.example.com
php bin/console queue:topology --host=k0smos.example.com
TENANT_ENV=k0smos.example.com php bin/console queue:work --queue=default --worker-slot=1

States progress through requested, preparing, ready, building, validated and active; failures remain visible. Every publication gets a new sequence even if only company settings or Media changed. A late older worker cannot activate after a newer request. A failed capture or build preserves the previous release.

php bin/console site:publish --host=k0smos.example.com --actor=7 --retry=generation_001
php bin/console site:build --host=k0smos.example.com --actor=7 --generation=generation_001
php bin/console site:rollback --host=k0smos.example.com --actor=7 --generation=generation_001
php bin/console site:status --host=k0smos.example.com --actor=7 --reconcile

site:build is an explicit operator alternative to queue processing for one requested or frozen generation; when capture is still pending it performs that capture before building. Retry reuses a complete checked generation or requests a new capture when preparation never completed. A validated release whose activation failed can be queued again without recompiling; the editor offers Retry until its first activation. Previously activated releases instead offer Rollback, and duplicate retry requests cannot undo an operator's rollback. Rollback selects a retained validated release without reverting edited content. Recovery compares the live release marker and checksums with the ledger after an interrupted activation; it never assumes that a successful export is online. The full command inventory is in Console and Composer commands.

Portable contract and release storage

The independent repository owns contracts/schema-v1.sql, its sample/import manifests and contracts/README.md. Only explicit site tooling initializes the projection schema; the publisher inserts validated data. Build processes open the completed projection read-only and never query the changing tenant DB.

data/source-binding.json
data/generations/<generation>/
  manifest.json
  site.sqlite
  media/
releases/<generation>/
  release.json
  public/
published-assets/
  assets/
  site-media/
current -> releases/<generation>/public

Schema version 1 carries the tenant's resolved site code, opaque source identity, generation ID, monotonic sequence, document revision, timestamp, public origin, locale/route inventory and SHA-256 checksums. SQLite contains generation metadata, explicit public settings/navigation, documents under profile-owned portable keys, selected legal pages, redirects and referenced Media. dm3 writes one dm3 document and leaves redirects empty. content writes its site descriptor plus page/<slug> and blog/<slug> rows per locale and uses the redirect table. There are no private credential or raw editor-project tables.

The optional PostgreSQL v1 schema carries the same logical records, adding site_code and generation_id to every shared row so retained generations are append-only and selectable without overwriting one another. The independent site's data layer keeps SQLite as the default and may select one exact site/generation from PostgreSQL behind the same portable reader contract.

The publisher captures matching content and Media bytes, verifies copies and writes the manifest last. A committed generation is never overwritten. Required missing/private Media and malformed/checksum-mismatched input fail closed. A selected legal page that is no longer published disappears from the next complete generation. Legal routes are /legal/<slug>/; their renderer accepts semantic text and safe links, without arbitrary CSS, scripts or embedded assets.

Activation verifies the release and copies immutable assets into the append-only published-assets pool before switching current. Copies are checked in private staging on the same filesystem and installed by atomic rename. Interrupted copies cannot leave incomplete public assets; isolated partial-write and full-disk error injection verify that activation can be retried. Serve /assets/ and /site-media/ from that pool so browsers holding older HTML can still load their files after an activation or rollback. Retain the pool with all referenced releases; V1 has no automatic cleanup command. Linux activation uses atomic symlink replacement. Windows activation remains unsupported until its adapter is qualified.

Same-host routing

The reviewable DM3 Caddy profile at deploy/sites/Caddyfile.dm3 and the generic Apache template at deploy/sites/apache.site-publish.conf keep independent static roots separate from k0smos public/. The Apache template contains no tenant or profile name. Include it from a deployment-specific virtual-host file after defining SITE, ALIAS, MAIL, LOG, CERT, KEY, PHP, K0SMOS_ROOT, SITE_PUBLISH_ROOT, K0SMOS_BACKEND_PORT, SITE_PUBLISH_HTTP_PORT and SITE_PUBLISH_HTTPS_PORT as documented in its header. It creates a private loopback k0smos backend and preserves the original Host for tenant resolution. The Caddy example instead uses DM3_SERVER_NAME, DM3_SITE_ROOT and K0SMOS_BACKEND.

Neither server is a runtime requirement. A qualified static server must use the activated current directory for ordinary HTML, alias /assets/ and /site-media/ to published-assets/, apply the documented cache/range/security headers, deny private checkout paths, and return real static 404s. For the k0smos route inventory it may either:

  • reverse proxy to a private k0smos PHP HTTP listener, preserving Host; or
  • execute k0smos public/index.php directly through PHP-FPM with explicit aliases/rewrites for those backend paths.

Neither Apache arrangement proxies to a JavaScript development server. Do not apply k0smos's general PHP front-controller fallback to the static site: an unknown public path must not silently enter PHP.

The explicit backend route inventory covers admin/kiosk, API, login/logout/auth, OAuth and its well-known metadata, native /media, admin /build assets and supported localized forms. Review configured private-login and provider callback routes against the deployment before activating the profile. Public /, legal pages, robots/sitemap/404 and independent assets use the static roots. /app, unknown routes and missing assets return real 404s; there is no SPA or PHP catch-all rewrite.

The static server owns correct MIME/range responses, immutable asset caching, HTML revalidation and the CSP, including media-src blob: for the preserved scrub controller. Deny source, .git, environment files, private generation manifests, SQLite/WAL/SHM and unactivated inputs. The current landing uses no optional tracking or embedded provider; outbound manufacturer/WhatsApp links do not load an SDK.

Hosting-configuration backup/restore is deployment policy, not a Site Publish acceptance requirement. It remains separate from release rollback. Static delivery survives unavailable PHP endpoints when the static server remains running. A colocated server still shares hardware, disk, network and possibly the web-server process; stopping that infrastructure is not the application- outage test.

Current tenant qualification — 2026-09-21

Tenant 33 was resolved from its unique existing descriptor and SQLite database; the root database configuration was preserved. After a private consistent backup and checksum-confirmed import, revision 1 was published as generation g20260921135800-0d4666306d53d2ab. Its source manifest digest is 1daed1bd82021e34f5811af050aa497a22a189f5e7773f4a446c1eff9063a5d0 and its release digest is 12cdd599cc851eff5afe2b294e46436f5a3c0da662cd85ffd378785c7f4fed5c. Status independently verifies that release as active.

The Apache profile first passed syntax, route, denial, static 404, immutable cache, MP4 range, CSP and PHP-outage checks on private ports using the active release and real tenant routes. A privileged operator then switched the actual Apache listeners on 2026-09-21. HTTP and HTTPS root responses match the active release byte-for-byte; real static 404s, private-path denials, immutable CSS, MP4 range delivery and the explicit admin/login/OAuth backend routes pass with the tenant Host and TLS SNI. The canonical hostname is not resolvable from this workspace, so external DNS and network reachability are not inferred from the server-side listener checks. The active deployment now includes the generic deploy/sites/apache.site-publish.conf; repeat probes after that migration returned the same release hash and routing results.

The controlled backend-unavailable probe passed on the public listener: with a deliberately nonexistent PHP-FPM socket, root HTML retained its exact active release hash, immutable CSS remained 200, a real MP4 range remained 206 and the physical static 404 remained 404, while admin, login and OpenID returned 503. This proves separation from the PHP endpoint only. Apache, storage, hardware, network and the host are shared failure domains and provide no independent availability guarantee. The owner explicitly removed the old-vhost backup/restore exercise from this deployment's acceptance scope; release rollback remains a separate Site Publish operation.

After restoring the production PHP-FPM socket and reloading Apache, root retained the same active-release SHA-256, anonymous admin routes again redirected to their matching login routes, and login plus OpenID returned 200. No static release activation or rollback was involved in the outage or recovery.

Optional customer-hosted pull

A customer host that cannot accept inbound deployment connections can pull one already validated generation from k0smos. This is a private machine-to-machine surface, not a browser content API and not part of the public OpenAPI document. It is an available optional transport, not a required deployment workflow; deployment-specific scripts or Git branches may be used instead. All routes require the exact site_publish.pull permission and a personal API token that is explicitly scoped, has a future expiry and belongs to the live user. Full-authority legacy tokens, unscoped tokens, permanent tokens, JWTs and login cookies are deliberately insufficient.

Grant site_publish.pull only to the dedicated operator account used for that customer site. That user can issue its own token through POST /api/user-settings/api-tokens with an explicit expiry:

{
  "name": "customer-site pull",
  "scopes": ["site_publish.pull"],
  "expires_in_days": 30
}

The raw k0s_pat_... value is returned once. Store it only in the customer host's secret manager or root-readable environment file; never put it in tenant JSON, site source, generated assets, command arguments, URLs or build output. The pull path is classified as sensitive before routing, so application debug, SQL and request collectors exclude its headers and response bodies. Deployment proxy access logs remain the operator's responsibility and must not record Authorization headers.

Given the operator-selected site code and generation ID, the runner uses:

Method Private route Result
GET /api/site-publish/pull/<site>/<generation>/snapshot Raw, frozen manifest.json
GET /api/site-publish/pull/<site>/<generation>/database The manifest-listed site.sqlite
GET /api/site-publish/pull/<site>/<generation>/media/<asset> One exact manifest-listed Media file
POST /api/site-publish/pull/<site>/<generation>/acknowledgements Idempotent proof of activated manifest/release digests

Every request revalidates PAT state, user association, tenant-local site binding, generation state, source identity and manifest checksum. Downloads are available only for validated or active generations. Database and Media names must be listed in that immutable manifest, and the bytes are checksum-verified again on the opened stream before the private no-store response is sent. A route from one tenant cannot name a target in another tenant because authentication, target and generation ledgers all resolve from the current tenant database.

The customer runner must stage into a new private directory, reject an unknown schema or identity, download only the manifest inventory, verify every SHA-256, then invoke the site repository's own validate/build operations. It activates the new release atomically only after the build receipt matches the selected generation. Finally it posts exactly:

{
  "manifest_digest": "<64 lowercase hex>",
  "release_digest": "<64 lowercase hex>"
}

The first identical acknowledgement returns 201; retries return 200 and the original timestamp. A different digest conflicts and cannot replace the proof. k0smos stores only the token row ID and associated user ID, never the raw token. The independent checkout owns the actual runner and release layout; restore its documented tooling/ and contracts/ files before enabling this mode.

Rotate by issuing a second short-lived scoped token, testing one complete pull, then revoking the old token through DELETE /api/user-settings/api-tokens/<id>. Revocation is checked on the next request, and removing the permission or disabling the user also stops access. Keep the prior customer release active throughout rotation. The customer owns backups of its site repository, frozen inputs and release pointer; restoration selects a previously verified local release and does not require access to the k0smos tenant database.

Retain generations and checksum-addressed Media for at least every rollbackable customer release. No automatic generation or Media cleanup is shipped, so a site-specific, checksum-aware retention policy must be proven before deleting anything. During a k0smos/PHP outage, the last activated customer release stays live and new publication waits; do not retry activation from incomplete staging. Shared customer hardware, storage, network and static-server failures remain a separate availability domain.

The same client-host boundary applies to deployment-specific scripts or Git branches: serve only the activated static release, mount retained hashed files at /assets/ and /site-media/, and keep all staging, source and release metadata private. site.sqlite is a build input and is optional at runtime unless that independent frontend explicitly chooses a read-only server/runtime adapter for it. No client host needs PHP or access to the k0smos database.

Customer handoff and verified scope

From the independent checkout, the site-owned commands rebuild and serve a saved generation without PHP or k0smos:

bun tooling/site.ts validate --generation generation_001
bun tooling/site.ts build --generation generation_001
bun tooling/site.ts serve --generation generation_001 --port 4173
bun tooling/site.ts handoff --generation generation_001 --destination /tmp/dm3-customer

Handoff includes source, lockfile, selected local generation/assets, licenses and operating instructions. It excludes the editorial database and private backups. Install locked dependencies in app/ using bun install --frozen-lockfile; an uncached first install requires the package registry. Preserve original asset rights and upstream notices; possession of an export does not establish new redistribution rights.

Isolated PHPUnit checks cover migration-backed revisions, target isolation, stale-worker fencing and rollback. Real-kernel checks cover cookie authentication, ACL/CSRF, saves, conflicts, invalid media pairs and scoped/expiring pull-token revocation. Pull service checks cover tenant/site/generation fencing, manifest- listed streaming, tamper refusal and idempotent digest acknowledgement. Real ThemeEngine renders cover Default/Sober in IT/EN. The independent repository records its static browser, build, outage and handoff evidence separately.

As of 2026-09-15, tenant ID 33 was resolved uniquely from configuration, but its configured var/db/33.sqlite was absent in this workspace. The target was not imported or activated. Apache was present and no matching inspected virtual host established the final topology; the running web-server configuration was left unchanged. Local evidence does not establish tenant-specific live acceptance or a production Caddy reload.