Pages & Routing Reference
This reference details the page configuration API and file-based routing system in Coralite. Pages are HTML files located in your project's pages directory that render top-level route targets and export optional server-side configuration, path enumeration logic, and runtime behavior.
1. Page Structure #
A Coralite page file can combine static HTML markup, server-side data fetching, path enumeration, declared page information, and optional client scripts. Pages are configured with flat named exports inside a <script type="module"> block: export function server, export const meta, export const getters, export function client, and so on. export default definePage({ ... }) remains supported as a legacy shape.
<!DOCTYPE html>
<html lang="en">
<head>
<title></title>
</head>
<body>
<h1></h1>
<p></p>
<script type="module">
export async function server ({ route }) {
return {
title: `Welcome to Coralite (${route.path})`,
summary: 'Fast, native Web Component framework with zero build-time bloat.'
}
}
</script>
</body>
</html>
Static Page Exemption: Pages that require no dynamic data fetching or path enumeration can be authored as pure HTML documents with no <script type="module"> block. Coralite processes static HTML pages directly during build time.
2. File-Based Routing #
Coralite maps HTML file paths inside your pages directory directly to URL route targets:
pages/about.html→/aboutpages/index.html→/pages/blog/index.html→/blog
Subdirectory structures establish nested URL paths. Trailing index.html files serve as root pages for their containing directory.
3. Dynamic Routes #
Dynamic route parameters are declared using square brackets ([param]) in file or directory names. Parameter values are extracted during routing and passed into page context objects via route.params.
pages/posts/[id].html→/posts/:id(e.g./posts/123→route.params.id = '123')pages/[year]/[month].html→/:year/:month(e.g./2025/03→route.params.year = '2025',route.params.month = '03')pages/posts/[id]/index.html→ same aspages/posts/[id].htmlpages/docs/[...slug].html→ catch-all route matching/docs,/docs/a,/docs/a/b(e.g.route.params.slug = 'a/b')
Precedence Rules: When resolving incoming request URLs against defined route patterns, Coralite uses left-to-right segment precedence: static segments win over param segments, which win over catchall segments.
4. Enumeration via paths #
Dynamic pages must declare the exact parameter combinations to compile during build time by exporting a paths function.
Context Object: The paths function receives { app } context and can be synchronous or async. It must return an array of parameter objects matching the dynamic route variables.
export async function paths ({ app }) {
console.log('Enumerating paths for app:', app)
const fs = await import('node:fs/promises')
const raw = await fs.readFile('./content/posts.json', 'utf-8')
const posts = JSON.parse(raw)
return posts.map((post) => ({
id: post.id
}))
}
export async function server ({ route }) {
// Route parameters extracted per enumerated path
const postId = route.params.id
return {
postId
}
}
Plugin Path Enumeration: Plugins can also contribute route paths during collection by returning { paths: [...] } from an onPageSet hook. Paths returned by the page paths() function and plugin onPageSet returns are deduplicated automatically. If duplicate parameter sets originate from different sources or within the same source, Coralite raises a build error identifying the conflicting sources.
5. meta on Pages #
Components declare host inputs with attributes. Pages have no host element, so their public information surface is the document itself: <title>, <meta>, <html lang>, and any other page-level key/value information. The meta block declares that information and seeds the page's default state.
meta uses the same schema syntax as component attributes (type, default, values, validate). Keys are arbitrary because meta tags are generic key/value properties; keys are not limited to SEO fields.
export const meta = {
title: { type: String, default: 'Untitled' },
description: { type: String, default: '' },
lang: { type: String, default: 'en' },
count: { type: Number, default: 0 },
campaign: { type: String, default: 'organic' }
}
- State defaults: every resolved
metavalue seeds page state. Templates, getters, and client code read it as,state.count, and so on. - State precedence:
metadefaults are the base; existing render state andonPageSet/onBeforePageRenderhooks override them; theserver()return value overrides last. - page.meta precedence: literal
<title>/<meta>tags remain authoritative.metadefaults fill in missing keys only. No<title>or<meta>tags are rendered automatically. - Validation:
requiredis rejected on pages because there is no external input. Invalid defaults (failing type coercion,values, orvalidate) fail the build. - Component-only blocks:
attributesremains component-only and is rejected on pages.
Key rules:
- Verbatim keys: keys are used exactly as written in both state and
page.meta. Only identifier-safe keys are directly addressable as template tokens; use a getter for keys such asog:title,campaign-name, or dotted keys. - Reserved collisions: a
pagedefault is available atpage.meta.pagebut cannot seedstate.pagebecause the page object wins. Aroutedefault is available atpage.meta.route, but[object Object]always resolves the active route object. Astatedefault createsstate.state. - No default, no value: a schema entry without a default creates no state key and no
page.metaentry. - server() return values: returned keys update state only. To change page metadata, mutate
page.metadirectly insideserver()or a hook. - Render-scoped metadata: when defaults are declared, the effective
page.metais assembled per render. The page's literal metadata remains the source of truth, so removing a default does not leave a stalepage.metavalue.
The block is optional. A page that only needs static defaults can declare meta without a server() function.
6. server() on Pages #
The server() block executes once for each enumerated path during build-time rendering. It retrieves page-level data, reads filesystem files, or runs build queries to construct the page's initial state object.
Context Object: Receives { state, route, app, session, signal, page, ...pluginContext }. state contains the resolved meta defaults plus any earlier render state, and page.meta contains the extracted document metadata.
export async function server ({ state, route }) {
const { id } = route.params
return {
title: state.title,
articleId: id,
fetchedAt: new Date().toISOString()
}
}
Data returned from server() is merged into the page's reactive state and serialized for hydration in the browser. Returned keys do not update page.meta automatically; mutate page.meta directly when you need to change page metadata.
7. getters, style, provide, client, onError on Pages #
Pages support the same core context blocks as components, acting as the root node of the application context tree:
getters: Synchronous derived state functions destructuring({ state, route, refs, slots, signal, isServer }).style: Dynamic CSS Custom Property map applied synchronously to the<html>element scope:(state) => ({ '--page-theme': state.theme }).provide: Broadcasts context values down to descendant components via W3C Context protocol:provide ({ state }) { return { theme: state.theme } }.client: Client-side runtime logic executed ondocument.documentElementreceiving{ state, route, refs, signal, emit, observe }(supports lazy dependency stylesheet imports viaawait import('*.css')).onError: Build-time error boundary returning fallback template markup and state if server rendering fails:onError ({ error }) { return { template: '.Page Unavailable
', state: { err: error.message } } }
8. The route Context Key #
The route object provides normalized route metadata and parameters across server rendering, getters, client logic, and error boundaries.
type Route = {
path: string
pattern: string
params: Record<string, string>
dynamic: boolean
}
</string,>
Reading route inside page blocks:
export const getters = {
canonicalUrl: ({ route }) => `https://example.com${route.path}`,
activeTab: ({ route }) => route.params.id || 'overview'
}
export function client ({ route }) {
console.log('Page mounted at route:', route.path, 'with params:', route.params)
}
9. URL Conventions (cleanUrls) #
The cleanUrls option in coralite.config.js controls how build output files are named and how route.path is represented:
import { defineConfig } from 'coralite'
export default defineConfig({
cleanUrls: true // default: true
})
cleanUrls: true(default): Directory-style output.pages/about.htmlcompiles todist/about/index.htmlwith extensionlessroute.path = '/about'.cleanUrls: false: File-style output.pages/about.htmlcompiles todist/about.htmlwithroute.path = '/about.html'.- Root page (
pages/index.html) always compiles todist/index.htmlwithroute.path = '/'regardless ofcleanUrlsconfiguration.
10. Navigation #
By default, navigation between pages uses standard HTML anchor tags (<a href="...">) triggering full document loads accompanied by smooth View Transitions (crossfade animation):
<nav>
<a href="/">Home</a>
<a href="/about">About</a>
<a href="/posts/123">Read Post</a>
</nav>
View Transitions are enabled by default via the viewTransitions: true option in project configuration, delivering native CSS crossfade transitions without client-side routing overhead.
11. 404 Convention #
Creating a pages/404.html file at the root of your pages directory defines the standard 404 error page. It compiles directly to dist/404.html with route.path = '/404', making it instantly compatible with static web hosts (GitHub Pages, Netlify, Vercel, Cloudflare Pages).
Note: Subdirectory 404 files (such as pages/docs/404.html) are treated as standard content pages rather than root fallback pages.
12. Redirects #
While host-level web server redirects (e.g. _redirects or server headers) are recommended for production performance, pages can emit static HTML redirect stubs directly from their server() block by returning a __redirect object:
export async function server ({ route }) {
console.log('Redirecting from route:', route.path)
return {
__redirect: {
to: '/docs/getting-started',
status: 301 // 301 permanent or 302 temporary (default: 302)
}
}
}
When __redirect is returned, Coralite generates a lightweight HTML stub containing a <meta http-equiv="refresh"> tag, canonical <link> header, and instant location.replace() JavaScript fallback.
13. SPA Router (opt-in) #
For applications requiring persistent client-side state, audio playback, or continuous DOM trees across page navigations, Coralite provides an opt-in single-page application (SPA) router plugin via coralite/plugins/router.
import { defineConfig } from 'coralite'
import router from 'coralite/plugins/router'
export default defineConfig({
plugins: [
router({
base: '/',
outlet: 'data-router-outlet',
autoWrap: true,
outletOnly: false
})
]
})
The router plugin automatically wraps body contents in a <router-outlet> element, intercepts same-origin <a href> clicks, fetches target HTML documents via fetch(), swaps outlet DOM nodes, and updates document head tags without full page reloads.
Configuration Options #
The plugin factory accepts two string options and two boolean options (invalid types throw at registration):
| Option | Type | Default | Description |
|---|---|---|---|
base |
String | '/' |
Declared base path for the router. |
outlet |
String | 'data-router-outlet' |
The attribute name marking a page element as the outlet. Any element carrying this attribute (or a <router-outlet> tag) defines the SPA swap region. |
autoWrap |
Boolean | true |
When false, outlet-less pages are not automatically wrapped in a <router-outlet>. The page renders as authored; fetched SPA routes still need an outlet. |
outletOnly |
Boolean | false |
When true, link interception is skipped when the current document has no outlet, so clicks use native browser navigation. This guard does not affect router.navigate(). |
Outlet & Swap Region #
During page renders, if the router is active and a page declares no outlet, the plugin automatically wraps the page's full body content in a <router-outlet data-router-outlet> element (pages that already contain an outlet are never double-wrapped). Set autoWrap: false to skip that wrapper; outlet-less pages then render as authored and still resolve tokens normally. The plugin also registers a virtual <router-outlet> component, so outlets can live inside layout components as well as directly in pages.
On each client-side navigation, only the outlet's children are replaced. Elements outside the outlet — persistent navbars and footers, background audio players, live WebSockets — are never touched by the router.
Client navigation requires an outlet in both the live and fetched documents. Combine autoWrap: false with outletOnly: true for routes that should stay outside the SPA shell: their links fall through to native browser navigation, while router.navigate() remains unaffected and will still reject after updating history if called without an outlet.
Note: Registration is required — a data-router-outlet element without a registered plugin is inert and does not alter navigation behavior.
Client Router API #
The router API is injected into component and page client() blocks under the router key, and is also available at window.__coralite__.plugins.router:
router.navigate(path, { replace = false })— performs a client-side navigation: runs guards and thecoralite:navigatingevent, updates the history entry (pushState, orreplaceStatewithreplace: true), fetches the target page, swaps the outlet and head content, then re-hydrates the new outlet. Returns once the swap has settled.router.current()— returns{ path }, the current browser pathname.router.beforeEach(guard)— registers a navigation guard and returns an unregister function to remove it later. Guards receive({ to: { path }, from: { path }, signal })and may be asynchronous. Guard return values:false— cancel the navigation.- a relative path string (e.g.
'/login') — redirect: the router re-navigates client-side withreplace: true. Nested guard redirects are limited to a depth of 10; deeper chains are aborted (the dev runtime logs a redirect-loop error). - an absolute URL string (
http:///https://) — hard redirect viawindow.location.replace(). trueorundefined— allow the navigation.
export function client ({ router }) {
// Push a new history entry
router.navigate('/app/dashboard')
// Inspect the current route
const { path } = router.current()
// Replace instead of pushing (e.g. post-redirect flow)
router.navigate('/app/redirect-target', { replace: true })
}
Navigation guards can be registered using beforeEach inside client logic:
export function client ({ router }) {
router.beforeEach(({ to, from }) => {
console.log('Navigating from', from.path, 'to', to.path)
if (to.path.startsWith('/admin') && !isAuthenticated()) {
return '/login' // Relative redirect to /login
}
// Return true (or undefined) to allow navigation
return true
})
}
Navigation Events #
The router dispatches two CustomEvents on window (bubbling and composed) with a detail payload of { from, to }:
coralite:navigating— dispatched before the target page is fetched. It is cancelable: callingpreventDefault()on the event aborts the navigation.coralite:navigated— dispatched after a successful swap. Use it for post-navigation side effects (analytics, scroll management, third-party widget sync).
Behavior Contract #
- Link interception. Left-clicks on same-origin
<a href>links become client-side navigations. Interception is skipped for modifier-key clicks (meta/ctrl/shift/alt), non-_selftargetvalues,downloadlinks, and links marked withdata-router-ignore(the per-link opt-out that keeps native full document loads). Cross-origin links are never intercepted. - Back/forward. Browser history navigation (
popstate) re-navigates to the current path withreplace: true, so every back/forward step re-fetches and renders the route's pre-built static HTML. - Head reconciliation. After each navigation the live document's
<title>and<meta name|property>tags are reconciled with the fetched page: added, updated, or removed accordingly. - Component re-hydration. The fetched page's hydration payload (
<script data-coralite-hydration>) is parsed and installed into the shared hydration store before the swap; the new outlet content is then upgraded, so every SPA navigation yields fully re-hydrated component instances. - Scroll position. The window scrolls to the top after each navigation unless the target path contains a
#hash fragment. - Fetch contract. SPA navigation requests carry the
X-Coralite-Router: trueheader so hosts, service workers, or edge functions can distinguish client route fetches from initial loads. Non-OK responses (or a fetched document without an outlet) fail the navigation with an error. - View Transitions. When the project sets
viewTransitions: true, the browser supportsdocument.startViewTransition, and the user has not enabledprefers-reduced-motion, the outlet swap runs inside a native View Transition (crossfade); otherwise the swap is a direct DOM replacement. This is distinct from the native MPA crossfades between full page loads described in Section 9.
For the full API reference, see the Router Plugin Reference. For detailed SPA routing patterns, persistent state across route swaps, and production pitfalls, see the SPA Router Guide.