@devslab/site-kit 0.4.1 → 0.5.1
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/README.ko.md +34 -3
- package/README.md +34 -3
- package/dist/chrome.d.ts +7 -0
- package/dist/chrome.d.ts.map +1 -1
- package/dist/locale-menu.d.ts +7 -1
- package/dist/locale-menu.d.ts.map +1 -1
- package/dist/solid.js +213 -95
- package/dist/solid.js.map +1 -1
- package/dist/solid.server.js +194 -26
- package/dist/solid.server.js.map +1 -1
- package/package.json +2 -2
- package/src/core/catalog.d.mts +7 -3
- package/src/core/catalog.mjs +17 -4
- package/src/core/flags.d.mts +3 -1
- package/src/core/flags.mjs +50 -13
- package/src/core/index.mjs +1 -1
- package/src/core/locales.d.mts +34 -0
- package/src/core/locales.mjs +86 -26
- package/src/core/seo.d.mts +10 -10
- package/src/core/seo.mjs +23 -17
- package/src/tanstack-start.d.mts +9 -2
package/src/core/locales.mjs
CHANGED
|
@@ -15,22 +15,89 @@ export const LOCALES = Object.freeze([
|
|
|
15
15
|
{ code: "ar", language: "Arabic", nativeName: "العربية", dir: "rtl" },
|
|
16
16
|
]);
|
|
17
17
|
|
|
18
|
-
const
|
|
19
|
-
const aliases = new Map([["zh", "zh-TW"], ["pt", "pt-BR"]]);
|
|
18
|
+
const FAMILY_ALIASES = Object.freeze([["zh", "zh-TW"], ["pt", "pt-BR"]]);
|
|
20
19
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
20
|
+
/**
|
|
21
|
+
* The languages a site actually sells in.
|
|
22
|
+
*
|
|
23
|
+
* The list above is the family's — what devslab.kr markets in, and the
|
|
24
|
+
* floor every product gets for free. It is not every product's list.
|
|
25
|
+
* BookLinq sells to salons in India and its assistant already answers in
|
|
26
|
+
* Tamil, Telugu, Bengali, Marathi, Gujarati and Kannada. Those are not
|
|
27
|
+
* family languages and are not going to become family languages: putting
|
|
28
|
+
* them in the list above would hand AskLinq and devslab.kr six entries
|
|
29
|
+
* they have no copy for. But a BookLinq page that cannot render them is a
|
|
30
|
+
* page that lies about what the product does.
|
|
31
|
+
*
|
|
32
|
+
* So the family owns the mechanism and the product names its own nouns. A
|
|
33
|
+
* registry is the family list plus whatever a product adds, and every
|
|
34
|
+
* locale-aware helper here can be bound to one. The bare exports at the
|
|
35
|
+
* bottom are the family registry, so a consumer that never calls this
|
|
36
|
+
* sees exactly what it saw before.
|
|
37
|
+
*
|
|
38
|
+
* An extra locale carries the same fields as a family one plus
|
|
39
|
+
* `flagCountry`, naming a flag this package already vendors (`FLAG_COUNTRY`
|
|
40
|
+
* lists them). Products do not ship SVG: flag data is licensed, generated
|
|
41
|
+
* and security-scanned here, and seven Indian languages share one flag
|
|
42
|
+
* anyway.
|
|
43
|
+
*
|
|
44
|
+
* @param {{ extra?: ReadonlyArray<object>, aliases?: ReadonlyArray<[string, string]> }} [options]
|
|
45
|
+
*/
|
|
46
|
+
export function defineLocaleRegistry(options = {}) {
|
|
47
|
+
const extra = options.extra ?? [];
|
|
48
|
+
const seen = new Set(LOCALES.map(({ code }) => code));
|
|
49
|
+
for (const locale of extra) {
|
|
50
|
+
if (!locale?.code) throw new TypeError("every extra locale needs a code");
|
|
51
|
+
if (seen.has(locale.code)) {
|
|
52
|
+
throw new RangeError(`${locale.code} is already a family locale — remove it from \`extra\``);
|
|
53
|
+
}
|
|
54
|
+
if (!locale.nativeName) throw new TypeError(`${locale.code} needs a nativeName — it is what the picker shows`);
|
|
55
|
+
if (locale.dir !== "ltr" && locale.dir !== "rtl") throw new RangeError(`${locale.code} needs dir "ltr" or "rtl"`);
|
|
56
|
+
seen.add(locale.code);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const locales = Object.freeze([...LOCALES, ...extra.map((locale) => Object.freeze({ ...locale }))]);
|
|
60
|
+
const byLower = new Map(locales.map((locale) => [locale.code.toLowerCase(), locale]));
|
|
61
|
+
const aliases = new Map([...FAMILY_ALIASES, ...(options.aliases ?? [])]);
|
|
62
|
+
|
|
63
|
+
function canonicalLocale(candidate) {
|
|
64
|
+
if (!candidate) return undefined;
|
|
65
|
+
const exact = byLower.get(String(candidate).trim().toLowerCase());
|
|
66
|
+
if (exact) return exact.code;
|
|
67
|
+
const base = String(candidate).trim().toLowerCase().split("-")[0];
|
|
68
|
+
if (aliases.has(base)) return aliases.get(base);
|
|
69
|
+
return byLower.get(base)?.code;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function definitionFor(locale) {
|
|
73
|
+
const canonical = canonicalLocale(locale);
|
|
74
|
+
if (!canonical) throw new RangeError(`Unsupported locale: ${locale}`);
|
|
75
|
+
return byLower.get(canonical.toLowerCase());
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function localeAttributes(locale) {
|
|
79
|
+
const definition = definitionFor(locale);
|
|
80
|
+
// `dir` comes off the definition rather than a test for Arabic. The
|
|
81
|
+
// old form read `canonical === "ar" ? "rtl" : "ltr"`, which was right
|
|
82
|
+
// only while Arabic stayed the family's one RTL language — a product
|
|
83
|
+
// adding Urdu or Hebrew would have had it rendered left-to-right with
|
|
84
|
+
// every test still green.
|
|
85
|
+
return { lang: definition.code, dir: definition.dir };
|
|
86
|
+
}
|
|
29
87
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
88
|
+
function resolveLocale({ pathname = "/", cookie = "", acceptLanguage = "", defaultLocale }) {
|
|
89
|
+
const routeLocale = canonicalLocale(pathname.split("/").filter(Boolean)[0]);
|
|
90
|
+
if (routeLocale) return { locale: routeLocale, source: "route" };
|
|
91
|
+
const storedLocale = canonicalLocale(cookieValue(cookie, "locale"));
|
|
92
|
+
if (storedLocale) return { locale: storedLocale, source: "cookie" };
|
|
93
|
+
const accepted = acceptedLocales(acceptLanguage, canonicalLocale)[0]?.locale;
|
|
94
|
+
if (accepted) return { locale: accepted, source: "accept-language" };
|
|
95
|
+
const fallback = canonicalLocale(defaultLocale);
|
|
96
|
+
if (!fallback) throw new RangeError(`Unsupported default locale: ${defaultLocale}`);
|
|
97
|
+
return { locale: fallback, source: "default" };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return Object.freeze({ LOCALES: locales, canonicalLocale, localeAttributes, resolveLocale, definitionFor });
|
|
34
101
|
}
|
|
35
102
|
|
|
36
103
|
function cookieValue(header, name) {
|
|
@@ -45,7 +112,7 @@ function cookieValue(header, name) {
|
|
|
45
112
|
return undefined;
|
|
46
113
|
}
|
|
47
114
|
|
|
48
|
-
function acceptedLocales(header) {
|
|
115
|
+
function acceptedLocales(header, canonicalLocale) {
|
|
49
116
|
return String(header ?? "").split(",").map((part, order) => {
|
|
50
117
|
const [tag, ...parameters] = part.trim().split(";");
|
|
51
118
|
const q = Number(parameters.find((parameter) => parameter.trim().startsWith("q="))?.split("=")[1] ?? 1);
|
|
@@ -53,14 +120,7 @@ function acceptedLocales(header) {
|
|
|
53
120
|
}).filter(({ locale, q }) => locale && q > 0).sort((a, b) => b.q - a.q || a.order - b.order);
|
|
54
121
|
}
|
|
55
122
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
if (storedLocale) return { locale: storedLocale, source: "cookie" };
|
|
61
|
-
const accepted = acceptedLocales(acceptLanguage)[0]?.locale;
|
|
62
|
-
if (accepted) return { locale: accepted, source: "accept-language" };
|
|
63
|
-
const fallback = canonicalLocale(defaultLocale);
|
|
64
|
-
if (!fallback) throw new RangeError(`Unsupported default locale: ${defaultLocale}`);
|
|
65
|
-
return { locale: fallback, source: "default" };
|
|
66
|
-
}
|
|
123
|
+
/** The family registry — what every helper in this package uses by default. */
|
|
124
|
+
export const FAMILY_LOCALES = defineLocaleRegistry();
|
|
125
|
+
|
|
126
|
+
export const { canonicalLocale, localeAttributes, resolveLocale } = FAMILY_LOCALES;
|
package/src/core/seo.d.mts
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
|
-
import type { SiteLocale, TextDirection } from "./locales.mjs";
|
|
2
|
-
export interface MetadataInput { baseUrl: string; path: string; locale:
|
|
3
|
-
export interface SiteMetadata {
|
|
4
|
-
html: { lang:
|
|
1
|
+
import type { LocaleRegistry, SiteLocale, TextDirection } from "./locales.mjs";
|
|
2
|
+
export interface MetadataInput<Code extends string = SiteLocale> { baseUrl: string; path: string; locale: Code; defaultLocale: Code; title: string; description: string; siteName: string; image: string; registry?: LocaleRegistry<Code> }
|
|
3
|
+
export interface SiteMetadata<Code extends string = SiteLocale> {
|
|
4
|
+
html: { lang: Code; dir: TextDirection };
|
|
5
5
|
title: string; description: string; canonical: string;
|
|
6
|
-
alternates: Array<{ hreflang:
|
|
7
|
-
openGraph: { type: "website"; locale:
|
|
6
|
+
alternates: Array<{ hreflang: Code | "x-default"; href: string }>;
|
|
7
|
+
openGraph: { type: "website"; locale: Code; url: string; siteName: string; title: string; description: string; images: Array<{ url: string }> };
|
|
8
8
|
twitter: { card: "summary_large_image"; title: string; description: string; image: string };
|
|
9
9
|
}
|
|
10
|
-
export declare function localizedPath(path: string, locale:
|
|
11
|
-
export declare function localizedUrl(baseUrl: string, path: string, locale:
|
|
12
|
-
export declare function buildMetadata(input: MetadataInput): SiteMetadata
|
|
13
|
-
export declare function buildSitemap(input: { baseUrl: string; routes: string[]; defaultLocale:
|
|
10
|
+
export declare function localizedPath<Code extends string = SiteLocale>(path: string, locale: Code, defaultLocale: Code, registry?: LocaleRegistry<Code>): string;
|
|
11
|
+
export declare function localizedUrl<Code extends string = SiteLocale>(baseUrl: string, path: string, locale: Code, defaultLocale: Code, registry?: LocaleRegistry<Code>): string;
|
|
12
|
+
export declare function buildMetadata<Code extends string = SiteLocale>(input: MetadataInput<Code>): SiteMetadata<Code>;
|
|
13
|
+
export declare function buildSitemap<Code extends string = SiteLocale>(input: { baseUrl: string; routes: string[]; defaultLocale: Code; lastModified?: string; registry?: LocaleRegistry<Code> }): Array<{ loc: string; locale: Code; alternates: Array<{ hreflang: Code | "x-default"; href: string }>; lastmod?: string }>;
|
|
14
14
|
export declare function renderSitemapXml(entries: ReturnType<typeof buildSitemap>): string;
|
|
15
15
|
export type RobotsPolicy = "allow" | "disallow";
|
|
16
16
|
export declare const ROBOTS_USER_AGENTS: { readonly citation: readonly string[]; readonly modelTraining: readonly string[] };
|
package/src/core/seo.mjs
CHANGED
|
@@ -1,4 +1,10 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { FAMILY_LOCALES } from "./locales.mjs";
|
|
2
|
+
|
|
3
|
+
// Every function here that names a locale takes an optional `registry`.
|
|
4
|
+
// Omitted it is the family's fourteen, which is what a family site wants.
|
|
5
|
+
// A product that added its own languages must pass its registry, or the
|
|
6
|
+
// hreflang alternates and the sitemap would quietly omit them — the page
|
|
7
|
+
// would render in Tamil while telling search engines Tamil does not exist.
|
|
2
8
|
|
|
3
9
|
const cleanBase = (baseUrl) => String(baseUrl).replace(/\/+$/, "");
|
|
4
10
|
const cleanPath = (path) => {
|
|
@@ -6,27 +12,27 @@ const cleanPath = (path) => {
|
|
|
6
12
|
return value === "/" ? "/" : value;
|
|
7
13
|
};
|
|
8
14
|
|
|
9
|
-
export function localizedPath(path, locale, defaultLocale) {
|
|
10
|
-
const canonical = canonicalLocale(locale);
|
|
11
|
-
const fallback = canonicalLocale(defaultLocale);
|
|
15
|
+
export function localizedPath(path, locale, defaultLocale, registry = FAMILY_LOCALES) {
|
|
16
|
+
const canonical = registry.canonicalLocale(locale);
|
|
17
|
+
const fallback = registry.canonicalLocale(defaultLocale);
|
|
12
18
|
if (!canonical || !fallback) throw new RangeError("localizedPath received an unsupported locale");
|
|
13
19
|
const normalized = cleanPath(path);
|
|
14
20
|
return canonical === fallback ? normalized : `/${canonical}${normalized === "/" ? "" : normalized}`;
|
|
15
21
|
}
|
|
16
22
|
|
|
17
|
-
export function localizedUrl(baseUrl, path, locale, defaultLocale) {
|
|
18
|
-
return `${cleanBase(baseUrl)}${localizedPath(path, locale, defaultLocale)}`;
|
|
23
|
+
export function localizedUrl(baseUrl, path, locale, defaultLocale, registry = FAMILY_LOCALES) {
|
|
24
|
+
return `${cleanBase(baseUrl)}${localizedPath(path, locale, defaultLocale, registry)}`;
|
|
19
25
|
}
|
|
20
26
|
|
|
21
|
-
export function buildMetadata({ baseUrl, path, locale, defaultLocale, title, description, siteName, image }) {
|
|
22
|
-
const canonicalLocaleCode = canonicalLocale(locale);
|
|
27
|
+
export function buildMetadata({ baseUrl, path, locale, defaultLocale, title, description, siteName, image, registry = FAMILY_LOCALES }) {
|
|
28
|
+
const canonicalLocaleCode = registry.canonicalLocale(locale);
|
|
23
29
|
if (!canonicalLocaleCode) throw new RangeError(`Unsupported locale: ${locale}`);
|
|
24
|
-
const canonical = localizedUrl(baseUrl, path, canonicalLocaleCode, defaultLocale);
|
|
25
|
-
const alternates = LOCALES.map(({ code }) => ({ hreflang: code, href: localizedUrl(baseUrl, path, code, defaultLocale) }));
|
|
26
|
-
alternates.push({ hreflang: "x-default", href: localizedUrl(baseUrl, path, defaultLocale, defaultLocale) });
|
|
30
|
+
const canonical = localizedUrl(baseUrl, path, canonicalLocaleCode, defaultLocale, registry);
|
|
31
|
+
const alternates = registry.LOCALES.map(({ code }) => ({ hreflang: code, href: localizedUrl(baseUrl, path, code, defaultLocale, registry) }));
|
|
32
|
+
alternates.push({ hreflang: "x-default", href: localizedUrl(baseUrl, path, defaultLocale, defaultLocale, registry) });
|
|
27
33
|
const imageUrl = new URL(image, `${cleanBase(baseUrl)}/`).href;
|
|
28
34
|
return {
|
|
29
|
-
html: localeAttributes(canonicalLocaleCode),
|
|
35
|
+
html: registry.localeAttributes(canonicalLocaleCode),
|
|
30
36
|
title,
|
|
31
37
|
description,
|
|
32
38
|
canonical,
|
|
@@ -44,13 +50,13 @@ export function buildMetadata({ baseUrl, path, locale, defaultLocale, title, des
|
|
|
44
50
|
};
|
|
45
51
|
}
|
|
46
52
|
|
|
47
|
-
export function buildSitemap({ baseUrl, routes, defaultLocale, lastModified }) {
|
|
48
|
-
return routes.flatMap((path) => LOCALES.map(({ code }) => ({
|
|
49
|
-
loc: localizedUrl(baseUrl, path, code, defaultLocale),
|
|
53
|
+
export function buildSitemap({ baseUrl, routes, defaultLocale, lastModified, registry = FAMILY_LOCALES }) {
|
|
54
|
+
return routes.flatMap((path) => registry.LOCALES.map(({ code }) => ({
|
|
55
|
+
loc: localizedUrl(baseUrl, path, code, defaultLocale, registry),
|
|
50
56
|
locale: code,
|
|
51
57
|
alternates: [
|
|
52
|
-
...LOCALES.map(({ code: alternate }) => ({ hreflang: alternate, href: localizedUrl(baseUrl, path, alternate, defaultLocale) })),
|
|
53
|
-
{ hreflang: "x-default", href: localizedUrl(baseUrl, path, defaultLocale, defaultLocale) },
|
|
58
|
+
...registry.LOCALES.map(({ code: alternate }) => ({ hreflang: alternate, href: localizedUrl(baseUrl, path, alternate, defaultLocale, registry) })),
|
|
59
|
+
{ hreflang: "x-default", href: localizedUrl(baseUrl, path, defaultLocale, defaultLocale, registry) },
|
|
54
60
|
],
|
|
55
61
|
...(lastModified ? { lastmod: lastModified } : {}),
|
|
56
62
|
})));
|
package/src/tanstack-start.d.mts
CHANGED
|
@@ -1,3 +1,10 @@
|
|
|
1
1
|
import type { SiteMetadata } from "./core/seo.mjs";
|
|
2
|
-
|
|
3
|
-
|
|
2
|
+
import type { SiteLocale } from "./core/locales.mjs";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Generic over the locale code, like the builders that feed it. A product
|
|
6
|
+
* registry (D-018) yields `SiteMetadata<string>`; the adapter reads the
|
|
7
|
+
* same fields whatever the code type is, so it must not refuse that.
|
|
8
|
+
*/
|
|
9
|
+
export declare function toTanStackHead<Code extends string = SiteLocale>(metadata: SiteMetadata<Code>): { meta: Array<Record<string, string>>; links: Array<Record<string, string>> };
|
|
10
|
+
export declare const toHtmlAttributes: <Code extends string = SiteLocale>(metadata: SiteMetadata<Code>) => { lang: Code; dir: string };
|