@webjsdev/cli 0.10.12 → 0.10.13

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.
@@ -1,334 +0,0 @@
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).