@ultimat3/seo 22.15.0 → 23.0.0
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/CLAUDE.md +5 -0
- package/README.md +1 -1
- package/package.json +4 -3
- package/src/meta.ts +46 -27
package/CLAUDE.md
CHANGED
|
@@ -114,6 +114,11 @@ Tier 1. May import `@ultimat3/core`, `@ultimat3/schema`, `@ultimat3/i18n`. Nothi
|
|
|
114
114
|
`Math.max(...times)`: a spread is one argument per item and the engine's limit is the caller's
|
|
115
115
|
stack depth, not the feed's size. "Now" arrives through `BuildFeedOptions.clock`; nothing in
|
|
116
116
|
`rss.ts` reads a clock of its own.
|
|
117
|
+
- **A document that is not indexed carries no social card.** `renderMeta` emits `og:*`, `article:*`
|
|
118
|
+
and `twitter:*` only when `meta.robots?.index !== false` — declared or derived, one rule and no
|
|
119
|
+
`social: false` beside it. The tags were unconditional, so every admin screen, every 4xx/5xx
|
|
120
|
+
(`defineRoute` marks them `noindex`) and every draft published its title and description to
|
|
121
|
+
whoever pasted the URL into a chat. `follow: false` alone withdraws nothing. `meta.test.ts`.
|
|
117
122
|
- Only `site/` routes are SEO-checked — `app/` is behind auth and crawlers never authenticate.
|
|
118
123
|
|
|
119
124
|
## Files
|
package/README.md
CHANGED
|
@@ -44,7 +44,7 @@ a job boundary the class is gone and the `code` is what survives — match on th
|
|
|
44
44
|
|
|
45
45
|
| File | Owns |
|
|
46
46
|
|---|---|
|
|
47
|
-
| `meta.ts` | the metadata model, `renderMeta()` → head tags: title template, canonical, robots, `og:*`, `twitter:*`, hreflang + `x-default`, `theme-color` per colour scheme |
|
|
47
|
+
| `meta.ts` | the metadata model, `renderMeta()` → head tags: title template, canonical, robots, `og:*`, `twitter:*`, hreflang + `x-default`, `theme-color` per colour scheme. **`robots: { index: false }` withdraws every `og:*`, `article:*` and `twitter:*` tag** — declared or derived; there is no `social` switch |
|
|
48
48
|
| `validate.ts` | the build gate — `validateMeta()` (`--json`-shaped) and `assertMeta()` |
|
|
49
49
|
| `ld.ts` | typed JSON-LD builders; required fields are required in the **input type** |
|
|
50
50
|
| `sitemap.ts` | `buildSitemap()` from the route table + each route's `prerender()`, per-locale alternates, automatic index splitting past 50k |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ultimat3/seo",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "23.0.0",
|
|
4
4
|
"description": "Enforced SEO: typed meta, JSON-LD, sitemap, robots, feeds, responsive images, perf budgets",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
"files": [
|
|
20
20
|
"src",
|
|
21
21
|
"!src/**/*.test.ts",
|
|
22
|
+
"!src/**/*-fixture.ts",
|
|
22
23
|
"CLAUDE.md",
|
|
23
24
|
"README.md",
|
|
24
25
|
"LICENSE"
|
|
@@ -31,7 +32,7 @@
|
|
|
31
32
|
"test": "bun test"
|
|
32
33
|
},
|
|
33
34
|
"dependencies": {
|
|
34
|
-
"@ultimat3/core": "
|
|
35
|
-
"@ultimat3/schema": "
|
|
35
|
+
"@ultimat3/core": "23.0.0",
|
|
36
|
+
"@ultimat3/schema": "23.0.0"
|
|
36
37
|
}
|
|
37
38
|
}
|
package/src/meta.ts
CHANGED
|
@@ -197,35 +197,23 @@ export function hreflangSet(
|
|
|
197
197
|
return href === undefined ? alternates : [...alternates, { hreflang: 'x-default', href }];
|
|
198
198
|
}
|
|
199
199
|
|
|
200
|
-
|
|
200
|
+
/**
|
|
201
|
+
* The card a link preview is built from: `og:*`, `article:*`, `twitter:*`.
|
|
202
|
+
*
|
|
203
|
+
* Emitted only for a document that may be INDEXED. `robots: { index: false }` withdraws the whole
|
|
204
|
+
* set, declared or derived — one rule, no second switch: a page its author keeps out of a search
|
|
205
|
+
* result is a page behind sign-in, an error, or a draft, and a card for it publishes the title and
|
|
206
|
+
* description the `noindex` was hiding. Every admin document carried one until 2026-10.
|
|
207
|
+
*/
|
|
208
|
+
function socialTags(
|
|
209
|
+
meta: RouteMeta,
|
|
210
|
+
localization: MetaLocalization | undefined,
|
|
211
|
+
canonicalHref: string | undefined,
|
|
212
|
+
abs: (value: string) => string,
|
|
213
|
+
): readonly HeadTag[] {
|
|
214
|
+
if (meta.robots?.index === false) return [];
|
|
201
215
|
const tags: HeadTag[] = [];
|
|
202
|
-
const abs = (value: string): string =>
|
|
203
|
-
options.baseUrl === undefined ? value : absoluteUrl(options.baseUrl, value);
|
|
204
|
-
|
|
205
|
-
const canonical = meta.canonical ?? (options.path === undefined ? undefined : options.path);
|
|
206
|
-
const canonicalHref = canonical === undefined ? undefined : abs(canonical);
|
|
207
|
-
|
|
208
|
-
if (meta.title !== undefined) {
|
|
209
|
-
tags.push({
|
|
210
|
-
tag: 'title',
|
|
211
|
-
attrs: {},
|
|
212
|
-
text: applyTitleTemplate(meta.title, meta.titleTemplate),
|
|
213
|
-
});
|
|
214
|
-
}
|
|
215
|
-
if (meta.description !== undefined) {
|
|
216
|
-
tags.push({ tag: 'meta', attrs: { name: 'description', content: meta.description } });
|
|
217
|
-
}
|
|
218
|
-
if (canonicalHref !== undefined) {
|
|
219
|
-
tags.push({ tag: 'link', attrs: { rel: 'canonical', href: canonicalHref } });
|
|
220
|
-
}
|
|
221
|
-
tags.push({
|
|
222
|
-
tag: 'meta',
|
|
223
|
-
attrs: { name: 'robots', content: robotsContent(meta.robots ?? {}) },
|
|
224
|
-
});
|
|
225
|
-
|
|
226
|
-
// --- Open Graph ------------------------------------------------------------
|
|
227
216
|
const og = meta.og ?? {};
|
|
228
|
-
const localization = options.localization;
|
|
229
217
|
const ogLocale =
|
|
230
218
|
og.locale ?? (localization === undefined ? undefined : ogLocaleTag(localization.locale));
|
|
231
219
|
const ogEntries: Array<[string, string | undefined]> = [
|
|
@@ -298,6 +286,37 @@ export function renderMeta(meta: RouteMeta, options: RenderMetaOptions = {}): re
|
|
|
298
286
|
for (const [name, content] of twitterEntries) {
|
|
299
287
|
if (content !== undefined) tags.push({ tag: 'meta', attrs: { name, content } });
|
|
300
288
|
}
|
|
289
|
+
return tags;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
export function renderMeta(meta: RouteMeta, options: RenderMetaOptions = {}): readonly HeadTag[] {
|
|
293
|
+
const tags: HeadTag[] = [];
|
|
294
|
+
const abs = (value: string): string =>
|
|
295
|
+
options.baseUrl === undefined ? value : absoluteUrl(options.baseUrl, value);
|
|
296
|
+
|
|
297
|
+
const canonical = meta.canonical ?? (options.path === undefined ? undefined : options.path);
|
|
298
|
+
const canonicalHref = canonical === undefined ? undefined : abs(canonical);
|
|
299
|
+
|
|
300
|
+
if (meta.title !== undefined) {
|
|
301
|
+
tags.push({
|
|
302
|
+
tag: 'title',
|
|
303
|
+
attrs: {},
|
|
304
|
+
text: applyTitleTemplate(meta.title, meta.titleTemplate),
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
if (meta.description !== undefined) {
|
|
308
|
+
tags.push({ tag: 'meta', attrs: { name: 'description', content: meta.description } });
|
|
309
|
+
}
|
|
310
|
+
if (canonicalHref !== undefined) {
|
|
311
|
+
tags.push({ tag: 'link', attrs: { rel: 'canonical', href: canonicalHref } });
|
|
312
|
+
}
|
|
313
|
+
tags.push({
|
|
314
|
+
tag: 'meta',
|
|
315
|
+
attrs: { name: 'robots', content: robotsContent(meta.robots ?? {}) },
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
const localization = options.localization;
|
|
319
|
+
tags.push(...socialTags(meta, localization, canonicalHref, abs));
|
|
301
320
|
|
|
302
321
|
// --- hreflang --------------------------------------------------------------
|
|
303
322
|
// A route's own `alternates` win whole; otherwise the cluster comes from the routed locales.
|