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.

html
Code copied!
  &lt;!DOCTYPE html&gt;
  &lt;html lang="en"&gt;
  &lt;head&gt;
    &lt;title&gt;&lt;/title&gt;
  &lt;/head&gt;
  &lt;body&gt;
    &lt;h1&gt;&lt;/h1&gt;
    &lt;p&gt;&lt;/p&gt;
  
    &lt;script type="module"&gt;
      export async function server ({ route }) {
        return {
          title: `Welcome to Coralite (${route.path})`,
          summary: 'Fast, native Web Component framework with zero build-time bloat.'
        }
      }
    &lt;/script&gt;
  &lt;/body&gt;
  &lt;/html&gt;

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:

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.

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.

javascript
Code copied!
  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.

javascript
Code copied!
  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' }
  }

Key rules:

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.

javascript
Code copied!
  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:

8. The route Context Key #

The route object provides normalized route metadata and parameters across server rendering, getters, client logic, and error boundaries.

typescript
Code copied!
  type Route = {
    path: string
    pattern: string
    params: Record<string, string>
    dynamic: boolean
  }
      </string,>

Reading route inside page blocks:

javascript
Code copied!
  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:

javascript
Code copied!
  import { defineConfig } from 'coralite'
  
  export default defineConfig({
    cleanUrls: true // default: true
  })

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):

html
Code copied!
  &lt;nav&gt;
    &lt;a href="/"&gt;Home&lt;/a&gt;
    &lt;a href="/about"&gt;About&lt;/a&gt;
    &lt;a href="/posts/123"&gt;Read Post&lt;/a&gt;
  &lt;/nav&gt;

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:

javascript
Code copied!
  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.

javascript
Code copied!
  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:

javascript
Code copied!
  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:

javascript
Code copied!
  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
    })
  }

The router dispatches two CustomEvents on window (bubbling and composed) with a detail payload of { from, to }:

Behavior Contract #

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.

Start Building with Coralite!

Use the scaffolding script to get jump started into your next project with Coralite

Copied commandline!