Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,8 @@ $ npm --workspace @domstack/basic-example run build

Each example is an npm workspace under `examples/*`, so a root `npm install` links the local `@domstack/static` package into example builds. Example packages keep `@domstack/static` declared as `file:../../.` so the local dependency target remains explicit.

The [`examples/pwa`](./examples/pwa) workspace shows a hand-rolled static PWA that consumes the unstable domstack manifest preview directly.

### Additional examples

Here are some additional external examples of larger domstack projects.
Expand Down Expand Up @@ -1087,7 +1089,8 @@ const html = renderCache.get(page.pageInfo.path) ?? ''
Every programmatic build returns a `domstackManifest` object in the build results. The CLI also writes
`domstack-manifest.json` into the destination directory by default. Applications can use this manifest
as the source of truth for service-worker precaching, deployment metadata, or other static output
integrations.
integrations. See [`examples/pwa`](./examples/pwa) for a hand-rolled static PWA implementation that
consumes the domstack manifest directly.

The manifest is a normalized list of files that domstack emitted:

Expand Down
62 changes: 62 additions & 0 deletions examples/pwa/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
# DOMStack PWA Example

This example shows a production-style static PWA using Domstack's domstack manifest and first-class service-worker build support.

> [!WARNING]
> The domstack manifest and first-class service-worker support shown here are unstable preview features.
> The option names, manifest schema, generated outputs, and browser defines may change outside of a major version while the API is validated.
> Pin `@domstack/static` to an exact version before building long-lived integrations on this preview contract.

It demonstrates:

- A `service-worker.js` source that Domstack bundles to `/service-worker.js`.
- A `domstack-manifest.settings.js` file that filters the generated domstack manifest.
- A global client runtime that registers the worker, handles update prompts, and disables sticky caches during local watch development.
- Offline precaching for app, docs, legal-style pages, static assets, shared chunks, and the web app manifest.
- Excluding `/blog/**`, `/admin/**`, source maps, metadata files, and pages with `precache: false` or `offline: false`.
- Verbose console logging in the window runtime and service worker so the lifecycle is easy to inspect while learning or testing.

## Running

```bash
cd examples/pwa
npm install
npm run serve
```

Service workers require a secure origin. `localhost` is allowed by browsers, and `npm run serve`
runs a manifest-enabled build so the PWA path works there. It also serves without live-reload HTML
injection so fetched files match the domstack manifest revisions. Wait until the UI says `Offline
cache current` before testing an offline refresh. Use `npm run watch` for development without a
sticky service worker cache. To clear all example workers and caches:

```txt
/?reset-sw=1
```

## Files

```txt
src/
global.client.js # Registers the service worker through pwa/runtime.js
global.css # Site styles
global.vars.js # Shared page variables
domstack-manifest.settings.js # Filters the written domstack manifest
manifest.webmanifest # Web app manifest copied as a static asset
service-worker.js # Site service worker entry
pwa/
cache-policy.js # Shared domstack manifest filtering and constants
runtime.js # Browser registration/update/recovery behavior
sw/
*.js # Service-worker install, update, cache, and fetch helpers
```

## Production Pattern

The worker fetches `/domstack-manifest.json` during installation and uses the revisioned URLs in that file as its cache plan. The manifest is generated after Domstack reconciles pages, bundles, chunks, worker output, copied static assets, and templates. Application policy stays in app code:

- `domstack-manifest.settings.js` decides what can ever enter the manifest.
- `service-worker.js` decides how to install, activate, update, and serve cached responses.
- `global.client.js` decides when to register, prompt, apply updates, or recover from a bad cache.

Watch mode does not write the domstack manifest, so use `npm run serve` when testing the offline lifecycle.
30 changes: 30 additions & 0 deletions examples/pwa/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
{
"name": "@domstack/pwa-example",
"version": "0.0.0",
"description": "DOMStack PWA offline cache example",
"type": "module",
"scripts": {
"start": "npm run serve",
"build": "npm run clean && domstack",
"clean": "rm -rf public && mkdir -p public",
"serve": "npm run clean && domstack --serve",
"watch": "npm run clean && dom --watch"
},
"keywords": [
"domstack",
"pwa",
"service-worker",
"offline"
],
"author": "",
"license": "MIT",
"dependencies": {
"@domstack/static": "file:../../."
},
"devDependencies": {
"npm-run-all2": "^9.0.0"
},
"engines": {
"node": "^22.0.0 || >=24.0.0"
}
}
79 changes: 79 additions & 0 deletions examples/pwa/src/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
---
title: App Shell
---

<section class="app-layout">
<div class="app-summary">
<div>
<h1>Static PWA shell</h1>
<p>This page is built as ordinary static HTML, then the service worker uses Domstack's domstack manifest to cache the static shell for offline launches.</p>
<div class="pill-row" aria-label="Cache policy">
<span class="pill ok">Pages</span>
<span class="pill ok">Bundles</span>
<span class="pill ok">Static assets</span>
<span class="pill warn">No API cache</span>
</div>
</div>
<div class="status-list" aria-live="polite">
<div class="status-row">
<span>Worker</span>
<strong data-pwa-status>Not registered</strong>
</div>
<div class="status-row">
<span>Cache version</span>
<strong data-pwa-version>Waiting for domstack manifest</strong>
</div>
<div class="status-row">
<span>Network</span>
<strong data-online-state>Checking</strong>
</div>
</div>
</div>

<ul class="route-grid" aria-label="Example routes">
<li class="route-card">
<h2>Docs</h2>
<p>Docs are part of the first offline bundle.</p>
<a class="button" href="/docs/">Open docs</a>
<div class="pill-row"><span class="pill ok">Precached</span></div>
</li>
<li class="route-card">
<h2>Legal</h2>
<p>Legal-style static pages use the same offline policy as docs.</p>
<a class="button" href="/legal/">Open legal</a>
<div class="pill-row"><span class="pill ok">Precached</span></div>
</li>
<li class="route-card">
<h2>Login</h2>
<p>Auth shells can load offline while submissions remain network-only.</p>
<a class="button" href="/login/">Open login</a>
<div class="pill-row"><span class="pill ok">Precached</span></div>
</li>
<li class="route-card">
<h2>Offline fallback</h2>
<p>Excluded navigations fall back to a small static page.</p>
<a class="button" href="/offline/">Open fallback</a>
<div class="pill-row"><span class="pill ok">Precached</span></div>
</li>
<li class="route-card">
<h2>Blog</h2>
<p>Blog pages are intentionally left out to reduce first install cost.</p>
<a class="button secondary" href="/blog/">Open blog</a>
<div class="pill-row"><span class="pill warn">Excluded</span></div>
</li>
<li class="route-card">
<h2>Admin</h2>
<p>Protected routes should stay network-only.</p>
<a class="button secondary" href="/admin/">Open admin</a>
<div class="pill-row"><span class="pill warn">Excluded</span></div>
</li>
<li class="route-card">
<h2>Opted-out</h2>
<p>Page vars can opt a static route out of precaching.</p>
<a class="button secondary" href="/private/">Open page</a>
<div class="pill-row"><span class="pill warn">Excluded</span></div>
</li>
</ul>

<aside class="pwa-update" data-pwa-update hidden aria-live="polite"></aside>
</section>
11 changes: 11 additions & 0 deletions examples/pwa/src/admin/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
title: Admin
precache: false
offline: false
---

# Admin

This protected route is intentionally excluded from the PWA precache by both path policy and page vars.

Admin traffic should stay network-only in this example.
13 changes: 13 additions & 0 deletions examples/pwa/src/blog/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
---
title: Blog
precache: false
offline: false
---

# Blog

This route is present in the static site but filtered out of the first PWA cache by `domstack-manifest.settings.js`.

Its page vars also set `precache: false` and `offline: false`, which lets the cache policy reject it without relying only on path names.

- [First post](/blog/first-post/)
9 changes: 9 additions & 0 deletions examples/pwa/src/blog/first-post/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
title: First Post
precache: false
offline: false
---

# First Post

Blog content exists in the generated site, but the example cache policy excludes `/blog/**` from the first install cache.
23 changes: 23 additions & 0 deletions examples/pwa/src/docs/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
---
title: Docs
---

# Docs

This route is intentionally included in the PWA cache. It represents static documentation, legal pages, help pages, or other public content that should remain available after the first online visit.

The service worker gets this page from the generated domstack manifest instead of a handwritten list.

## Cache Inputs

- Page HTML, including `/docs/`
- Global CSS and JavaScript bundles
- Shared chunks
- Static icons and the web app manifest

## Cache Exclusions

- `/api/**` requests
- `/admin/**` routes
- `/blog/**` routes
- Source maps and Domstack metadata
22 changes: 22 additions & 0 deletions examples/pwa/src/domstack-manifest.settings.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
/**
* @import { DomstackManifestEntry } from '@domstack/static'
*/

import { pwaManifestExclude, shouldIncludePwaOutput } from './pwa/cache-policy.js'

const origin = 'https://example.com'

export default {
exclude: pwaManifestExclude,
includeEntry,
}

/**
* Keep PWA cache policy close to the application while still letting Domstack
* emit a normal domstack manifest for the service worker to consume.
*
* @param {DomstackManifestEntry} entry
*/
function includeEntry (entry) {
return shouldIncludePwaOutput(entry, origin)
}
5 changes: 5 additions & 0 deletions examples/pwa/src/global.client.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { initializePwa } from './pwa/runtime.js'

initializePwa().catch(err => {
console.warn('Unable to initialize the Domstack PWA example runtime', err)
})
Loading