@ultimat3/seo 27.16.1 → 29.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 +9 -5
- package/README.md +11 -1
- package/package.json +4 -4
- package/src/meta.ts +14 -5
package/CLAUDE.md
CHANGED
|
@@ -141,11 +141,15 @@ Tier 1. May import `@ultimat3/core`, `@ultimat3/schema`, `@ultimat3/i18n`. Nothi
|
|
|
141
141
|
`Math.max(...times)`: a spread is one argument per item and the engine's limit is the caller's
|
|
142
142
|
stack depth, not the feed's size. "Now" arrives through `BuildFeedOptions.clock`; nothing in
|
|
143
143
|
`rss.ts` reads a clock of its own.
|
|
144
|
-
- **A document that is not indexed carries no social card
|
|
145
|
-
and `twitter:*`
|
|
146
|
-
`
|
|
147
|
-
|
|
148
|
-
|
|
144
|
+
- **A document that is not indexed carries no social card — unless the route asks for one.**
|
|
145
|
+
`renderMeta` emits `og:*`, `article:*` and `twitter:*` unless `meta.robots?.index === false`, and
|
|
146
|
+
a route declaring `socialCard: true` gets the card anyway: exactly the card an indexable
|
|
147
|
+
document gets, declared values winning and the derived ones coming with it. That field is the
|
|
148
|
+
ONE switch — the robots line is never touched by it, so the route stays noindex, and a route
|
|
149
|
+
without it keeps today's suppression. The tags were unconditional, so every admin screen, every
|
|
150
|
+
4xx/5xx (`defineRoute` marks them `noindex`) and every draft published its title and description
|
|
151
|
+
to whoever pasted the URL into a chat. `follow: false` alone withdraws nothing.
|
|
152
|
+
`meta-social.test.ts`.
|
|
149
153
|
- Only `site/` routes are SEO-checked — `app/` is behind auth and crawlers never authenticate.
|
|
150
154
|
|
|
151
155
|
## Files
|
package/README.md
CHANGED
|
@@ -45,7 +45,7 @@ a job boundary the class is gone and the `code` is what survives — match on th
|
|
|
45
45
|
|
|
46
46
|
| File | Owns |
|
|
47
47
|
|---|---|
|
|
48
|
-
| `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;
|
|
48
|
+
| `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; the one opt-in is `socialCard: true` |
|
|
49
49
|
| `validate.ts` | the build gate — `validateMeta()` (`--json`-shaped) and `assertMeta()` |
|
|
50
50
|
| `ld.ts` | typed JSON-LD builders; required fields are required in the **input type** |
|
|
51
51
|
| `sitemap.ts` | `buildSitemap()` from the route table + each route's `prerender()`, per-locale alternates. One urlset at `/sitemap.xml`; past a cap (`SITEMAP_MAX_URLS` 50,000, `SITEMAP_MAX_BYTES` 50 MB) the index stays `/sitemap.xml` and the parts are `/sitemaps/<n>.xml` (`SITEMAP_PARTS_DIR`). With `sections` it is always an index of `/sitemaps/<name>-<n>.xml` — see below |
|
|
@@ -68,6 +68,16 @@ ld.Article({ headline: 'Ship it', author: { name: 'Ada' } });
|
|
|
68
68
|
A missing `datePublished` is a compile error, not a Search Console warning three
|
|
69
69
|
weeks later.
|
|
70
70
|
|
|
71
|
+
## Social card
|
|
72
|
+
|
|
73
|
+
A document that may not be indexed carries no `og:*`, `article:*` or `twitter:*` tag — declared or
|
|
74
|
+
derived. One opt-in, declared by the route:
|
|
75
|
+
|
|
76
|
+
| Rule | |
|
|
77
|
+
|---|---|
|
|
78
|
+
| `socialCard: true` | the card is emitted exactly as for an indexable page — declared values win, the derived ones come with it |
|
|
79
|
+
| the robots directives | never changed by the opt-in — the route stays noindex |
|
|
80
|
+
|
|
71
81
|
## Sitemap: pages, sections, caps
|
|
72
82
|
|
|
73
83
|
```ts
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ultimat3/seo",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "29.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",
|
|
@@ -25,14 +25,14 @@
|
|
|
25
25
|
"LICENSE"
|
|
26
26
|
],
|
|
27
27
|
"engines": {
|
|
28
|
-
"bun": ">=1.4.
|
|
28
|
+
"bun": ">=1.4.4"
|
|
29
29
|
},
|
|
30
30
|
"scripts": {
|
|
31
31
|
"typecheck": "tsc --noEmit -p tsconfig.json",
|
|
32
32
|
"test": "bun test"
|
|
33
33
|
},
|
|
34
34
|
"dependencies": {
|
|
35
|
-
"@ultimat3/core": "
|
|
36
|
-
"@ultimat3/schema": "
|
|
35
|
+
"@ultimat3/core": "29.0.0",
|
|
36
|
+
"@ultimat3/schema": "29.0.0"
|
|
37
37
|
}
|
|
38
38
|
}
|
package/src/meta.ts
CHANGED
|
@@ -74,6 +74,13 @@ export interface RouteMeta {
|
|
|
74
74
|
description?: string;
|
|
75
75
|
canonical?: string;
|
|
76
76
|
robots?: RobotsDirectives;
|
|
77
|
+
/**
|
|
78
|
+
* The social card opt-in for a document that may NOT be indexed. `robots: { index: false }`
|
|
79
|
+
* withdraws every `og:*`, `article:*` and `twitter:*` tag; a route declaring `socialCard: true`
|
|
80
|
+
* gets the card anyway — emitted exactly as an indexable document's, declared values winning and
|
|
81
|
+
* the derived ones coming with it. The robots directives are never touched by this field.
|
|
82
|
+
*/
|
|
83
|
+
socialCard?: boolean;
|
|
77
84
|
og?: OpenGraph;
|
|
78
85
|
twitter?: TwitterCard;
|
|
79
86
|
/** One entry per locale. `x-default` is added automatically when absent. */
|
|
@@ -222,10 +229,12 @@ export function hreflangSet(
|
|
|
222
229
|
/**
|
|
223
230
|
* The card a link preview is built from: `og:*`, `article:*`, `twitter:*`.
|
|
224
231
|
*
|
|
225
|
-
*
|
|
226
|
-
*
|
|
227
|
-
*
|
|
228
|
-
*
|
|
232
|
+
* Withdrawn from a document that may not be INDEXED: `robots: { index: false }` is a page its
|
|
233
|
+
* author keeps out of a search result — behind sign-in, an error, or a draft — and a card for it
|
|
234
|
+
* publishes the title and description the `noindex` was hiding. A route that wants the card anyway
|
|
235
|
+
* says so ONCE, with `socialCard: true`, and then gets exactly the card an indexable document
|
|
236
|
+
* gets: declared values win, the derived ones come with it. That field never touches the robots
|
|
237
|
+
* directives, so the route stays noindex.
|
|
229
238
|
*/
|
|
230
239
|
function socialTags(
|
|
231
240
|
meta: RouteMeta,
|
|
@@ -233,7 +242,7 @@ function socialTags(
|
|
|
233
242
|
canonicalHref: string | undefined,
|
|
234
243
|
abs: (value: string) => string,
|
|
235
244
|
): readonly HeadTag[] {
|
|
236
|
-
if (meta.robots?.index === false) return [];
|
|
245
|
+
if (meta.robots?.index === false && meta.socialCard !== true) return [];
|
|
237
246
|
const tags: HeadTag[] = [];
|
|
238
247
|
const og = meta.og ?? {};
|
|
239
248
|
const ogLocale =
|