@webjsdev/cli 0.10.10 → 0.10.12
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.
- package/bin/webjs.js +6 -4
- package/lib/create.js +16 -2
- package/lib/mcp-docs.js +400 -0
- package/lib/mcp-source.js +244 -0
- package/lib/mcp.js +167 -18
- package/package.json +7 -2
- package/resources/AGENTS.md +404 -0
- package/resources/agent-docs/advanced.md +1090 -0
- package/resources/agent-docs/built-ins.md +367 -0
- package/resources/agent-docs/components.md +486 -0
- package/resources/agent-docs/configuration.md +207 -0
- package/resources/agent-docs/framework-dev.md +65 -0
- package/resources/agent-docs/lit-muscle-memory-gotchas.md +456 -0
- package/resources/agent-docs/metadata.md +334 -0
- package/resources/agent-docs/recipes.md +440 -0
- package/resources/agent-docs/service-worker.md +100 -0
- package/resources/agent-docs/ssr-partial-nav-design.md +214 -0
- package/resources/agent-docs/styling.md +235 -0
- package/resources/agent-docs/testing.md +372 -0
- package/resources/agent-docs/typescript.md +334 -0
- package/templates/.dockerignore +6 -4
- package/templates/AGENTS.md +25 -10
- package/templates/CONVENTIONS.md +18 -1
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
# Metadata API: full field reference
|
|
2
|
+
|
|
3
|
+
Page modules export `metadata` (static) or `generateMetadata(ctx)`
|
|
4
|
+
(request-scoped). Values flow into `<head>` at SSR time and merge across
|
|
5
|
+
nested layouts (deeper wins). Surface is Next.js-compatible.
|
|
6
|
+
|
|
7
|
+
## Type it with `Metadata`
|
|
8
|
+
|
|
9
|
+
webjs exports a `Metadata` type covering every field below, so a typo
|
|
10
|
+
(`titel`, `descripton`), wrong nesting, or a wrong-typed value
|
|
11
|
+
(`themeColor: 123`) is a tsserver / checkJs error instead of a silently
|
|
12
|
+
dropped tag. Import it from `@webjsdev/core` (the same isomorphic surface
|
|
13
|
+
a page already imports `html` from). `MetadataContext` types the
|
|
14
|
+
`generateMetadata(ctx)` argument.
|
|
15
|
+
|
|
16
|
+
```ts
|
|
17
|
+
import type { Metadata, MetadataContext } from '@webjsdev/core';
|
|
18
|
+
|
|
19
|
+
// static
|
|
20
|
+
export const metadata: Metadata = { title: 'Home', description: 'Welcome' };
|
|
21
|
+
|
|
22
|
+
// request-scoped
|
|
23
|
+
export async function generateMetadata(ctx: MetadataContext): Promise<Metadata> {
|
|
24
|
+
return { title: `Post: ${ctx.params.slug}` };
|
|
25
|
+
}
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Every field is optional. Where the framework accepts a string OR an
|
|
29
|
+
object (`title`, `viewport`, `robots`, `appleWebApp`, `icons`), the type
|
|
30
|
+
is a union, so both forms type-check. The type lives in
|
|
31
|
+
`packages/core/src/metadata.d.ts` (types-only, zero runtime, no build);
|
|
32
|
+
it mirrors exactly what `packages/server/src/ssr.js` consumes.
|
|
33
|
+
|
|
34
|
+
```ts
|
|
35
|
+
export const metadata = {
|
|
36
|
+
// ----- Identity -----
|
|
37
|
+
title: 'Blog post title', // → <title>
|
|
38
|
+
// OR { template, default, absolute }. The template propagates from
|
|
39
|
+
// outer layouts, and deeper plain-string titles get wrapped via "%s".
|
|
40
|
+
// title: { template: '%s | webjs', default: 'webjs', absolute: 'standalone title' },
|
|
41
|
+
description: 'Short summary', // → <meta name="description">
|
|
42
|
+
keywords: ['ai', 'web components'], // → <meta name="keywords"> (or single string)
|
|
43
|
+
authors: [{ name: 'Vivek', url: 'https://...' }], // → <meta name="author"> (+ optional <link rel="author">)
|
|
44
|
+
creator: 'Vivek', // → <meta name="creator">
|
|
45
|
+
publisher: 'My Co', // → <meta name="publisher">
|
|
46
|
+
applicationName: 'webjs', // → <meta name="application-name">
|
|
47
|
+
generator: 'webjs 0.5', // → <meta name="generator">
|
|
48
|
+
referrer: 'origin-when-cross-origin', // → <meta name="referrer">
|
|
49
|
+
|
|
50
|
+
// ----- Base URL for relative metadata URLs -----
|
|
51
|
+
metadataBase: 'https://example.com',
|
|
52
|
+
// Any relative URL in openGraph.image, openGraph.url, twitter.image,
|
|
53
|
+
// alternates.canonical / languages / media / types, icons, authors[].url,
|
|
54
|
+
// archives / assets / bookmarks gets resolved against this base.
|
|
55
|
+
|
|
56
|
+
// ----- Viewport / theme -----
|
|
57
|
+
viewport: 'width=device-width,initial-scale=1', // string OR object
|
|
58
|
+
// viewport: { width: 'device-width', initialScale: 1, maximumScale: 5, userScalable: true },
|
|
59
|
+
// (or split-export: `export const viewport = { … }`, the Next.js 14+ style)
|
|
60
|
+
themeColor: '#1c1613', // → <meta name="theme-color">
|
|
61
|
+
colorScheme: 'light dark', // → <meta name="color-scheme">
|
|
62
|
+
|
|
63
|
+
// ----- Crawler / SEO -----
|
|
64
|
+
robots: { index: true, follow: true, googleBot: 'index, max-snippet:-1' },
|
|
65
|
+
// OR robots: 'noindex, nofollow'
|
|
66
|
+
alternates: {
|
|
67
|
+
canonical: '/post', // → <link rel="canonical">
|
|
68
|
+
languages: { 'es-ES': '/es', 'fr-FR': '/fr' }, // → hreflang <link>s
|
|
69
|
+
media: { '(max-width: 600px)': '/mobile' }, // → media alternates
|
|
70
|
+
types: { 'application/rss+xml': '/rss.xml' }, // → RSS / Atom alternates
|
|
71
|
+
},
|
|
72
|
+
verification: {
|
|
73
|
+
google: 'token', // → <meta name="google-site-verification">
|
|
74
|
+
yandex: 'token', // → <meta name="yandex-verification">
|
|
75
|
+
yahoo: 'token', // → <meta name="y_key">
|
|
76
|
+
me: 'https://me.example', // → <meta name="me"> (IndieAuth)
|
|
77
|
+
other: { 'facebook-domain-verification': 'fb-token' },
|
|
78
|
+
},
|
|
79
|
+
|
|
80
|
+
// ----- Icons + manifest -----
|
|
81
|
+
icons: {
|
|
82
|
+
icon: [{ url: '/icon.svg', type: 'image/svg+xml' }, { url: '/icon-32.png', sizes: '32x32' }],
|
|
83
|
+
apple: '/apple-touch-icon.png',
|
|
84
|
+
shortcut: '/favicon.ico',
|
|
85
|
+
other: [{ rel: 'mask-icon', url: '/mask.svg' }],
|
|
86
|
+
},
|
|
87
|
+
manifest: '/manifest.webmanifest', // → <link rel="manifest">
|
|
88
|
+
|
|
89
|
+
// ----- Open Graph -----
|
|
90
|
+
openGraph: {
|
|
91
|
+
type: 'website',
|
|
92
|
+
title: 'OG title',
|
|
93
|
+
description: 'OG description',
|
|
94
|
+
url: '/post', // relative resolves via metadataBase
|
|
95
|
+
image: '/og.png', // ditto
|
|
96
|
+
'image:width': '1200',
|
|
97
|
+
'image:height': '630',
|
|
98
|
+
'image:alt': 'Post cover',
|
|
99
|
+
'site_name': 'My Site',
|
|
100
|
+
},
|
|
101
|
+
|
|
102
|
+
// ----- Twitter -----
|
|
103
|
+
twitter: {
|
|
104
|
+
card: 'summary_large_image', // required for big-image preview
|
|
105
|
+
title: 'Twitter title',
|
|
106
|
+
description: 'Twitter description',
|
|
107
|
+
image: '/og.png',
|
|
108
|
+
},
|
|
109
|
+
|
|
110
|
+
// ----- iOS / mobile -----
|
|
111
|
+
appleWebApp: {
|
|
112
|
+
capable: true, // → apple-mobile-web-app-capable
|
|
113
|
+
title: 'My App', // → apple-mobile-web-app-title
|
|
114
|
+
statusBarStyle: 'black-translucent',
|
|
115
|
+
startupImage: [{ url: '/splash.png', media: '(device-width: 320px)' }],
|
|
116
|
+
},
|
|
117
|
+
formatDetection: { telephone: false, email: false },
|
|
118
|
+
itunes: { appId: '12345', appArgument: 'myapp://open' },
|
|
119
|
+
|
|
120
|
+
// ----- Long-tail descriptive -----
|
|
121
|
+
category: 'tech',
|
|
122
|
+
classification: 'documentation',
|
|
123
|
+
abstract: 'A short summary',
|
|
124
|
+
archives: ['/archive-2024', '/archive-2023'], // → <link rel="archives">
|
|
125
|
+
assets: '/assets-cdn', // → <link rel="assets">
|
|
126
|
+
bookmarks: ['/bm-1'], // → <link rel="bookmark">
|
|
127
|
+
|
|
128
|
+
// ----- Cache-control (response header, not <meta>) -----
|
|
129
|
+
cacheControl: 'public, max-age=60', // pages default to no-store
|
|
130
|
+
preload: [
|
|
131
|
+
{ href: '/public/fonts/Inter.woff2', as: 'font', type: 'font/woff2', crossorigin: 'anonymous' },
|
|
132
|
+
],
|
|
133
|
+
|
|
134
|
+
// ----- Connection-warming hints (#243) -----
|
|
135
|
+
preconnect: [ // → <link rel="preconnect">
|
|
136
|
+
'https://api.example.com', // warms DNS + TLS + TCP
|
|
137
|
+
{ url: 'https://fonts.gstatic.com', crossorigin: true },
|
|
138
|
+
],
|
|
139
|
+
dnsPrefetch: 'https://analytics.example.com', // → <link rel="dns-prefetch"> (DNS only)
|
|
140
|
+
|
|
141
|
+
// ----- Catch-all -----
|
|
142
|
+
other: {
|
|
143
|
+
'msvalidate.01': 'bing-token',
|
|
144
|
+
'mobile-web-app-capable': 'yes',
|
|
145
|
+
},
|
|
146
|
+
|
|
147
|
+
// ----- JSON-LD structured data (schema.org) -----
|
|
148
|
+
jsonLd: {
|
|
149
|
+
'@context': 'https://schema.org',
|
|
150
|
+
'@type': 'Article',
|
|
151
|
+
headline: 'How webjs ships zero dead JS',
|
|
152
|
+
author: { '@type': 'Person', name: 'Vivek' },
|
|
153
|
+
datePublished: '2026-06-01',
|
|
154
|
+
},
|
|
155
|
+
};
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
## Request-scoped via `generateMetadata`
|
|
159
|
+
|
|
160
|
+
```ts
|
|
161
|
+
import type { Metadata, MetadataContext } from '@webjsdev/core';
|
|
162
|
+
|
|
163
|
+
export function generateMetadata(ctx: MetadataContext): Metadata {
|
|
164
|
+
return {
|
|
165
|
+
title: `Post: ${ctx.params.slug}`,
|
|
166
|
+
metadataBase: new URL(ctx.url).origin,
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
## Split viewport export (Next.js 14+ pattern)
|
|
172
|
+
|
|
173
|
+
All fields above under `metadata.viewport` are equally valid here, plus
|
|
174
|
+
`themeColor` and `colorScheme` bubble up to their own meta tags.
|
|
175
|
+
|
|
176
|
+
```ts
|
|
177
|
+
export const viewport = {
|
|
178
|
+
width: 'device-width',
|
|
179
|
+
initialScale: 1,
|
|
180
|
+
themeColor: '#1c1613',
|
|
181
|
+
colorScheme: 'light dark',
|
|
182
|
+
};
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
## Special: `cacheControl`
|
|
186
|
+
|
|
187
|
+
Emitted as a **response header**, not a `<meta>` tag. Pages default to
|
|
188
|
+
`no-store` for safety. Opt into caching by setting this explicitly.
|
|
189
|
+
|
|
190
|
+
A **public** value (e.g. `public, max-age=60`) also enables conditional
|
|
191
|
+
GET on the page (#240): the buffered HTML response gets a weak content-hash
|
|
192
|
+
`ETag` (`W/"..."`) and a repeat request whose `If-None-Match` matches it
|
|
193
|
+
returns a `304 Not Modified` with no body. A `no-store` or `private` page
|
|
194
|
+
gets NO ETag and never 304s, so private / per-user content is never
|
|
195
|
+
revalidated across sessions. A streamed Suspense response is not ETagged.
|
|
196
|
+
See the conditional-GET section in the framework root `AGENTS.md`.
|
|
197
|
+
|
|
198
|
+
## Connection-warming: `preconnect` / `dnsPrefetch` (#243)
|
|
199
|
+
|
|
200
|
+
Warm a cross-origin connection the page is about to use (an API host, a
|
|
201
|
+
font / image CDN) so the browser pays the DNS + TLS + TCP cost ahead of the
|
|
202
|
+
first real request:
|
|
203
|
+
|
|
204
|
+
```ts
|
|
205
|
+
export const metadata = {
|
|
206
|
+
preconnect: [
|
|
207
|
+
'https://api.example.com', // bare URL
|
|
208
|
+
{ url: 'https://fonts.gstatic.com', crossorigin: true },// crossorigin set
|
|
209
|
+
],
|
|
210
|
+
dnsPrefetch: 'https://analytics.example.com', // a single URL
|
|
211
|
+
};
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
- **`preconnect`** emits `<link rel="preconnect" href="..." [crossorigin]>`,
|
|
215
|
+
warming DNS + TLS + TCP. Each entry is a URL string or
|
|
216
|
+
`{ url, crossorigin? }` (`crossorigin: true` / `''` emits a bare
|
|
217
|
+
`crossorigin`; a string like `'anonymous'` emits its value). A font CDN
|
|
218
|
+
needs `crossorigin`.
|
|
219
|
+
- **`dnsPrefetch`** emits `<link rel="dns-prefetch" href="...">`, which
|
|
220
|
+
resolves DNS only (a lighter-weight precursor; it never carries
|
|
221
|
+
`crossorigin`).
|
|
222
|
+
- Each field takes a URL string, the object form, or an array of either.
|
|
223
|
+
Every href is HTML-escaped.
|
|
224
|
+
|
|
225
|
+
**Auto vendor preconnect.** For an UNPINNED app resolving vendors live from
|
|
226
|
+
a cross-origin CDN, the framework auto-emits ONE
|
|
227
|
+
`<link rel="preconnect" href="<cdn-origin>" crossorigin>` (the resolved
|
|
228
|
+
vendor CDN origin, e.g. `https://ga.jspm.io`, derived from the importmap so
|
|
229
|
+
a `--from jsdelivr` app preconnects to jsdelivr), so the browser warms that
|
|
230
|
+
connection before the importmap resolves. It is DEDUPED against an
|
|
231
|
+
author-declared `preconnect` to the same origin, and NONE is emitted for a
|
|
232
|
+
same-origin pinned app (vendors served from the app's own origin) or an app
|
|
233
|
+
with no cross-origin vendors.
|
|
234
|
+
|
|
235
|
+
## JSON-LD structured data (`jsonLd`)
|
|
236
|
+
|
|
237
|
+
`metadata.jsonLd` emits schema.org structured data as one or more
|
|
238
|
+
`<script type="application/ld+json">` blocks in `<head>`. This is the
|
|
239
|
+
highest-leverage modern SEO surface (Google's Article, Product,
|
|
240
|
+
BreadcrumbList, Organization, and FAQ rich results all read it). webjs
|
|
241
|
+
stays true to its no-build identity here. JSON-LD is a web standard
|
|
242
|
+
rendered as a plain script tag, so the framework ONLY serializes and
|
|
243
|
+
escapes. There is no schema library and no validation. **You own the
|
|
244
|
+
schema.org object.**
|
|
245
|
+
|
|
246
|
+
**Single object** emits one script:
|
|
247
|
+
|
|
248
|
+
```ts
|
|
249
|
+
import type { Metadata } from '@webjsdev/core';
|
|
250
|
+
|
|
251
|
+
export const metadata: Metadata = {
|
|
252
|
+
jsonLd: {
|
|
253
|
+
'@context': 'https://schema.org',
|
|
254
|
+
'@type': 'Article',
|
|
255
|
+
headline: 'How webjs ships zero dead JS',
|
|
256
|
+
author: { '@type': 'Person', name: 'Vivek' },
|
|
257
|
+
datePublished: '2026-06-01',
|
|
258
|
+
image: 'https://example.com/og.png',
|
|
259
|
+
},
|
|
260
|
+
};
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
renders:
|
|
264
|
+
|
|
265
|
+
```html
|
|
266
|
+
<script type="application/ld+json">{"@context":"https://schema.org","@type":"Article",...}</script>
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
**An array** emits one script PER element. Use it to ship several graphs
|
|
270
|
+
for one page (a Product alongside its BreadcrumbList, say):
|
|
271
|
+
|
|
272
|
+
```ts
|
|
273
|
+
export const metadata: Metadata = {
|
|
274
|
+
jsonLd: [
|
|
275
|
+
{
|
|
276
|
+
'@context': 'https://schema.org',
|
|
277
|
+
'@type': 'Product',
|
|
278
|
+
name: 'Acme Widget',
|
|
279
|
+
offers: { '@type': 'Offer', price: '19.99', priceCurrency: 'USD' },
|
|
280
|
+
},
|
|
281
|
+
{
|
|
282
|
+
'@context': 'https://schema.org',
|
|
283
|
+
'@type': 'BreadcrumbList',
|
|
284
|
+
itemListElement: [
|
|
285
|
+
{ '@type': 'ListItem', position: 1, name: 'Shop', item: 'https://example.com/shop' },
|
|
286
|
+
{ '@type': 'ListItem', position: 2, name: 'Widget', item: 'https://example.com/shop/widget' },
|
|
287
|
+
],
|
|
288
|
+
},
|
|
289
|
+
],
|
|
290
|
+
};
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
**Per-request data** works the same way through `generateMetadata`, so a
|
|
294
|
+
dynamic route can build the Article from the loaded record:
|
|
295
|
+
|
|
296
|
+
```ts
|
|
297
|
+
import type { Metadata, MetadataContext } from '@webjsdev/core';
|
|
298
|
+
|
|
299
|
+
export async function generateMetadata(ctx: MetadataContext): Promise<Metadata> {
|
|
300
|
+
const post = await getPost(ctx.params.slug); // via a server query
|
|
301
|
+
return {
|
|
302
|
+
title: post.title,
|
|
303
|
+
jsonLd: {
|
|
304
|
+
'@context': 'https://schema.org',
|
|
305
|
+
'@type': 'Article',
|
|
306
|
+
headline: post.title,
|
|
307
|
+
datePublished: post.publishedAt,
|
|
308
|
+
author: { '@type': 'Person', name: post.authorName },
|
|
309
|
+
},
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
### Escaping guarantee
|
|
315
|
+
|
|
316
|
+
The serialized JSON is HTML-safe-escaped automatically. `<`, `>`, `&`,
|
|
317
|
+
and the line separators U+2028 / U+2029 are replaced with their JSON
|
|
318
|
+
Unicode escapes (`<` and friends). A JSON parser decodes those back to
|
|
319
|
+
the original characters, so the embedded data still parses to your exact
|
|
320
|
+
object, while the literal byte sequence `</script>` can never form in the
|
|
321
|
+
served HTML. So a value containing `</script><img src=x onerror=...>`
|
|
322
|
+
cannot break out of the script tag. You do not escape anything yourself.
|
|
323
|
+
|
|
324
|
+
The block is a NON-EXECUTABLE data island (`type="application/ld+json"`),
|
|
325
|
+
so a Content-Security-Policy `script-src` does not gate it and it carries
|
|
326
|
+
NO nonce.
|
|
327
|
+
|
|
328
|
+
### Robustness
|
|
329
|
+
|
|
330
|
+
The framework fails SAFE per element. An entry that is not a plain object,
|
|
331
|
+
or an object with a circular reference that `JSON.stringify` cannot
|
|
332
|
+
serialize, is skipped (with a one-line `console.warn`) and never breaks
|
|
333
|
+
the rest of the head. Absent `jsonLd` emits nothing (the field is purely
|
|
334
|
+
additive).
|