Skip to content
Merged
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
3 changes: 2 additions & 1 deletion .agents/skills/webjs/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ Rows point rather than explain. The reference is the authority on the rule, and
| add a URL, static or with a dynamic segment | a file at `app/<path>/page.ts`, `[id]` for a param | registering the route in a table or config | `references/routing-and-pages.md` | `app/features/routing` |
| abandon a render because something is missing or not allowed | throw `notFound()` / `forbidden()` / `unauthorized()` | returning an error object and branching in the template | `references/routing-and-pages.md` | `app/features/boundaries` |
| set a page's title, description, or social preview | `export const metadata` or `generateMetadata()` | writing `<head>` tags in the page | `references/routing-and-pages.md` | `app/features/metadata` |
| give the app its own favicon, home-screen icon and manifest | replace the placeholder `app/icon.svg` with a simple symbol for the app in its colours, add `app/apple-icon.png`, edit `app/manifest.webmanifest` | leaving the scaffold placeholder, or a hand-written `<link rel="icon">` | `references/routing-and-pages.md` (App icon and manifest) | `app/icon.ts` |
| make part of the page respond to a click or hold state | a `WebComponent` custom element | expecting the page's own markup to hydrate | `references/components.md` | `app/features/components` |
| render a keyed list, or swap one node when state changes | `repeat()` / `watch()` from `/directives` | re-rendering the component or diffing by hand | `references/components.md` | `app/features/directives` |
| get server data into a component's first paint | `async render()` awaiting an action | fetching in `connectedCallback`, which SSR never calls | `references/components.md` | `app/features/async-render` |
Expand Down Expand Up @@ -174,7 +175,7 @@ Find the right export fast. Load the linked reference for full examples.

### File conventions

`page.ts` (server-only fn), `layout.ts` (embeds `children`), `route.ts` (HTTP handler), `middleware.ts`, `*.server.ts` (server boundary), `error.ts` / `loading.ts` / `not-found.ts` / `forbidden.ts` / `unauthorized.ts` (boundaries), metadata routes (`sitemap.ts`, `robots.ts`, `manifest.ts`, `icon.ts`, `opengraph-image.ts`).
`page.ts` (server-only fn), `layout.ts` (embeds `children`), `route.ts` (HTTP handler), `middleware.ts`, `*.server.ts` (server boundary), `error.ts` / `loading.ts` / `not-found.ts` / `forbidden.ts` / `unauthorized.ts` (boundaries), metadata routes (`sitemap.ts`, `robots.ts`, `manifest.ts`, `icon.ts`, `opengraph-image.ts`), and the static app-root icon files (`icon.svg`, `apple-icon.png`, `manifest.webmanifest`, `favicon.ico`), auto-linked into `<head>`. The scaffold's `app/icon.svg` is a placeholder: replace it with the app's own icon.

## Canonical Patterns

Expand Down
2 changes: 2 additions & 0 deletions .agents/skills/webjs/references/built-ins.md
Original file line number Diff line number Diff line change
Expand Up @@ -296,6 +296,8 @@ Three levels, the same scale ESLint uses: `error` fails the exit, `warn` reports

Two guarantees worth knowing. A result that could not check (a network or toolchain outage) is capped at `warn` and can never be escalated, so a jspm or npm outage cannot red your CI. And a malformed gate exits 1 naming the offender rather than being ignored, so a typo cannot silently un-gate the build. That covers an unknown code, a bad severity, a wrong shape (a non-object `doctor` or `gate`), and a misspelled sibling of `gate` such as `gates`, since every one of those would otherwise leave the build un-gated while the `package.json` looks gated. Under `--json` the offenders come back as a `configErrors` array alongside an empty `results`, each entry a `{ kind }` of `malformed` / `unknown-key` / `unknown-code` / `bad-severity`. Wire it up with one workflow step, `npm run doctor`, and change what is fatal in `package.json` rather than in the workflow.

`APP_ICON` warns while the favicon is still the scaffold's: the placeholder `app/icon.svg` (marked `data-webjs-placeholder`) or the WebJs mark older scaffolds shipped at `public/favicon.svg`. Replace it with the app's own icon (`references/routing-and-pages.md`, "App icon and manifest").

### Dependency audit allowlist

`webjs audit` runs `npm audit` or `bun audit` (by the nearest lockfile, so a workspace member uses the root's) and fails on any advisory at or above `webjs.audit.level` (default `high`) that `webjs.audit.ignore` does not list. The scaffold's `Security: dependency audit` CI step runs it.
Expand Down
22 changes: 18 additions & 4 deletions .agents/skills/webjs/references/routing-and-pages.md
Original file line number Diff line number Diff line change
Expand Up @@ -278,12 +278,26 @@ export default function robots({ siteUrl }: MetadataRouteContext) {

The IMAGE metadata routes (`icon`, `apple-icon`, `opengraph-image`, `twitter-image`) default-export a function returning a `Response` with an explicit `content-type`, so an inline SVG needs no asset file (buildless).

**`icon` and `apple-icon` are LINKED for you.** An app that declares no `metadata.icons` gets `<link rel="icon" href="/icon">` and `<link rel="apple-touch-icon" href="/apple-icon">` in the head automatically, for whichever of the two routes it defines (base-path prefixed, since that is where the route answers). No `type` or `sizes` is asserted, because the route picks its content type at request time and the browser sniffs the served one.
### App icon and manifest (replace the placeholder)

Declaring `metadata.icons` **suppresses** the routes rather than merging with them, which is what Next does with its static icon files. So an app that outgrows a placeholder `app/icon.ts` names its real icons and the route stops being linked without having to be deleted:
**Every app needs its OWN icon.** `webjs create` ships `app/icon.svg` as a neutral PLACEHOLDER (a grey tile with a dashed frame, marked `data-webjs-placeholder="icon"`) and `app/manifest.webmanifest` with the app's name. Replacing the placeholder is part of building the app, not polish: until you do, the tab, the bookmark and the home-screen icon look like every other unfinished app, and `webjs doctor` warns (`APP_ICON`). Draw a simple symbol for what the app IS (a grid for a tic-tac-toe game, a cup for a cafe, a check for a task list), in the app's own colours, on a 32x32 or 24x24 `viewBox`: a filled rounded tile in the primary colour with one bold shape in its foreground colour reads at 16px. Avoid thin strokes (under 2px at 32px), text longer than one letter, and detail that blurs at tab size. Then set `name`, `short_name`, `theme_color` and `background_color` in `app/manifest.webmanifest` to match.

The icon conventions, all at the app ROOT and all auto-linked into `<head>` when the app declares no `metadata.icons`:

| File | Served at | Linked as |
|---|---|---|
| `app/icon.svg` / `icon.png` / `icon.ico` | `/icon.svg` ... | `<link rel="icon">` with `type`, `sizes="any"` (SVG) or the PNG's real pixel size |
| `app/apple-icon.png` (180x180) | `/apple-icon.png` | `<link rel="apple-touch-icon" sizes="180x180">` (iOS needs PNG, not SVG) |
| `app/icon.ts` / `apple-icon.ts` | `/icon`, `/apple-icon` | bare link (the route picks its content type per request) |
| `app/manifest.webmanifest` / `manifest.json` / `manifest.ts` | `/manifest.webmanifest`, `/manifest.json` | `<link rel="manifest">` (`metadata.manifest` wins; `manifest: null` opts out) |
| `app/favicon.ico` | `/favicon.ico` | not linked; browsers request it themselves |

`/favicon.ico` always answers: `public/favicon.ico`, else `app/favicon.ico`, else the app's icon (raster preferred over SVG, then the `icon.ts` route), so a crawler or feed reader that reads no markup gets the same icon. A static icon file wins the link over an icon route when both exist (the route still serves at its URL). Raster icons are linked before SVG, because Google's favicon crawler takes the first usable icon and wants a square raster. Use `icon.ts` only when the mark must be computed per request (per theme, per tenant); a route can render a PNG for `apple-icon.ts` the same way.

Declaring `metadata.icons` **suppresses** all of the above rather than merging with them, which is what Next does with its static icon files. So name icons explicitly only when they live elsewhere (a CDN, `public/`):

```ts
// app/layout.ts -> these win; /icon and /apple-icon are no longer linked
// app/layout.ts -> these win; app/icon.* and app/apple-icon.* are no longer linked
export const metadata = {
icons: {
icon: [
Expand All @@ -295,7 +309,7 @@ export const metadata = {
};
```

Declare a favicon through `metadata.icons` (or a metadata route), never as a hand-written `<link rel="icon">`: only the root layout may write a shell at all (invariant 8), so a hand-written tag is unavailable to every other layout. A `public/favicon.ico` needs no declaration either way, since the framework serves it at the origin root for crawlers that read no markup.
Never write a favicon as a hand-written `<link rel="icon">`: only the root layout may write a shell at all (invariant 8), so a hand-written tag is unavailable to every other layout.

`opengraph-image` and `twitter-image` are LINKED too, Next's behaviour: a page that declares no `openGraph.images` gets `og:image` pointing at the nearest `opengraph-image` route above it (absolute against the site URL), and likewise `twitter:image`. A page that declares its own image keeps it.

Expand Down
1 change: 1 addition & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,7 @@ env.js optional boot-time env validation (schema or validat
instrumentation.js optional boot-time hook (register(); wire APM via setOnError, #848)
instrumentation-client.js optional client boot hook (runs first, before app modules, #848)
sitemap.js robots.js manifest.js icon.js opengraph-image.js twitter-image.js apple-icon.js metadata routes
icon.svg icon.png apple-icon.png favicon.ico manifest.webmanifest static app-root metadata files, auto-linked (replace the placeholder icon.svg)
lib/ app-wide code (lib/*.server.js infra, lib/utils/ browser-safe helpers)
modules/<feature>/ feature-scoped: actions/ (mutations), queries/ (reads), components/, utils/, types.js
components/*.js SHARED presentational primitives
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ older, unrelated Java framework also used the name WebJS.
- **The essentials ship in the box.** Auth, sessions, caching, WebSocket broadcast, and rate limiting are all built in, sharing one pluggable cache store that is in-memory by default. Call `setStore(redisStore({ url: process.env.REDIS_URL }))` once at startup to move all four onto Redis for horizontal scaling.
- **Components can lazy-load themselves.** Setting `static lazy = true` defers the module download until the component is first visible (scrolled into view, or its tab or dialog opened), even when another component imports it. The SSR content stays visible throughout, so only the JavaScript is lazy.
- **Error boundaries and loading states are file conventions.** `error.ts` catches render failures at any route level, and `loading.ts` automatically wraps pages in Suspense boundaries.
- **Metadata routes are functions rather than static files.** `sitemap.ts`, `robots.ts`, `manifest.ts`, `icon.ts`, and `opengraph-image.ts` generate SEO and PWA metadata dynamically.
- **Metadata routes are functions, or plain files.** `sitemap.ts`, `robots.ts`, `manifest.ts`, `icon.ts`, and `opengraph-image.ts` generate SEO and PWA metadata dynamically; a static `app/icon.svg`, `app/apple-icon.png` or `app/manifest.webmanifest` is served and linked into `<head>` with no declaration.
- **REST endpoints come from `route.ts`.** Expose a server action over HTTP by importing it into a `route.ts` handler, or reach for the one-line `route(action)` adapter from `@webjsdev/server`. Input validation is optional.
- **WebJs is production ready.** CSRF protection, gzip and brotli, HTTP/2, 103 Early Hints, CSP nonces, modulepreload, rate limiting, health probes, graceful shutdown, and streaming Suspense all ship with the framework.
- **WebJs UI is the matching AI-first component library.** Its 35 primitives at [webjs.dev/ui](https://webjs.dev/ui) are written for AI agents, in two tiers: pure class-helper functions (`buttonClass`, `cardClass`, `inputClass`) for visual primitives, plus a small set of stateful custom elements (`<ui-dialog>`, `<ui-tabs>`, `<ui-popover>`) for the cases where state matters. Running `webjs ui add button card dialog` copies the source into your project, so you own it and can edit it. It ships as a hard dependency of `@webjsdev/cli`, so a WebJs app that installed the CLI needs no separate install, and a WebJs app that skipped the global install runs `npm install -D @webjsdev/ui` and `npm install @webjsdev/core` first, then `npx webjsui init` and `npx webjsui add button card dialog`.
Expand Down
14 changes: 8 additions & 6 deletions gallery/app/icon.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,14 @@
// content type, so an inline SVG needs no asset file. Generate it dynamically
// (per-theme, per-tenant) when the mark must be computed at request time.
//
// This is the DEMO of that surface, not the gallery's own favicon. A metadata
// route is not auto-linked: the framework emits `<link rel="icon">` only from
// metadata.icons, so the gallery declares the static WebJs brand mark from
// public/ there (see app/layout.ts) and this route stays browsable at /icon.
// For a favicon that never changes, that static path is the one to copy; drop
// this route when your app has no request-time mark to compute.
// This is the DEMO of that surface, not the gallery's own favicon. With no
// metadata.icons declared, the framework auto-links an app-root icon: a STATIC
// file (app/icon.svg, app/icon.png) wins that link over this route, and a
// declared metadata.icons wins over both. The gallery declares its WebJs brand
// mark in app/layout.ts, so this route stays browsable at /icon without being
// the tab icon. For an icon that never changes, write app/icon.svg instead
// (what `webjs create` ships); keep a route like this only when the mark must
// be computed at request time (per theme, per tenant).
export default function Icon() {
const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
<rect width="32" height="32" rx="7" fill="#1e2226"/>
Expand Down
5 changes: 4 additions & 1 deletion gallery/app/manifest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@
// icons to your app; pair it with the opt-in service worker for an installable
// PWA. See agent-docs/service-worker.md. (Gallery files are copied verbatim, so
// set the real app name here by hand rather than expecting substitution.)
// The framework links an app-root manifest into <head> by itself. A static
// app/manifest.webmanifest (what `webjs create` ships) wins that link over this
// route; write a route like this only when a value must be computed.
export default function Manifest() {
return {
name: 'webjs app',
Expand All @@ -12,7 +15,7 @@ export default function Manifest() {
background_color: '#ffffff',
theme_color: '#1e2226',
icons: [
{ src: '/favicon.svg', sizes: 'any', type: 'image/svg+xml' },
{ src: '/icon', sizes: 'any', type: 'image/svg+xml' },
],
};
}
55 changes: 55 additions & 0 deletions packages/cli/lib/app-icon.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
/**
* The app icon a new scaffold starts with, and how `webjs doctor` recognises
* an icon that was never replaced.
*
* A scaffold ships `app/icon.svg` (auto-linked as the favicon, served at
* /icon.svg and as the /favicon.ico fallback) and `app/manifest.webmanifest`
* (auto-linked as the web app manifest). The icon is a deliberately NEUTRAL
* placeholder: a grey tile with a dashed frame, so a tab strip shows at a
* glance that the app has no icon of its own yet, and no app ever ships
* looking like a WebJs demo. It carries `data-webjs-placeholder` so a check
* (webjs doctor, or any agent's own tester) can tell it apart from a real
* icon without comparing bytes.
*/

/** The attribute that marks the scaffold's placeholder icon. */
export const PLACEHOLDER_MARKER = 'data-webjs-placeholder';

/** The placeholder `app/icon.svg`. Replace it with the app's own mark. */
export const PLACEHOLDER_ICON_SVG = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" width="32" height="32" ${PLACEHOLDER_MARKER}="icon">
<!-- PLACEHOLDER app icon from \`webjs create\`. Replace this file with the
app's own icon: a simple symbol for what the app is, in its colours,
legible at 16px. Keep the file name (app/icon.svg) and the framework
links it, serves it and answers /favicon.ico with it. -->
<rect width="32" height="32" rx="8" fill="#d4d4d8"/>
<rect x="7" y="7" width="18" height="18" rx="3" fill="none" stroke="#71717a" stroke-width="2" stroke-dasharray="3 2.4"/>
</svg>
`;

/**
* The `app/manifest.webmanifest` a scaffold starts with: the app's name, the
* neutral colours of the scaffold palette, and the icon. Grow it with the app
* (its real theme colour, a 192 and 512 PNG for installability).
* @param {string} name the app's display name
*/
export function appManifest(name) {
return JSON.stringify({
name,
short_name: name,
start_url: '/',
display: 'standalone',
background_color: '#ffffff',
theme_color: '#ffffff',
icons: [{ src: '/icon.svg', sizes: 'any', type: 'image/svg+xml' }],
}, null, 2) + '\n';
}

/**
* Whether an SVG is the WebJs brand mark earlier scaffolds shipped as
* `public/favicon.svg` (a rounded square with the gallery's grey gradient).
* Apps made before the placeholder still serve it as their favicon.
* @param {string} svg
*/
export function isLegacyBrandFavicon(svg) {
return /aria-label="WebJs"/.test(svg) && /<linearGradient id="wj"/.test(svg);
}
23 changes: 14 additions & 9 deletions packages/cli/lib/create.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import { bunifyProse, bunifyDockerfile, bunifyCompose, bunifyCi, bunifyEnvExampl
import { postgresCompose, postgresCi } from './db-rewrite.js';
import { assertValidAppName, toDatabaseName } from './app-name.js';
import { isGalleryAppShellFile } from './gallery-shell-files.js';
import { PLACEHOLDER_ICON_SVG, appManifest } from './app-icon.js';
import { detectPackageManager } from './package-manager.js';

/**
Expand Down Expand Up @@ -1246,10 +1247,15 @@ export type ActionResult<T> =
const swSrc = join(TEMPLATES, 'public', swFile);
if (existsSync(swSrc)) await cp(swSrc, join(publicDir, swFile));
}
// A base SVG favicon (the root layout links it). It ships with the app, not
// the gallery, so it survives `npm run gallery:clear`.
const faviconSrc = join(TEMPLATES, 'public', 'favicon.svg');
if (existsSync(faviconSrc)) await cp(faviconSrc, join(publicDir, 'favicon.svg'));
// The app icon and web app manifest: `app/icon.svg` (a neutral PLACEHOLDER
// the agent replaces with the app's own icon; `webjs doctor` warns while it
// is still there) and `app/manifest.webmanifest` (the app's name). The
// framework links both into <head> and answers /favicon.ico with the icon,
// so the layout declares nothing. They ship with the app, not the gallery,
// so they survive `gallery:clear`.
await mkdir(join(appDir, 'app'), { recursive: true });
await writeFile(join(appDir, 'app', 'icon.svg'), PLACEHOLDER_ICON_SVG);
await writeFile(join(appDir, 'app', 'manifest.webmanifest'), appManifest(displayName));

// The gallery-reset script (wired as `gallery:clear`). Only UI templates have
// a gallery, so it ships here (NOT in the flat templateFiles list, which would
Expand Down Expand Up @@ -1355,11 +1361,10 @@ import '#components/theme-toggle.ts';
* text-foreground, bg-card, bg-primary, and border-border all work.
*/

// Declare the favicon via metadata.icons (NOT a hand-written <link> in the
// template): the framework emits metadata links into <head>, whereas a <link>
// written in the layout body stays in <body>, where browsers ignore it. The SVG
// lives at public/favicon.svg and serves at /public/favicon.svg.
export const metadata = { icons: '/public/favicon.svg' };
// The favicon and manifest are app/icon.svg and app/manifest.webmanifest: the
// framework links them into <head> itself, so nothing is declared here. Replace
// the placeholder icon with this app's own (see the skill's routing-and-pages
// reference, "App icon").

// LayoutProps types every layout argument (children, params, searchParams,
// url) from the framework, so children is a TemplateResult rather than an
Expand Down
1 change: 1 addition & 0 deletions packages/cli/lib/doctor/codes.js
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ export const DOCTOR_CODES = {
'Static build outputs (dev.regenerate freshness)': 'STATIC_ASSET_FRESHNESS',
'Asset urls (unmarked stylesheet links)': 'UNMARKED_ASSET_LINKS',
'workspace-overrides': 'WORKSPACE_OVERRIDES',
'app-icon': 'APP_ICON',
};

/**
Expand Down
Loading
Loading