Extending Your Site

Your site is a Vite app. That means the entire Vite plugin ecosystem is available to you — PWA support, sitemaps, image optimization, bundle analysis, and anything else the community has built. You don't need Uniweb to support these features specifically. You install a plugin, register it in your vite.config.js, and it works.

This guide covers how to add Vite plugins to a site, walks through PWA as a worked example, and maps out the different extensibility layers so you know which one fits what you're trying to do.


Adding Vite Plugins

Every site has a vite.config.js that looks like this:

import { defineSiteConfig } from '@uniweb/build/site'

export default defineSiteConfig()

defineSiteConfig() returns a full Vite config with everything Uniweb needs — React, Tailwind, reading your pages and records, routing. It also accepts standard Vite options, including a plugins array. Your plugins run alongside the built-in ones:

import { defineSiteConfig } from '@uniweb/build/site'
import myPlugin from 'some-vite-plugin'

export default defineSiteConfig({
  plugins: [myPlugin()],
})

To use a Vite plugin, you install it as a dev dependency in your site package (not the foundation — the site is the Vite app that builds the final output):

cd site
pnpm add -D some-vite-plugin

Then import and register it in vite.config.js. That's the full pattern. The rest of this guide applies it to specific cases.


Worked Example: PWA Support

A Progressive Web App needs three things: a web app manifest (tells the browser the app's name, icons, colors), a service worker (handles offline caching and updates), and registration code (connects the service worker to the page). The Vite ecosystem has a plugin that handles all three.

Step 1: Install the plugin

From your site directory:

cd site
pnpm add -D vite-plugin-pwa

This adds vite-plugin-pwa to your site's devDependencies.

Step 2: Configure it

Open site/vite.config.js and register the plugin:

import { defineSiteConfig } from '@uniweb/build/site'
import { VitePWA } from 'vite-plugin-pwa'

export default defineSiteConfig({
  plugins: [
    VitePWA({
      registerType: 'autoUpdate',
      workbox: {
        navigateFallback: null,  // Required for localized sites (see below)
      },
      manifest: {
        name: 'My Site',
        short_name: 'MySite',
        description: 'A brief description of your site',
        theme_color: '#ffffff',
        icons: [
          {
            src: 'pwa-192x192.png',
            sizes: '192x192',
            type: 'image/png',
          },
          {
            src: 'pwa-512x512.png',
            sizes: '512x512',
            type: 'image/png',
          },
        ],
      },
    }),
  ],
})

Step 3: Add icons

Place your PWA icon files in site/public/:

site/
├── public/
│   ├── pwa-192x192.png
│   └── pwa-512x512.png
├── vite.config.js
└── ...

Files in public/ are served at the site root. The manifest references them by filename.

Step 4: Build and verify

pnpm build
pnpm preview

Open DevTools → Application → Manifest to confirm the manifest loaded. The Service Workers panel shows the registered worker. On a second visit, the site works offline.

Customizing further

vite-plugin-pwa has options for workbox strategies (cache-first vs network-first), custom service worker code, and update prompts. See the vite-plugin-pwa documentation for the full API. Everything it supports works inside defineSiteConfig() — there's nothing Uniweb-specific to account for.

If your site uses a base path for subdirectory deployment (base: /docs/ in site.yml), the plugin picks it up automatically from the Vite config. No extra configuration needed.

Localized sites: disable navigateFallback

If your site uses multiple locales (i18n: in site.yml), you need navigateFallback: null in the workbox config — this is already included in the example above.

Without it, the service worker intercepts every navigation request and returns the root index.html. That breaks localized routes: /fr/about, /es/contact, and any direct navigation to a non-default locale will 404 or load the wrong language. The symptom is that language switching stops working after the service worker installs (typically on the second visit).

Setting navigateFallback: null tells the service worker to stay out of navigation. Static assets are still cached for offline use — only the navigation interception is disabled.

If you've already deployed without this setting, returning visitors may have a cached service worker. They'll need to clear it: DevTools → Application → Service Workers → Unregister, then hard refresh.


Other Common Plugins

The same install-and-register pattern works for any Vite plugin. A few that site authors commonly reach for:

Sitemap generation:

cd site
pnpm add -D vite-plugin-sitemap
import { defineSiteConfig } from '@uniweb/build/site'
import sitemap from 'vite-plugin-sitemap'

export default defineSiteConfig({
  plugins: [
    sitemap({ hostname: 'https://example.com' }),
  ],
})

Compression (gzip/brotli):

cd site
pnpm add -D vite-plugin-compression
import { defineSiteConfig } from '@uniweb/build/site'
import compression from 'vite-plugin-compression'

export default defineSiteConfig({
  plugins: [compression()],
})

Each plugin's npm page or GitHub README explains its options. Install it, register it, done.


Extensibility Layers

Vite plugins are one of several ways to extend a Uniweb site. Each layer solves a different kind of problem:

Vite plugins — build-time site features

What you've seen above. Plugins run during the Vite build and can generate files, transform assets, inject HTML, and hook into the dev server. Use this for anything that's a build concern: PWA, sitemaps, compression, image optimization, bundle analysis, environment variable handling.

Installed to: site's devDependencies Configured in: site/vite.config.js

head.html — external scripts and tags

A head.html file in your site root injects raw HTML into every page's <head>. No bundling, no build step — the content is inserted as-is. Use this for third-party scripts that provide their own loading mechanism: analytics, chat widgets, consent banners, custom fonts, verification meta tags.

<!-- site/head.html -->
<script defer data-domain="example.com" src="https://plausible.io/js/script.js"></script>
<meta name="google-site-verification" content="..." />

Installed to: nothing — just a file Configured in: site/head.html

Applies when the framework builds your pages (uniweb export, uniweb deploy --host=<adapter>). On Uniweb Cloud (uniweb publish) the host generates the pages from your synced content, so head.html is not applied there — see Custom Head Injection.

Extensions — additional section types

Extensions are secondary foundations loaded at runtime. They contribute section types that content authors can use in markdown, alongside the primary foundation's types. Use this when you want to add a pack of components (charts, effects, embeds) without modifying the primary foundation.

# site/site.yml
extensions:
  - https://cdn.example.com/charts/entry.js

Extensions can only add section types. They don't provide layouts, theme variables, or site-level behavior. They're structurally identical to foundations — same build, same output — but loaded as supplements.

An extension declares itself in its main.js:

// effects/main.js
export default {
  extension: true
}

The build writes role: "extension" to the extension's schema.json and warns if the extension declares theme variables or layouts (since those belong to the primary foundation).

Installed to: nothing — loaded by URL Configured in: site/site.yml

Foundation npm dependencies — libraries used by components

Components in your foundation can use any npm package. Charting libraries, animation frameworks, date utilities — install them in the foundation and import them in your components. They're bundled into dist/entry.js by Vite and tree-shaken per foundation.

cd src
pnpm add framer-motion
// src/sections/AnimatedHero.jsx
import { motion } from 'framer-motion'

export default function AnimatedHero({ content }) {
  return (
    <motion.h1 initial={{ opacity: 0 }} animate={{ opacity: 1 }}>
      {content.title}
    </motion.h1>
  )
}

Installed to: foundation's dependencies Configured in: nothing — standard imports


Choosing the Right Layer

You want to...Use
Add offline support, generate sitemapsVite plugin (site)
Optimize images at build timeVite plugin (site)
Report page views and usage eventsservices.tracking (site.yml)
Load a vendor's tracking script (a <script src>)services.tracking.scripts (site.yml) — works on every host
Run a snippet with inline config, or a chat widgethead.html — only where the framework builds your pages
Add a verification meta taghead.html
Use a third-party component packExtension (site.yml)
Use an animation library in componentsnpm package (foundation)
Use a data visualization librarynpm package (foundation)

The layers don't overlap much. Build-time features are Vite plugins. External scripts are head.html. Component libraries are foundation dependencies. Additional component packs from a URL are extensions.


See Also