@ofidj/generator-fidj 1.0.1 → 1.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (27) hide show
  1. package/README.md +58 -14
  2. package/bin/create-fidj.cjs +9 -1
  3. package/generators/app/index.js +57 -15
  4. package/generators/app/templates/typescript/README.md +12 -2
  5. package/generators/app/templates/typescript/app.config.json +5 -1
  6. package/generators/app/templates/typescript/package.json +1 -1
  7. package/generators/app/templates/typescript/public/fidj-logo.png +0 -0
  8. package/generators/app/templates/typescript/public/fonts/IBMPlexMono-400-latin-ext.woff2 +0 -0
  9. package/generators/app/templates/typescript/public/fonts/IBMPlexMono-400-latin.woff2 +0 -0
  10. package/generators/app/templates/typescript/public/fonts/IBMPlexMono-500-latin-ext.woff2 +0 -0
  11. package/generators/app/templates/typescript/public/fonts/IBMPlexMono-500-latin.woff2 +0 -0
  12. package/generators/app/templates/typescript/public/fonts/IBMPlexSans-400_600-latin-ext.woff2 +0 -0
  13. package/generators/app/templates/typescript/public/fonts/IBMPlexSans-400_600-latin.woff2 +0 -0
  14. package/generators/app/templates/typescript/public/fonts/InstrumentSerif-400-latin-ext.woff2 +0 -0
  15. package/generators/app/templates/typescript/public/fonts/InstrumentSerif-400-latin.woff2 +0 -0
  16. package/generators/app/templates/typescript/scripts/build.mjs +3 -0
  17. package/generators/app/templates/typescript/scripts/render-content.mjs +2 -2
  18. package/generators/app/templates/typescript/server/index.ts +10 -1
  19. package/generators/app/templates/typescript/src/content.ts +128 -37
  20. package/generators/app/templates/typescript/src/fonts.css +92 -0
  21. package/generators/app/templates/typescript/src/main.ts +31 -9
  22. package/generators/app/templates/typescript/src/service-agreement.ts +51 -0
  23. package/generators/app/templates/typescript/src/style.css +640 -363
  24. package/generators/app/templates/typescript/src/tokens.css +56 -0
  25. package/lib/app-module.cjs +6 -1
  26. package/lib/scaffold.cjs +72 -3
  27. package/package.json +2 -6
package/README.md CHANGED
@@ -2,10 +2,12 @@
2
2
 
3
3
  Generate a Fidj-integrated TypeScript app with a short command. Public identity and content are generator inputs; authentication and per-app privacy are reusable template behavior. Node 22 or 24 is the validation matrix.
4
4
 
5
+ Registry examples require published compatible versions. For current workspace development, supply a built local SDK with `--sdk-path`.
6
+
5
7
  ## Content app — static hosting
6
8
 
7
9
  ```sh
8
- create-fidj my-app --app-id YOUR_FIDJ_ID --title "My App" --welcome "Welcome" --content "<p>About my app</p>" --domain example.com
10
+ npx -y -p @ofidj/generator-fidj -p yo yo @ofidj/fidj my-app --app-id YOUR_FIDJ_ID --title "My App" --welcome "Welcome" --content "<p>About my app</p>" --domain example.com
9
11
  cd my-app
10
12
  npm install && npm run build-prod
11
13
  ```
@@ -18,27 +20,55 @@ Serve `www/` on a static host. It contains your public content, SDK sign-in/acco
18
20
 
19
21
  Omit `--content` to generate Studio Notes: a browser client plus Node backend with private persisted notes. The backend revalidates the session and live app roles for every protected request. Owner/Editor may write; other members may read their own notes. This mode requires Node hosting and is separate from the static content app scenario.
20
22
 
23
+ Departure uses an in-app confirmation that states the membership, consent and notes being removed and preserves the membership when cancelled.
24
+
21
25
  Configure the generated privacy adapter so departure from either Studio Notes or Fidj erases the same app data. Notes persist outside disposable output. Failed cleanup stays pending in Fidj and can be retried; completed receipts remain visible. See the generated README for server-side configuration and persistence limits.
22
26
 
23
27
  ## CLI and local development
24
28
 
25
- The package exposes `create-fidj`; from its checkout use `node bin/create-fidj.cjs`. Yeoman `yo @ofidj/fidj my-app --app-id YOUR_FIDJ_ID` invokes the same scaffolder and accepts the content/title/welcome/domain options. The old `app2021` positional command is a historical reference, not the maintained interface.
29
+ The public entry is `yo @ofidj/fidj`; output goes to `<cwd>/<appname>`. Use the `npx` form above or install once:
30
+
31
+ ```sh
32
+ npm install --no-save @ofidj/generator-fidj yo
33
+ npx --no-install yo @ofidj/fidj my-app --app-id YOUR_FIDJ_ID
34
+ ```
35
+
36
+ Repeat a flag for `--highlight` and `--badge`; the values are read from the
37
+ command line, so a comma inside one is safe. The old `app2021` positional command
38
+ and its Ionic/Cordova templates are gone; the `typescript` template is the only
39
+ one.
40
+
41
+ > Maintainers: `bin/create-fidj.cjs` is the same scaffolder without Yeoman, for
42
+ > tooling that runs from a checkout and cannot install `yo` —
43
+ > `scripts/local-stack.py` and `fidj-app`'s `create:local` use it.
44
+ > `test/parity.test.cjs` fails if either door ever gains an input the other
45
+ > lacks. It is not an entry point to hand to anyone building an app.
26
46
 
27
47
  - `--anonymous true|false`: show or hide anonymous entry in content apps (default: `true`). Set `--anonymous false` for a sign-in-only entry flow, as mleweb does.
48
+ - `--highlight "<heading>|<body>"`: add a numbered cell to the sign-in panel, repeatable up to six. These are the app's own selling points, so an app that passes none simply shows its identity — mleweb passes none, Fidj passes four.
49
+ - `--badge <text>`: add a trust badge under the sign-in form, repeatable up to four, 40 characters each. None are supplied by default — "EU-hosted" or "GDPR art. 17 · 20" are claims about a particular app, not about every app the generator makes.
50
+ - `--logo <image>` and `--favicon <image>`: the app's own marks, copied into `public/brand/`. The logo sits beside the app name at the top of the sign-in panel; the favicon goes in the browser tab. Both fall back to the Fidj mark, so neither is ever blank. Accepts `.png`, `.svg`, `.gif`, `.jpg`, `.webp` or `.ico` under 512KB — an animated GIF works as a logo, and as a favicon in the browsers that animate one.
28
51
  - `--api-endpoint`: select the API (default: hosted sandbox).
29
52
  - `--sdk-path` or `FIDJ_SDK_DIR`: use a built local SDK during coordinated development.
30
53
  - `--local` or `FIDJ_LOCAL=true`: use the loopback API/console. Test credentials stay in the validation guide, outside the content app UI. Supply the matching local app ID; `FIDJ_APP_ID` can override it.
31
54
  - `--replace`: regenerate only a destination containing `.fidj-generated`; unmarked projects are protected.
32
55
 
56
+ The generated shell paints from one design system: `src/tokens.css` holds every
57
+ colour, family and radius, and `src/style.css` may not introduce a literal of
58
+ its own. Fonts are self-hosted under `public/fonts` and served from the build
59
+ manifest, so a generated site stays statically hostable and makes no
60
+ third-party request on sign-in. fidj-app consumes the same tokens, which is why
61
+ a style change lands here first.
62
+
33
63
  Generation writes `.env.example`, `.env` and public `app.config.json`. Static configuration is embedded at build time: regenerate/rebuild when changing endpoints. Notes server configuration is read at runtime. Do not place secrets in any public configuration or content input.
34
64
 
35
- During this unreleased milestone, build `../fidj-node` and pass `--sdk-path ../fidj-node/dist`. Committed templates use registry dependency `@ofidj/node ^3.6.24`; a registry-only install is not validated until coordinated versions are published.
65
+ For unpublished coordinated changes, build the sibling SDK and pass `--sdk-path` with its absolute `dist` path. Committed templates use registry dependency `@ofidj/node ^3.6.24`; a registry-only install is not validated until coordinated versions are published.
36
66
 
37
67
  ## Real validation repository
38
68
 
39
69
  [mleweb](https://github.com/mlefree/mleweb) is the thin command-line validation fixture whose generated website is mlefree.com. Its `package.json` passes the original Mario GIF, welcome text and About/CV/contact links directly into this generator. No custom downstream renderer is required. GitHub CI runs the same generation/build path on Node 22/24 and uploads artifacts. Publishing the site to gh-pages, which is what mlefree.com serves, is a separate deliberate job: it runs on mleweb's `master` or on an explicit workflow dispatch, never from a version branch.
40
70
 
41
- Run `npm test` for scaffolding/CLI safety tests. Generated projects include TypeScript checking and HTTP integration tests for live authorization and privacy isolation. Local acceptance steps are in the workspace's `product-plan/09-generated-app-validation.md`.
71
+ Run `npm test` for scaffolding/CLI safety tests. Generated projects include TypeScript checking and HTTP integration tests for live authorization and privacy isolation. See the [local acceptance walkthrough](../LOCAL-DEVELOPMENT.md). Follow TDD: add/run a failing generator or generated-app test before implementation, make it green, then refactor and rerun regression checks.
42
72
 
43
73
 
44
74
  ## Releasing to npm
@@ -49,8 +79,8 @@ with `NPM_TOKEN`; this package publishes from the repository root rather than
49
79
  from a `dist` directory, because it ships its sources.
50
80
 
51
81
  The package contains `bin/`, `lib/` and the `typescript` template only. The
52
- `app2018` and `app2021` Yeoman templates are excluded: nothing reads them, and
53
- they accounted for 11.8MB of the 11.9MB published. `main` points at
82
+ `app2018` and `app2021` Yeoman templates were deleted: nothing read them, and
83
+ they accounted for 11.8MB of the repository. `main` points at
54
84
  `lib/scaffold.cjs`, so `require('@ofidj/generator-fidj')` returns `{ scaffold }`
55
85
  alongside the `create-fidj` binary.
56
86
 
@@ -67,7 +97,7 @@ The generated content app opens on `/#/signin`. Sign in, or choose **Enter anony
67
97
  Build your application for the `/module/` base URL, then pass its public output to the same generator:
68
98
 
69
99
  ```sh
70
- create-fidj my-app --app-id YOUR_FIDJ_ID --title "My App" --welcome "Welcome back" --description "Your app description" --anonymous false --module ./built-console --module-entry 'index.html#/my'
100
+ npx -y -p @ofidj/generator-fidj -p yo yo @ofidj/fidj my-app --app-id YOUR_FIDJ_ID --title "My App" --welcome "Welcome back" --description "Your app description" --anonymous false --module ./built-console --module-entry 'index.html#/my'
71
101
  cd my-app && npm install && npm run build-prod
72
102
  ```
73
103
 
@@ -96,14 +126,28 @@ Generated starters include `npm run privacy:check`: configure the server-only `F
96
126
 
97
127
  Readiness confirms this handler only. Register its trusted URL and independent secret with the Fidj API operator; the app owner console can then check the same connection. Group roles granted in Fidj are enforced by the generated backend on each protected operation.
98
128
 
99
- ## Generate an OIDC app
129
+ ## Beta OIDC input
100
130
 
101
- `--oidc-issuer` switches the shared content/module entry to “Continue with Fidj”. The issuer owns password entry; the generator preserves your title, welcome message, logo/content inputs and anonymous-entry choice.
131
+ `--oidc-issuer` selects the OIDC entry for content/module apps while preserving branding and anonymous-entry configuration. This is beta: the API ships an integrated provider from 3.6.26, but it serves no `/oidc` routes until an operator configures an issuer and signing keys, so confirm the issuer you pass actually answers discovery before offering it.
102
132
 
103
- ```sh
104
- create-fidj my-app --app-id YOUR_APP_ID --api-endpoint https://api.sandbox.fidj.ovh/v3 --oidc-issuer https://api.sandbox.fidj.ovh/oidc --anonymous false --content '<h1>My app</h1>'
105
- ```
133
+ A compatible issuer, REST API and registered callback without a fragment are prerequisites. Validate that integration and coordinated SDK publication before offering it to app builders. Notes retains its existing authentication flow; no subject migration is implied.
134
+
135
+ ## Required service agreement
136
+
137
+ All generated sign-in and account-creation forms (content, composed Fidj and
138
+ Notes) share an unchecked required agreement checkbox. Both submit buttons stay
139
+ disabled until it is checked. Reading the agreement opens an in-app dialog;
140
+ failed agreement loading keeps sign-in blocked. Anonymous entry, when enabled,
141
+ is not a login and does not record agreement acceptance.
106
142
 
107
- An operator must enable that issuer before this command can authenticate. Register the exact deployed entry URL as the callback; it must not contain a fragment. Local loopback HTTP is supported. During the unreleased beta, use the coordinated SDK checkout through `--sdk-path`; registry-only publication is still pending.
143
+ The text and version come from the app's public API metadata, not copied generator
144
+ settings. The API records acceptance before issuing the app token, preserves
145
+ optional choices, and rejects a missing choice or stale version. Repeated login
146
+ with the same version does not duplicate the history. Administrative membership
147
+ creation does not fabricate a user's acceptance.
108
148
 
109
- OIDC currently supports static content and application-module assemblies. The Notes storage fixture retains its existing auth flow; no migration of stored Notes subjects is implied. Static HTML/assets remain public regardless of the login screen. Confidential data needs server authorization, preferably with an HttpOnly backend session. mleweb remains the command-line generator fixture: retain the original Mario and About/CV content, and activate OIDC only after its target issuer and callback are configured.
149
+ Before release, the owner sets `configurationAsJSON.serviceAgreement` to
150
+ `{"version":"2026-09-11","text":"Your complete app service agreement"}` through
151
+ `PUT /apps/:appId`, preserving its other configuration fields. Changing the text
152
+ requires a new version. Until configured, the API exposes the explicitly labelled
153
+ `starter-demo-1` demo agreement. No generated-source edit is necessary.
@@ -13,6 +13,10 @@ try {
13
13
  welcome: { type: "string" },
14
14
  description: { type: "string" },
15
15
  content: { type: "string" },
16
+ highlight: { type: "string", multiple: true },
17
+ badge: { type: "string", multiple: true },
18
+ logo: { type: "string" },
19
+ favicon: { type: "string" },
16
20
  anonymous: { type: "string" },
17
21
  domain: { type: "string" },
18
22
  module: { type: "string" },
@@ -24,7 +28,7 @@ try {
24
28
  });
25
29
  if (values.help || positionals.length !== 1) {
26
30
  console.log(
27
- "Usage: create-fidj <directory> --app-id <fidjId> [--api-endpoint <url>] [--title <text> --welcome <text> --description <text> --content <html> --domain <hostname>] [--module <built-directory> --module-entry <index.html#/route>] [--oidc-issuer https://api.example/oidc] [--anonymous true|false] [--local] [--replace]",
31
+ "Usage: create-fidj <directory> --app-id <fidjId> [--api-endpoint <url>] [--title <text> --welcome <text> --description <text> --content <html> --domain <hostname>] [--highlight '<heading>|<body>' ...] [--badge <text> ...] [--logo <image>] [--favicon <image>] [--module <built-directory> --module-entry <index.html#/route>] [--oidc-issuer https://api.example/oidc] [--anonymous true|false] [--local] [--replace]",
28
32
  );
29
33
  process.exitCode = values.help ? 0 : 1;
30
34
  } else {
@@ -37,6 +41,10 @@ try {
37
41
  welcome: values.welcome,
38
42
  description: values.description,
39
43
  content: values.content,
44
+ highlights: values.highlight,
45
+ badges: values.badge,
46
+ logo: values.logo,
47
+ favicon: values.favicon,
40
48
  anonymous: values.anonymous,
41
49
  domain: values.domain,
42
50
  module: values.module,
@@ -1,23 +1,55 @@
1
1
  const Generator = require("yeoman-generator");
2
2
  const { scaffold } = require("../../lib/scaffold.cjs");
3
3
 
4
+ // Yeoman and bin/create-fidj.cjs are two doors into the same scaffolder, and
5
+ // they must stay at parity: an input added to one and not the other silently
6
+ // disappears for half the users. test/parity.test.cjs fails when they drift.
7
+ const TEXT_OPTIONS = [
8
+ "app-id",
9
+ "api-endpoint",
10
+ "oidc-issuer",
11
+ "sdk-path",
12
+ "title",
13
+ "welcome",
14
+ "description",
15
+ "content",
16
+ "anonymous",
17
+ "domain",
18
+ "module",
19
+ "module-entry",
20
+ "logo",
21
+ "favicon",
22
+ ];
23
+ // Repeatable: --highlight twice yields two cells.
24
+ const LIST_OPTIONS = ["highlight", "badge"];
25
+ const FLAG_OPTIONS = ["local", "replace"];
26
+
27
+ // Yeoman joins a repeated flag into one comma-separated string before any type
28
+ // coercion, and splitting that back is wrong: a highlight body legitimately
29
+ // contains commas ("An install, a fidjId, a few lines"). Read the occurrences
30
+ // from argv, which is exactly what was typed, and fall back to Yeoman's value
31
+ // when the generator is composed programmatically rather than from a command
32
+ // line.
33
+ function repeated(name, fallback, argv = process.argv) {
34
+ const found = [];
35
+ for (let index = 0; index < argv.length; index += 1) {
36
+ if (argv[index] === "--" + name && index + 1 < argv.length)
37
+ found.push(argv[index + 1]);
38
+ else if (argv[index].startsWith("--" + name + "="))
39
+ found.push(argv[index].slice(name.length + 3));
40
+ }
41
+ if (found.length) return found;
42
+ if (fallback === undefined) return undefined;
43
+ return Array.isArray(fallback) ? fallback : [fallback];
44
+ }
45
+
4
46
  module.exports = class extends Generator {
5
47
  constructor(args, opts) {
6
48
  super(args, opts);
7
49
  this.argument("appname", { type: String, required: false });
8
- this.option("app-id", { type: String });
9
- for (const key of [
10
- "title",
11
- "welcome",
12
- "description",
13
- "content",
14
- "domain",
15
- "sdk-path",
16
- "anonymous",
17
- "module",
18
- "module-entry",
19
- ])
20
- this.option(key, { type: String });
50
+ for (const key of TEXT_OPTIONS) this.option(key, { type: String });
51
+ for (const key of LIST_OPTIONS) this.option(key, { type: String });
52
+ for (const key of FLAG_OPTIONS) this.option(key, { type: Boolean });
21
53
  this.option("api-endpoint", {
22
54
  type: String,
23
55
  default: "https://api.sandbox.fidj.ovh/v3",
@@ -55,11 +87,19 @@ module.exports = class extends Generator {
55
87
  description: this.options.description,
56
88
  content: this.options.content,
57
89
  anonymous: this.options.anonymous,
90
+ highlights: repeated("highlight", this.options.highlight),
91
+ badges: repeated("badge", this.options.badge),
92
+ logo: this.options.logo,
93
+ favicon: this.options.favicon,
58
94
  module: this.options.module,
59
95
  moduleEntry: this.options["module-entry"],
60
96
  domain: this.options.domain,
61
- sdkPath: this.options["sdk-path"],
62
- appId: this.options["app-id"] || this.answers.appId,
97
+ sdkPath: this.options["sdk-path"] || process.env.FIDJ_SDK_DIR,
98
+ oidcIssuer: this.options["oidc-issuer"],
99
+ local: this.options.local || process.env.FIDJ_LOCAL === "true",
100
+ replace: this.options.replace,
101
+ appId:
102
+ process.env.FIDJ_APP_ID || this.options["app-id"] || this.answers.appId,
63
103
  apiEndpoint: this.options["api-endpoint"],
64
104
  });
65
105
  this.log(
@@ -67,3 +107,5 @@ module.exports = class extends Generator {
67
107
  );
68
108
  }
69
109
  };
110
+
111
+ module.exports.repeatedOption = repeated;
@@ -29,7 +29,7 @@ The current unreleased generator requires the matching `@ofidj/node` 3.6.24 SDK.
29
29
  - `test/server.test.mjs`: fresh HTTP servers verify role changes, revoked/foreign sessions, outage failure, per-user notes and privacy scope.
30
30
  - `scripts/build.mjs`: browser and Node builds using [esbuild](https://esbuild.github.io/getting-started/), with a separate TypeScript typecheck.
31
31
 
32
- Run `npm test` before changing the template. Every fresh generated app includes the same tests.
32
+ Use TDD: add and run a failing behavior test before implementation (red), make it pass (green), then refactor and run `npm test`. Change maintained template source and regenerate to verify that every fresh app receives the fix.
33
33
 
34
34
  ## Privacy scope
35
35
 
@@ -41,7 +41,7 @@ The browser SDK stores the app’s session in this origin’s local storage. Use
41
41
 
42
42
  ## Local scenario
43
43
 
44
- Only when using the loopback API, set `LOCAL_DEMO=true` to show synthetic Alex/Maya/Sam sign-in shortcuts. These accounts must be seeded by your local Fidj API; the starter does not create accounts or seed data remotely. Alex is the owner, and the owner console can grant Maya Editor access.
44
+ For the Notes fixture only, `LOCAL_DEMO=true` with the loopback API enables synthetic Alex/Maya/Sam sign-in shortcuts. Content/module entries keep test accounts in developer documentation. These accounts must be seeded by your local Fidj API; the starter does not create accounts or seed data remotely. Alex is the owner, and the owner console can grant Maya Editor access.
45
45
 
46
46
  ## Entry flow
47
47
 
@@ -71,3 +71,13 @@ Keep `FIDJ_DATA_DIR` outside public assets and generated output. Use one writer
71
71
  After starting the Notes backend with its server-only adapter secret, run `npm run privacy:check`. The signed check confirms export/erase capabilities and writable storage without accessing user records. Override `FIDJ_APP_URL` for a deployed HTTPS backend. Keep this secret out of browser configuration.
72
72
 
73
73
  Run `npm run privacy:rehearse` for an isolated temporary-store HTTP scenario covering live roles, scoped export, persistence, failure and idempotent erasure. It never points at your running data directory. Groups are managed in the Fidj owner console; direct and group roles are checked live by the backend.
74
+
75
+ ## Agreement at sign-in
76
+
77
+ Sign-in and account creation require the unchecked service-agreement checkbox.
78
+ Read the app's current text/version from the dialog before accepting. If the
79
+ agreement is not accepted, submitting explains the requirement while preserving
80
+ the form; if the agreement cannot load, the form stays blocked. The API records the accepted
81
+ version per app; optional privacy choices are separate. The app owner must replace
82
+ the default demo agreement using the Fidj app's `configurationAsJSON.serviceAgreement`
83
+ (`version` and `text`) before release. No generated-source edit is needed.
@@ -6,5 +6,9 @@
6
6
  "localDemo": false,
7
7
  "allowAnonymous": true,
8
8
  "moduleEntry": "",
9
- "description": "A space to explore, with an account that puts you in control."
9
+ "description": "A space to explore, with an account that puts you in control.",
10
+ "highlights": [],
11
+ "badges": [],
12
+ "logo": "./fidj-logo.png",
13
+ "favicon": "./fidj-logo.png"
10
14
  }
@@ -15,7 +15,7 @@
15
15
  "privacy:rehearse": "npm run build && node --test test/server.test.mjs"
16
16
  },
17
17
  "dependencies": {
18
- "@ofidj/node": "^3.6.24"
18
+ "@ofidj/node": "^3.6.26"
19
19
  },
20
20
  "devDependencies": {
21
21
  "@types/node": "^22.0.0",
@@ -13,6 +13,9 @@ await Promise.all([
13
13
  target: "es2022",
14
14
  outfile: "dist/public/main.js",
15
15
  sourcemap: true,
16
+ // The design system's fonts are copied with public/; esbuild must leave
17
+ // their url()s alone rather than try to resolve them at build time.
18
+ external: ["*.woff2"],
16
19
  }),
17
20
  build({
18
21
  entryPoints: ["server/index.ts"],
@@ -8,7 +8,7 @@ const escape = (value) =>
8
8
  );
9
9
  export function renderContent(config) {
10
10
  // HTML is supplied by the developer at generation time, never by an app visitor.
11
- return `<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><meta name="referrer" content="no-referrer"><title>${escape(config.title)}</title><link rel="icon" href="./fidj-logo.png"><link rel="stylesheet" href="./main.css"></head><body>
12
- <header class="topbar"><a class="brand" href="#/content">${escape(config.title)}</a><a class="fidj-brand" href="${escape(config.moduleEntry || config.dashboardUrl + "/#/my")}"><img src="./fidj-logo.png" alt="Fidj">My privacy ↗</a></header>
11
+ return `<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><meta name="referrer" content="no-referrer"><title>${escape(config.title)}</title><link rel="icon" href="${escape(config.favicon)}"><link rel="stylesheet" href="./main.css"></head><body>
12
+ <header class="topbar"><a class="brand" href="#/content"><img src="${escape(config.logo)}" alt="">${escape(config.title)}</a></header>
13
13
  <main><template id="public-content"><section class="card public-content"><h1>${escape(config.welcome)}</h1><div>${config.content}</div></section></template><div id="app" aria-live="polite"></div><footer>Built with Fidj · Your choices belong to this app.</footer></main><script type="module" src="./main.js"></script></body></html>`;
14
14
  }
@@ -241,7 +241,16 @@ export function createApp(
241
241
  "/main.css": ["main.css", "text/css"],
242
242
  "/fidj-logo.png": ["fidj-logo.png", "image/png"],
243
243
  };
244
- if (!files[pathname] && pathname.startsWith("/module/")) {
244
+ // Beyond the named files above, three directories are served from the
245
+ // build manifest: the assembled application module, the design system's
246
+ // self-hosted fonts, and any logo or favicon the app supplied. The
247
+ // manifest only resolves paths the build actually produced.
248
+ if (
249
+ !files[pathname] &&
250
+ (pathname.startsWith("/module/") ||
251
+ pathname.startsWith("/fonts/") ||
252
+ pathname.startsWith("/brand/"))
253
+ ) {
245
254
  const manifest = JSON.parse(
246
255
  await readFile(join(__dirname, "public-files.json"), "utf8"),
247
256
  );