The theme and overrides

4 min read

The theme's manifest, the virtual modules the plugin answers, replacing a component by name, and the /app shell.

The manifest

packages/movable-types/src/theme.js names the theme's entry (main.js), its design stylesheet, its strings directory, its section schemas, its build steps (steps: modules exporting a step record each, run among the kit's — see the build steps), and what the phone probe lets run to the edge (probe.allow, and probe.rails for a scroller that may leave on the right while keeping its left gutter). The components are every .vue under src, registered by file name; the kit refuses a theme where two share one. loadTheme() in the kit reads the manifest, and a second theme meets the same shape.

The virtual modules

The Vite plugin (packages/kit/vite/index.mjs, platformkit() in a two-line vite.config.js) reads the config and answers a family of import specifiers. Under Node — the build scripts, the tests — each has a fallback in the packages' imports maps, so the same code runs both sides. In development, the plugin watches Markdown changes and runs the matching build phases; it reports a failed step and does not continue later refresh phases for that change. It also serves the development-only GET /__platformkit/site-meta endpoint used by workspace directory cards, applying the same head resolver as the site.

Specifier Exports From
#site the public config, modules, moduleKeys (every module a manifest declares — lib/modules.js refuses a name that is not among them), places (the community places the site registered, each page loaded when first opened) the config, community.places
#site/looks LOOKS, DEFAULT_LOOK (and imports every look's css). A site that registers none wears the theme's own palette: its css is imported, DEFAULT_LOOK is the theme's name for it, and LOOKS stays empty — so such a site shows no look switcher, because a switcher offers what the SITE declared. Without the fallback a site with no look imported no palette at all: data-look was still stamped, every var(--x) fell through to an undefined custom property, and the landing's button drew its text in the colour of its own background. build/theme-look.test.mjs holds all three — the import, the name, and the empty list looks
#site/landing SKINS_BY_LOOK, SECTIONS, SECTION_SCHEMAS, STAGES, STAGE_SCHEMAS looks[].skins, landing
#site/app tabs, answered app.tabs
#site/routes routes (components imported) routes
#site/nav links, cta (or null) nav
#site/analytics panels, events, sources (names only, never a key) analytics
#site/instrument Instrument, TYPEN, DOMAINS, LEVELS, CHS, LV, LVN, ARCH, STATEMENTS, cellName — a contract with a default for every name instrument.module
#site/recommendations readerProfile, readerSignals, recommendedBooks, topicByKey, invites — a contract recommendations.module
#site/member panels — a contract, [] by default: what a site puts on the private half of a member's card in a space spaces.member
#site/desk sources — a contract, [] by default: optional staff Desk adapters registered by the site spaces.deskSources
#site/admin sections admin.sections
#content the compiled content (.velite) velite
#content/*, #site/content/* one compiled file; one raw file of the site's aliases

The public #site config also carries the community, network and coaching settings used by the theme; the assembled function-side config preserves the network access and coaching booking preferences needed by server-side flows. The CMS is compiled into production when the resolved content store is rows; a backend and repository alone do not turn it on when content is file-backed.

The contracts (INSTRUMENT, RECOMMENDATIONS in packages/kit/build/site.mjs) are what let the theme's admin name a reading's areas and levels, or the hub ask for recommendations, on a site that has neither: the demo gets empty lists and nothing breaks.

invites is the one that decides whether something is shown. It answers whether the site can earn recommendations for somebody who has none — a diagnostic to take, a survey, anything other than waiting for them to read more. The kit's answer is no, and then Recommended For You is not offered at all until there is something in it: no toggle, no card, no mention of it. A site that says yes gets the view offered while it is still empty, because there the empty view is not empty — it carries the invitation.

Overrides

A site names a component and a file: overrides: { SiteClose: './src/overrides/SiteClose.vue' }. The plugin answers every import that lands on the theme's SiteClose.vue with the site's file, however the import is written — package specifier, relative path from a theme file, the @/ alias — so the theme needs no registry indirection. The override importing the component it replaces gets the original, so a site can wrap as well as replace. Two imports are left alone: one from an .html file and one from the override itself.

overrides.test.mjs drives the real plugin on a Vite server in middleware mode with every one of the theme's components stubbed and resolves each the three ways; it runs in platformkit test.

These docs override exactly one component: SiteMark.vue, the small emblem beside the name in the header, the admin, the share cards and the favicon — here, PlatformKit's three cubes. The demo overrides nothing and wears the theme's own circle-and-dot, which is the honest thing for a site standing in for somebody else's press.

The mark is drawn rather than linked, because the build photographs it for the favicon at every size and sets it into the share cards. Keep the two props the convention allows (size, stroke) and nothing else in the markup — no interpolation, no v-if, no v-for — so the build can lift the SVG whole for the Edge Functions; anything it cannot read it simply leaves out.

The /app shell

tabs.js in the theme lists the kit modules' tabs — accomplishments, coaching, chat — each under the module that switches it, coaching and chat also under coaching.where (with coaching in the community a member has neither — they are the coaching place there — and somebody who gives sessions keeps coaching). A site's own tabs come through #site/app with their components, their words, a start() for a page that starts a round, props() built once per sheet (several readings can share one results model), and first: true to lead. ResultsSheet.vue renders the registry alone. A site with no instrument gets AppShell.vue: the sheet with the kit's tabs.

The router

@platform-kit/kit/lib/router.js owns the routes (ROUTES), navigate(), the page-route test, the app's page slugs (APP_PAGES), the site's own routes from #site/routes, and the three rules the switches make — KINDS_ON, ACCOUNTS_ON, DOCS_ONLY (with DOCS_BASE, where the docs are addressed); the theme re-exports it. /content carries nested paths; the admin and the share cards carry their place in the path; a kind switched off falls to the hub; on a docs-only site /content/docs/… is an alias of /docs/…. With the social community layout, bare /spaces resolves to the configured community home; /spaces/<slug> remains the route for an individual space.