Skip to main content

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.

page JSON
{
  "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.

site/components/VxNotice.vue
<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.

terminal
npx cms-create-extension my-ext

The 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.

extensions/my-ext/extension.config.json
{
  "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.

terminal
# package.json  ->  "my-ext": "file:./extensions/my-ext"
# vite.config.js -> extensions: ['@koehler8/cms-ext-compliance', 'my-ext']

npx cms-validate-extensions --site-dir ./site

First-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.