@ultimat3/seo 7.0.0 → 9.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 +7 -0
- package/package.json +2 -2
- package/src/sitemap.ts +44 -13
package/CLAUDE.md
CHANGED
|
@@ -39,6 +39,13 @@ Tier 1. May import `@ultimat3/core`, `@ultimat3/schema`, `@ultimat3/i18n`. Nothi
|
|
|
39
39
|
because `DEFAULT_WIDTHS` happens to ascend — `widths: [1200, 640]` handed every browser without
|
|
40
40
|
`srcset` support the 640 variant of a 1200-wide image. Never re-derive it from position, and
|
|
41
41
|
never sort inside `usableWidths`: the `srcset` order is the caller's to choose.
|
|
42
|
+
- **`x-default` names a URL the sitemap CONTAINS, or it is not emitted.** `buildSitemap` pushed
|
|
43
|
+
the unprefixed `path` for every route with `locales` set — and with no `defaultLocale`,
|
|
44
|
+
`localize` prefixes every locale, so the whole hreflang cluster pointed at a URL the sitemap
|
|
45
|
+
never lists. A dangling `x-default` is the shape a search engine drops the entire cluster for,
|
|
46
|
+
which costs the alternates that WERE right. `defaultLocaleUrl` answers `undefined` unless the
|
|
47
|
+
default locale's own URL is among the ones this route emits — the same explicit-fallback shape
|
|
48
|
+
`meta.ts`'s `hreflangSet` takes. Never re-derive it from `path`.
|
|
42
49
|
- **Errors name the file, not the URL.** `RouteRecord.file` is in every cause and every fix; an agent must be able to open the source without guessing.
|
|
43
50
|
- **Fail closed, and core reads the key.** `isIndexable()` is `environment === 'production'` and
|
|
44
51
|
nothing else — `staging`, a laptop, a typo and an unset variable all disallow. `ULTIMATE_ENV` has
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ultimat3/seo",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "9.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",
|
|
@@ -31,6 +31,6 @@
|
|
|
31
31
|
"test": "bun test"
|
|
32
32
|
},
|
|
33
33
|
"dependencies": {
|
|
34
|
-
"@ultimat3/core": "
|
|
34
|
+
"@ultimat3/core": "9.0.0"
|
|
35
35
|
}
|
|
36
36
|
}
|
package/src/sitemap.ts
CHANGED
|
@@ -44,7 +44,10 @@ export interface BuildSitemapOptions {
|
|
|
44
44
|
locales?: readonly string[];
|
|
45
45
|
/** Defaults to `/{locale}{path}`. */
|
|
46
46
|
localizePath?: (path: string, locale: string) => string;
|
|
47
|
-
/**
|
|
47
|
+
/**
|
|
48
|
+
* The locale whose URLs are unprefixed and become `x-default`. **Omitted, no `x-default` is
|
|
49
|
+
* emitted at all** — there is no unprefixed URL in the sitemap for it to name.
|
|
50
|
+
*/
|
|
48
51
|
defaultLocale?: string;
|
|
49
52
|
maxUrls?: number;
|
|
50
53
|
lastmod?: string;
|
|
@@ -56,6 +59,24 @@ function localize(path: string, locale: string, options: BuildSitemapOptions): s
|
|
|
56
59
|
return fn(path, locale);
|
|
57
60
|
}
|
|
58
61
|
|
|
62
|
+
/**
|
|
63
|
+
* The path `x-default` may point at: the default locale's own, and only when this route emits it.
|
|
64
|
+
* `undefined` when no `defaultLocale` was declared, or when it names a locale outside `locales` —
|
|
65
|
+
* in both cases the URL it would carry is one the sitemap does not contain.
|
|
66
|
+
*
|
|
67
|
+
* Matched on the LOCALE, never on the path. Recomputing the default's URL and asking whether any
|
|
68
|
+
* entry carries it made a `localizePath` that maps two locales onto one URL answer yes for a
|
|
69
|
+
* `defaultLocale` outside `locales`: the href existed, but it belonged to another locale, so
|
|
70
|
+
* `x-default` spoke for a locale this cluster does not carry. The entry that emits it is the only
|
|
71
|
+
* thing that can prove it exists, so read the answer off that entry.
|
|
72
|
+
*/
|
|
73
|
+
function defaultLocaleUrl(
|
|
74
|
+
localised: readonly { readonly locale: string; readonly path: string }[],
|
|
75
|
+
options: BuildSitemapOptions,
|
|
76
|
+
): string | undefined {
|
|
77
|
+
return localised.find((entry) => entry.locale === options.defaultLocale)?.path;
|
|
78
|
+
}
|
|
79
|
+
|
|
59
80
|
/** Every concrete URL the route table produces, with per-locale alternates. */
|
|
60
81
|
export async function sitemapUrls(
|
|
61
82
|
routes: readonly RouteRecord[],
|
|
@@ -66,23 +87,33 @@ export async function sitemapUrls(
|
|
|
66
87
|
|
|
67
88
|
for (const route of indexableRoutes(routes)) {
|
|
68
89
|
for (const path of await expandRoute(route)) {
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
90
|
+
// Localised once, then read three times — the alternates, the `x-default` candidate and the
|
|
91
|
+
// `<loc>`s below are three questions with one answer, and they drifted apart when each
|
|
92
|
+
// computed its own.
|
|
93
|
+
const localised = locales.map((locale) => ({
|
|
94
|
+
locale,
|
|
95
|
+
path: localize(path, locale, options),
|
|
72
96
|
}));
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
97
|
+
const alternates: SitemapAlternate[] = localised.map((entry) => ({
|
|
98
|
+
hreflang: entry.locale,
|
|
99
|
+
href: absoluteUrl(options.baseUrl, entry.path),
|
|
100
|
+
}));
|
|
101
|
+
// `x-default` only when it names a URL THIS sitemap lists. It was the bare `path` for every
|
|
102
|
+
// route: with `locales` set and no `defaultLocale`, every `<loc>` is prefixed and the
|
|
103
|
+
// unprefixed path is one the sitemap never mentions — an hreflang cluster pointing at a URL
|
|
104
|
+
// outside itself, which is the shape a search engine drops the whole cluster for. Same rule
|
|
105
|
+
// as `meta.ts`'s `hreflangSet`, which takes the fallback href explicitly rather than
|
|
106
|
+
// assuming one exists.
|
|
107
|
+
const fallback = defaultLocaleUrl(localised, options);
|
|
108
|
+
if (alternates.length > 0 && fallback !== undefined) {
|
|
109
|
+
alternates.push({ hreflang: 'x-default', href: absoluteUrl(options.baseUrl, fallback) });
|
|
78
110
|
}
|
|
79
111
|
|
|
80
112
|
const lastmod = route.lastmod ?? options.lastmod;
|
|
81
|
-
const emitFor =
|
|
82
|
-
|
|
83
|
-
for (const localised of emitFor) {
|
|
113
|
+
const emitFor = locales.length === 0 ? [path] : localised.map((entry) => entry.path);
|
|
114
|
+
for (const emitted of emitFor) {
|
|
84
115
|
urls.push({
|
|
85
|
-
loc: absoluteUrl(options.baseUrl,
|
|
116
|
+
loc: absoluteUrl(options.baseUrl, emitted),
|
|
86
117
|
...(lastmod === undefined ? {} : { lastmod }),
|
|
87
118
|
...(route.changefreq === undefined ? {} : { changefreq: route.changefreq }),
|
|
88
119
|
...(route.priority === undefined ? {} : { priority: route.priority }),
|