@jasonshimmy/vite-plugin-cer-app

Nuxt-style meta-framework for @jasonshimmy/custom-elements-runtime
10,097
Install
npm install @jasonshimmy/vite-plugin-cer-app
Latest Version:0.29.0
License:MIT
Last Updated:Oct 7, 2026
Links: GitHub  ·  npm
Maintainer: jshimkoski

@jasonshimmy/vite-plugin-cer-app

A Nuxt/Next.js-style meta-framework built on top of @jasonshimmy/custom-elements-runtime. Turns any Vite project into a full-stack application with file-based routing, server-side rendering, static site generation, server API routes, and more — all through native Web Components.


Features

  • File-based routing — app/pages/ directory maps directly to routes
  • Layouts — app/layouts/ with <slot> composition
  • Three rendering modes — SPA, SSR (streaming), and SSG
  • Server API routes — server/api/ with per-method handlers (GET, POST, …)
  • Auto-imports — runtime APIs (component, html, ref, …) injected automatically in page files
  • Data loading — loader export per page; serialized server→client via window.__CER_DATA__
  • useHead() — document head management (title, meta, OG tags) with SSR injection
  • App plugins — ordered plugin loading with DI via provide/inject
  • Route middleware — global and per-page guards
  • Server middleware — CORS, auth, and other HTTP-level middleware
  • JIT CSS — Tailwind-compatible, build-time via the runtime's cerPlugin
  • HMR — virtual module invalidation when pages/components are added or removed

Installation

npm install -D @jasonshimmy/vite-plugin-cer-app
npm install @jasonshimmy/custom-elements-runtime

Add the plugin to vite.config.ts:

// vite.config.ts
import { defineConfig } from 'vite'
import { cerApp } from '@jasonshimmy/vite-plugin-cer-app'

export default defineConfig({
  plugins: [cerApp()],
})

Or use a cer.config.ts alongside vite.config.ts (the CLI reads this automatically):

// cer.config.ts
import { defineConfig } from '@jasonshimmy/vite-plugin-cer-app'

export default defineConfig({
  mode: 'spa', // 'spa' | 'ssr' | 'ssg'
})

Quickstart with the CLI

The fastest path is scaffolding a new project:

npx --package @jasonshimmy/vite-plugin-cer-app create-cer-app my-app
# → choose spa / ssr / ssg
cd my-app
npm install
npm run dev

Note: The --package flag is required because create-cer-app is bundled inside @jasonshimmy/vite-plugin-cer-app rather than published as a standalone package.

Or install the CLI globally to skip the flag entirely:

npm install -g @jasonshimmy/vite-plugin-cer-app
create-cer-app my-app
cer-app dev

Project Structure

my-app/
├── app/                        # All client-side app code
│   ├── pages/
│   │   ├── index.ts            # → route /
│   │   ├── about.ts            # → route /about
│   │   ├── blog/
│   │   │   ├── index.ts        # → route /blog
│   │   │   └── [slug].ts       # → route /blog/:slug
│   │   └── [...all].ts         # → catch-all /*
│   ├── layouts/
│   │   └── default.ts          # Default layout wrapper
│   ├── components/             # Auto-registered custom elements
│   ├── composables/            # Auto-imported composables
│   ├── plugins/                # App plugins (01.store.ts → loaded first)
│   └── middleware/             # Global route middleware
├── server/
│   ├── api/
│   │   ├── users/
│   │   │   ├── index.ts        # GET/POST /api/users
│   │   │   └── [id].ts         # GET/PUT/DELETE /api/users/:id
│   │   └── health.ts           # GET /api/health
│   └── middleware/             # Server-only HTTP middleware
├── public/                     # Copied as-is to dist/
├── index.html                  # HTML entry
└── cer.config.ts               # Framework config

.cer/ is auto-generated on every dev/build and gitignored. The framework bootstrap (app.ts) lives there and is never user-owned — plugin updates propagate automatically.


Pages

Every file in app/pages/ defines a custom element and optionally exports page metadata and a data loader:

// app/pages/blog/[slug].ts

// component, html, useProps are auto-imported — no import statement needed
component('page-blog-slug', () => {
  const props = useProps({ slug: '' })

  return html`
    <div class="prose">
      <h1>${props.slug}</h1>
    </div>
  `
})

// Optional: page metadata
export const meta = {
  layout: 'default',
  middleware: ['auth'],
  hydrate: 'load',
}

// Optional: server-side data loader
export const loader = async ({ params }) => {
  const post = await fetch(`/api/posts/${params.slug}`).then(r => r.json())
  return { post }
}

File → Route mapping

File Route
app/pages/index.ts /
app/pages/about.ts /about
app/pages/blog/index.ts /blog
app/pages/blog/[slug].ts /blog/:slug
app/pages/[...all].ts /*
app/pages/(auth)/login.ts /login (group prefix stripped)

Layouts

// app/layouts/default.ts
component('layout-default', () => {
  return html`
    <header><nav>...</nav></header>
    <main><slot></slot></main>
    <footer>...</footer>
  `
})

The framework wraps each route's content inside the layout declared by meta.layout. Defaults to 'default' if the file exists.


Server API Routes

// server/api/users/[id].ts
import type { ApiHandler } from '@jasonshimmy/vite-plugin-cer-app/types'

export const GET: ApiHandler = async (req, res) => {
  res.json({ id: req.params.id })
}

export const DELETE: ApiHandler = async (req, res) => {
  res.status(204).end()
}

useHead()

import { useHead } from '@jasonshimmy/vite-plugin-cer-app/composables'

component('page-about', () => {
  useHead({
    title: 'About Us',
    meta: [
      { name: 'description', content: 'Learn more about us.' },
      { property: 'og:title', content: 'About Us' },
    ],
  })

  return html`<h1>About</h1>`
})

Content Layer

Drop Markdown and JSON files into content/ and query them with queryContent().

Numeric ordering prefixes are supported on both directories and files. A leading NN. is stripped from the public content path, which lets you keep source-tree ordering without leaking the prefix into URLs:

content/
  01.docs/
    01.getting-started.md   -> /docs/getting-started
    02.routing.md           -> /docs/routing
  02.blog/
    01.index.md             -> /blog
    02.2026-04-01-hello.md  -> /blog/hello

Date-prefixed filenames still work the same way after the numeric prefix is removed.

For content-driven routing, use app/pages/[...all].ts to resolve valid nested URLs at runtime. Unlike app/pages/404.ts, a catch-all page is not treated as a 404 automatically. Existing content routes stay HTTP 200, and in SSG with ssg.routes: 'auto' a catch-all page that uses queryContent() can auto-generate concrete output paths from the content store.

See docs/content.md for the full content-layer API and examples.


Documentation

Guide Description
Getting Started Installation, scaffolding, first app
Configuration All cer.config.ts options
Routing File-based routing, dynamic segments, route groups
Layouts Layout system and <slot> composition
Components Auto-registered custom elements
Composables Auto-imported composables
Content Layer File-based Markdown/JSON content with queryContent() and useContentSearch()
Plugins App plugin system and DI
Middleware Route guards and server middleware
Server API Routes HTTP handlers in server/api/
Data Loading Page loaders and SSR data hydration
Head Management useHead() reference
Rendering Modes SPA, SSR, and SSG in detail
CLI Reference cer-app and create-cer-app commands
Manual Testing Guide How to test every feature end-to-end

Runtime release maintenance

After publishing a new @jasonshimmy/custom-elements-runtime release, synchronize the plugin with the npm latest tag:

npm run update:runtime

The command updates the plugin's peer and development dependency ranges, the installed runtime record in package-lock.json, and every package.json.tpl used by create-cer-app. Template manifests are discovered recursively, so newly added SPA, SSR, SSG, or future build-mode templates are included automatically. The update aborts before writing if any required reference is missing, registry metadata is incomplete, or the published runtime declares dependencies. Run npm install or npm ci afterward when the local node_modules tree also needs to be refreshed; CI already does this from the updated lockfile.

For a read-only CI/release check, use npm run check:runtime. It exits non-zero and lists stale files when an update is required. If npm's latest tag has not propagated yet, a known published version can be selected explicitly with npm run update:runtime -- --version 3.9.2.


License

MIT

See production content, islands and quality checks for complete Markdown SSR, selective content imports, Vite preview, static CSS and release gates.

Related Packages

@mono-lit/helper

Lit web components (tables, forms, inputs, dialogs, charts) for Vue and Nuxt, wi...

769 0
@skeehn/wc

Framework-agnostic Web Components for skeehn — custom elements that work in Vue,...

318 0
pl-mobile

a mobile component library for Vue.js.

1,443 9
@wc-toolkit/cem-generator-vue

Vue detector plugin for generating Custom Elements Manifests.

1,401 1
@corvina/web-components-library

Corvina web components (custom elements)

1,165

Version History

Version Released License
0.29.0 MIT
0.28.3 MIT
0.28.2 MIT
0.28.1 MIT
0.28.0 MIT