Documentation
Components & extensions
Vertex resolves a component name across three tiers. Understanding the order is most of what you need to know about extending a site.
Three-tier resolution
When a page lists a component by name, the framework looks for it in site-local components first, then in registered extensions, then in the bundled set. Most specific wins.
That ordering is what makes overriding painless: to replace the bundled Header on one site, add site/components/Header.vue. Nothing else changes, and no page JSON has to be edited.
- site/components/ — auto-globbed, highest priority
- extensions — registered in vite.config.js
- bundled — Header, Hero, Footer, Contact, About, Portfolio, Team, NotFound, and the rest
Disambiguating a collision
When two tiers provide the same name and you want the one that lost, qualify it by source. Pages can also give a component an explicit source instead of a bare string.
{
"components[0]": "Header",
"components[1].name": "Privacy",
"components[1].source": "compliance",
"components[2]": "site:Hero",
"path": "/privacy"
}Writing a site-local component
Drop a .vue file into site/components/ and it's available by filename — no registration step. Read your content block from the injected pageContent, and gate the whole component on it so a page that doesn't supply content simply doesn't render the section.
Prefix site-local component names with the site's initials. It keeps them from colliding with bundled or extension components as either grows.
<script setup>
import { computed, inject, ref } from 'vue';
const pageContent = inject('pageContent', ref({}));
const notice = computed(() => pageContent.value?.notice || {});
</script>
<template>
<aside v-if="notice.body" class="notice">
<h2 v-if="notice.title">{{ notice.title }}</h2>
<p>{{ notice.body }}</p>
</aside>
</template>
<style scoped>
.notice {
border: 1px solid var(--theme-surface-border);
color: var(--brand-text-primary);
}
</style>When to build an extension instead
An extension is a bundle of components with a validated manifest. Reach for one when the same set of components needs to appear on more than one site — legal pages, a real-estate listing suite, token-launch widgets. For anything used by exactly one site, a site-local component is less machinery.
npx cms-create-extension my-extThe extension manifest
Each entry maps a public component name to a module and the content key it reads. The manifest is validated against a JSON schema when the Vite plugin loads it, so a typo fails the build with a message instead of rendering nothing.
{
"slug": "my-ext",
"version": "1.0.0",
"license": "MIT",
"provider": { "name": "My Extension" },
"components": [
{
"name": "Pricing",
"label": "Pricing Table",
"description": "Tiered pricing cards.",
"module": "./components/Pricing.vue",
"configKey": "pricing"
}
]
}Registering and validating
Add the extension to vite.config.js — as a file dependency for a local one, or by package name once it's published. The validator runs the same schema check the plugin does, which is worth wiring into CI.
# package.json -> "my-ext": "file:./extensions/my-ext"
# vite.config.js -> extensions: ['@koehler8/cms-ext-compliance', 'my-ext']
npx cms-validate-extensions --site-dir ./siteFirst-party extensions
Two extensions ship alongside the framework and are installable from npm today.
- @koehler8/cms-ext-compliance — Privacy, Terms, Cookies, a Legal footer strip, and a cookie-consent banner.
- @koehler8/cms-ext-crypto — token-launch components for crypto product sites.