@jdevalk/astro-seo-graph 0.1.0-alpha.1 → 0.2.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/README.md +82 -12
- package/dist/alternates.d.ts +63 -0
- package/dist/alternates.d.ts.map +1 -0
- package/dist/alternates.js +113 -0
- package/dist/alternates.js.map +1 -0
- package/dist/components/seo-props.d.ts +23 -0
- package/dist/components/seo-props.d.ts.map +1 -1
- package/dist/components/seo-props.js +24 -5
- package/dist/components/seo-props.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/package.json +60 -60
- package/LICENSE +0 -21
package/README.md
CHANGED
|
@@ -4,24 +4,27 @@ Astro integration for [`@jdevalk/seo-graph-core`](../seo-graph-core). Ships a
|
|
|
4
4
|
`<Seo>` component, route factories for agent-ready schema endpoints, a
|
|
5
5
|
content-collection aggregator, and Zod helpers for content schemas.
|
|
6
6
|
|
|
7
|
-
> **Status:** pre-
|
|
8
|
-
>
|
|
7
|
+
> **Status:** `0.1.0` (pre-1.0). The API works and has two real consumers
|
|
8
|
+
> in production (joost.blog and limonaia.house), but a few known warts in
|
|
9
|
+
> the core will be smoothed out in `0.2.x` without breaking changes. See
|
|
10
|
+
> `@jdevalk/seo-graph-core`'s README for the full list.
|
|
9
11
|
|
|
10
12
|
## What's in v0.1
|
|
11
13
|
|
|
12
|
-
| API | Purpose
|
|
13
|
-
| ------------------------------ |
|
|
14
|
-
| **`<Seo>`** (`./Seo.astro`) | Single head component covering `<title>`, meta description, canonical, Open Graph, Twitter card, and optional JSON-LD `@graph`. Wraps [`astro-seo`](https://github.com/jonasmerlin/astro-seo) for the meta tags.
|
|
15
|
-
| **`createSchemaEndpoint`** | Factory returning an Astro `APIRoute` handler that serves a corpus-wide JSON-LD `@graph` for a content collection.
|
|
16
|
-
| **`createSchemaMap`** | Factory returning an `APIRoute` handler that emits a sitemap-style XML listing of your site's schema endpoints — the discovery point for agent crawlers.
|
|
17
|
-
| **`aggregate`** | Shared engine behind the endpoint factories. Walks a list of entries, runs a caller-supplied mapper, deduplicates by `@id`.
|
|
18
|
-
| **`seoSchema`, `imageSchema`** | Zod schemas for the `seo` and `image` fields on content collections. Import them into `src/content.config.ts`.
|
|
19
|
-
| **`buildAstroSeoProps`** | Pure-TS logic that powers `<Seo>` — exported for users who want to feed a different head component.
|
|
14
|
+
| API | Purpose |
|
|
15
|
+
| ------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
16
|
+
| **`<Seo>`** (`./Seo.astro`) | Single head component covering `<title>`, meta description, canonical, Open Graph, Twitter card, hreflang alternates, and optional JSON-LD `@graph`. Wraps [`astro-seo`](https://github.com/jonasmerlin/astro-seo) for the meta tags. |
|
|
17
|
+
| **`createSchemaEndpoint`** | Factory returning an Astro `APIRoute` handler that serves a corpus-wide JSON-LD `@graph` for a content collection. |
|
|
18
|
+
| **`createSchemaMap`** | Factory returning an `APIRoute` handler that emits a sitemap-style XML listing of your site's schema endpoints — the discovery point for agent crawlers. |
|
|
19
|
+
| **`aggregate`** | Shared engine behind the endpoint factories. Walks a list of entries, runs a caller-supplied mapper, deduplicates by `@id`. |
|
|
20
|
+
| **`seoSchema`, `imageSchema`** | Zod schemas for the `seo` and `image` fields on content collections. Import them into `src/content.config.ts`. |
|
|
21
|
+
| **`buildAstroSeoProps`** | Pure-TS logic that powers `<Seo>` — exported for users who want to feed a different head component. |
|
|
22
|
+
| **`buildAlternateLinks`** | Pure helper that turns a `{ hreflang, href }` entry list into normalized `<link rel="alternate">` tags plus an `x-default`. Used internally by `<Seo>`'s `alternates` prop, and exported for non-Astro callers (e.g. CMS plugins feeding their own metadata pipelines). |
|
|
20
23
|
|
|
21
|
-
## Not in
|
|
24
|
+
## Not in `0.1.x` (coming in `0.2.x`)
|
|
22
25
|
|
|
23
26
|
- **`createOgRenderer`** — a themeable `satori` + `sharp` wrapper for generating
|
|
24
|
-
Open Graph images at build time. Pulled out of
|
|
27
|
+
Open Graph images at build time. Pulled out of `0.1` to keep the dep tree
|
|
25
28
|
free of native binaries. Keep using your own `og-image.ts` for now.
|
|
26
29
|
|
|
27
30
|
## Installation
|
|
@@ -75,6 +78,73 @@ const graph = buildSchemaGraph({
|
|
|
75
78
|
</html>
|
|
76
79
|
```
|
|
77
80
|
|
|
81
|
+
## hreflang alternates
|
|
82
|
+
|
|
83
|
+
For multilingual sites, pass an `alternates` prop with one entry per locale.
|
|
84
|
+
`<Seo>` emits a `<link rel="alternate">` for every entry plus an
|
|
85
|
+
`x-default`, normalizes BCP 47 tags on the way out, and drops entries with
|
|
86
|
+
relative or non-http(s) URLs.
|
|
87
|
+
|
|
88
|
+
```astro
|
|
89
|
+
---
|
|
90
|
+
import Seo from '@jdevalk/astro-seo-graph/Seo.astro';
|
|
91
|
+
---
|
|
92
|
+
|
|
93
|
+
<Seo
|
|
94
|
+
title="Hello"
|
|
95
|
+
alternates={{
|
|
96
|
+
defaultLocale: 'en',
|
|
97
|
+
entries: [
|
|
98
|
+
{ hreflang: 'en', href: 'https://example.com/hello/' },
|
|
99
|
+
{ hreflang: 'fr-CA', href: 'https://example.com/fr-ca/bonjour/' },
|
|
100
|
+
{ hreflang: 'nl', href: 'https://example.com/nl/hallo/' },
|
|
101
|
+
],
|
|
102
|
+
}}
|
|
103
|
+
/>
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
Renders roughly:
|
|
107
|
+
|
|
108
|
+
```html
|
|
109
|
+
<link rel="alternate" hreflang="en" href="https://example.com/hello/" />
|
|
110
|
+
<link rel="alternate" hreflang="fr-CA" href="https://example.com/fr-ca/bonjour/" />
|
|
111
|
+
<link rel="alternate" hreflang="nl" href="https://example.com/nl/hallo/" />
|
|
112
|
+
<link rel="alternate" hreflang="x-default" href="https://example.com/hello/" />
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
### Rules
|
|
116
|
+
|
|
117
|
+
- **Absolute URLs only.** Relative (`/hello/`), protocol-relative (`//…`), and non-http schemes (`mailto:`) are dropped silently.
|
|
118
|
+
- **Include the current page.** Google treats self-referential hreflang as required, not optional.
|
|
119
|
+
- **BCP 47 normalization.** `fr-ca` becomes `fr-CA`, `zh-hant-hk` becomes `zh-Hant-HK`. Language subtag lowercase, script subtag title-case, region subtag uppercase.
|
|
120
|
+
- **First entry wins.** Duplicate normalized tags are collapsed to the first one.
|
|
121
|
+
- **Automatic `x-default`.** Points at `defaultLocale` if it matches an entry; otherwise falls back to the first entry.
|
|
122
|
+
- **< 2 entries → nothing emitted.** A single-locale page has no meaningful alternates.
|
|
123
|
+
- **`"x-default"` is reserved.** Passing it as an input `hreflang` gets dropped; it's only ever added automatically.
|
|
124
|
+
|
|
125
|
+
### Feeding `buildAlternateLinks` from other renderers
|
|
126
|
+
|
|
127
|
+
If you're not using `<Seo>` directly (e.g. you're writing a CMS plugin that
|
|
128
|
+
contributes to its own metadata pipeline), import `buildAlternateLinks` from
|
|
129
|
+
the main package entry:
|
|
130
|
+
|
|
131
|
+
```ts
|
|
132
|
+
import { buildAlternateLinks } from '@jdevalk/astro-seo-graph';
|
|
133
|
+
|
|
134
|
+
const links = buildAlternateLinks({
|
|
135
|
+
defaultLocale: 'en',
|
|
136
|
+
entries: [
|
|
137
|
+
{ hreflang: 'en', href: siteEn },
|
|
138
|
+
{ hreflang: 'fr', href: siteFr },
|
|
139
|
+
],
|
|
140
|
+
});
|
|
141
|
+
// → [{ rel: 'alternate', hreflang: 'en', href: ... }, ..., { hreflang: 'x-default', ... }]
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
The main package entry is pure TypeScript — importing `buildAlternateLinks`
|
|
145
|
+
does not pull in any Astro runtime, so it's safe to use from non-Astro
|
|
146
|
+
contexts.
|
|
147
|
+
|
|
78
148
|
## Schema endpoints
|
|
79
149
|
|
|
80
150
|
```ts
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* hreflang alternate-language link builder.
|
|
3
|
+
*
|
|
4
|
+
* Pure function: no DOM, no fetch, no Astro runtime. Safe to import from
|
|
5
|
+
* non-Astro TypeScript (in particular, from `@jdevalk/emdash-plugin-seo`'s
|
|
6
|
+
* EmDash `page:metadata` hook).
|
|
7
|
+
*
|
|
8
|
+
* Callers provide a set of `{ hreflang, href }` entries; this module
|
|
9
|
+
* normalizes BCP 47 tags, validates URLs, dedupes, and appends an
|
|
10
|
+
* `x-default` entry. It does NOT source translation data, construct URLs
|
|
11
|
+
* from slugs, or know about any particular CMS.
|
|
12
|
+
*/
|
|
13
|
+
/** Output shape: a single rendered `<link rel="alternate" hreflang="…" href="…">` tag. */
|
|
14
|
+
export interface AlternateLink {
|
|
15
|
+
/** Always `"alternate"`. */
|
|
16
|
+
rel: 'alternate';
|
|
17
|
+
/** Normalized BCP 47 tag (e.g. `"fr-CA"`) or the literal `"x-default"`. */
|
|
18
|
+
hreflang: string;
|
|
19
|
+
/** Absolute `http(s)://` URL string. */
|
|
20
|
+
href: string;
|
|
21
|
+
}
|
|
22
|
+
/** Input shape accepted by `buildAlternateLinks`. */
|
|
23
|
+
export interface BuildAlternateLinksInput {
|
|
24
|
+
/**
|
|
25
|
+
* BCP 47 locale tag of the default sibling, used for the `x-default`
|
|
26
|
+
* entry. Matched case-insensitively against the normalized
|
|
27
|
+
* `entries[].hreflang` values. If omitted (or no match), the first
|
|
28
|
+
* surviving entry is used as the default.
|
|
29
|
+
*/
|
|
30
|
+
defaultLocale?: string;
|
|
31
|
+
/**
|
|
32
|
+
* Per-locale URL entries. Order is preserved in the output. The
|
|
33
|
+
* caller MUST include the current page itself — Google treats
|
|
34
|
+
* self-referential hreflang as required, not optional.
|
|
35
|
+
*/
|
|
36
|
+
entries: ReadonlyArray<{
|
|
37
|
+
/** BCP 47 tag, e.g. `"en"`, `"fr"`, `"fr-CA"`. */
|
|
38
|
+
hreflang: string;
|
|
39
|
+
/** Absolute URL. Relative or non-http(s) values are dropped. */
|
|
40
|
+
href: string | URL;
|
|
41
|
+
}>;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Build a deduplicated, normalized set of hreflang alternate links,
|
|
45
|
+
* including an `x-default` entry.
|
|
46
|
+
*
|
|
47
|
+
* Behaviour:
|
|
48
|
+
* - Returns `[]` when fewer than 2 entries survive validation (a
|
|
49
|
+
* single-locale page has no meaningful alternates).
|
|
50
|
+
* - Drops entries with malformed / non-http(s) URLs.
|
|
51
|
+
* - Drops entries with empty hreflang.
|
|
52
|
+
* - Drops entries whose input hreflang is the literal `"x-default"` —
|
|
53
|
+
* that's reserved and added automatically.
|
|
54
|
+
* - On duplicate normalized hreflang, the first entry wins.
|
|
55
|
+
* - Preserves input order for per-locale entries.
|
|
56
|
+
* - Appends `x-default` as the final entry, pointing at the
|
|
57
|
+
* `defaultLocale` match (or the first entry as fallback).
|
|
58
|
+
*
|
|
59
|
+
* @param input Entries plus optional default locale hint.
|
|
60
|
+
* @returns Deduped, normalized link tags. Never throws.
|
|
61
|
+
*/
|
|
62
|
+
export declare function buildAlternateLinks(input: BuildAlternateLinksInput): AlternateLink[];
|
|
63
|
+
//# sourceMappingURL=alternates.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"alternates.d.ts","sourceRoot":"","sources":["../src/alternates.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,0FAA0F;AAC1F,MAAM,WAAW,aAAa;IAC1B,4BAA4B;IAC5B,GAAG,EAAE,WAAW,CAAC;IACjB,2EAA2E;IAC3E,QAAQ,EAAE,MAAM,CAAC;IACjB,wCAAwC;IACxC,IAAI,EAAE,MAAM,CAAC;CAChB;AAED,qDAAqD;AACrD,MAAM,WAAW,wBAAwB;IACrC;;;;;OAKG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;IAEvB;;;;OAIG;IACH,OAAO,EAAE,aAAa,CAAC;QACnB,kDAAkD;QAClD,QAAQ,EAAE,MAAM,CAAC;QACjB,gEAAgE;QAChE,IAAI,EAAE,MAAM,GAAG,GAAG,CAAC;KACtB,CAAC,CAAC;CACN;AAkDD;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,wBAAwB,GAAG,aAAa,EAAE,CAkCpF"}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* hreflang alternate-language link builder.
|
|
3
|
+
*
|
|
4
|
+
* Pure function: no DOM, no fetch, no Astro runtime. Safe to import from
|
|
5
|
+
* non-Astro TypeScript (in particular, from `@jdevalk/emdash-plugin-seo`'s
|
|
6
|
+
* EmDash `page:metadata` hook).
|
|
7
|
+
*
|
|
8
|
+
* Callers provide a set of `{ hreflang, href }` entries; this module
|
|
9
|
+
* normalizes BCP 47 tags, validates URLs, dedupes, and appends an
|
|
10
|
+
* `x-default` entry. It does NOT source translation data, construct URLs
|
|
11
|
+
* from slugs, or know about any particular CMS.
|
|
12
|
+
*/
|
|
13
|
+
/** BCP 47 subtag structure we handle: language-[script]-[region]. */
|
|
14
|
+
const BCP47_RE = /^([a-z]{2,3})(?:-([a-z]{4}))?(?:-([a-z]{2,3}))?$/i;
|
|
15
|
+
/**
|
|
16
|
+
* Normalize a BCP 47 locale tag to conventional casing:
|
|
17
|
+
* language subtag lowercase, script subtag title-case, region subtag
|
|
18
|
+
* uppercase. Returns `null` for empty / whitespace-only input.
|
|
19
|
+
*
|
|
20
|
+
* For tags that don't match the language-script-region pattern
|
|
21
|
+
* (variants, extended tags, private-use, grandfathered), this falls
|
|
22
|
+
* back to lowercasing the entire tag. hreflang rarely uses those.
|
|
23
|
+
*/
|
|
24
|
+
function normalizeLocaleTag(raw) {
|
|
25
|
+
const trimmed = raw.trim();
|
|
26
|
+
if (trimmed.length === 0)
|
|
27
|
+
return null;
|
|
28
|
+
const match = BCP47_RE.exec(trimmed);
|
|
29
|
+
if (match === null)
|
|
30
|
+
return trimmed.toLowerCase();
|
|
31
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
32
|
+
const [, lang, script, region] = match;
|
|
33
|
+
let out = lang.toLowerCase();
|
|
34
|
+
if (script !== undefined) {
|
|
35
|
+
out += '-' + script.charAt(0).toUpperCase() + script.slice(1).toLowerCase();
|
|
36
|
+
}
|
|
37
|
+
if (region !== undefined) {
|
|
38
|
+
out += '-' + region.toUpperCase();
|
|
39
|
+
}
|
|
40
|
+
return out;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Validate that a URL-ish value is absolute http(s). Returns the
|
|
44
|
+
* stringified URL on success, `null` on failure.
|
|
45
|
+
*/
|
|
46
|
+
function validateAbsoluteUrl(href) {
|
|
47
|
+
try {
|
|
48
|
+
const str = typeof href === 'string' ? href : href.toString();
|
|
49
|
+
const parsed = new URL(str);
|
|
50
|
+
if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
return parsed.toString();
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Build a deduplicated, normalized set of hreflang alternate links,
|
|
61
|
+
* including an `x-default` entry.
|
|
62
|
+
*
|
|
63
|
+
* Behaviour:
|
|
64
|
+
* - Returns `[]` when fewer than 2 entries survive validation (a
|
|
65
|
+
* single-locale page has no meaningful alternates).
|
|
66
|
+
* - Drops entries with malformed / non-http(s) URLs.
|
|
67
|
+
* - Drops entries with empty hreflang.
|
|
68
|
+
* - Drops entries whose input hreflang is the literal `"x-default"` —
|
|
69
|
+
* that's reserved and added automatically.
|
|
70
|
+
* - On duplicate normalized hreflang, the first entry wins.
|
|
71
|
+
* - Preserves input order for per-locale entries.
|
|
72
|
+
* - Appends `x-default` as the final entry, pointing at the
|
|
73
|
+
* `defaultLocale` match (or the first entry as fallback).
|
|
74
|
+
*
|
|
75
|
+
* @param input Entries plus optional default locale hint.
|
|
76
|
+
* @returns Deduped, normalized link tags. Never throws.
|
|
77
|
+
*/
|
|
78
|
+
export function buildAlternateLinks(input) {
|
|
79
|
+
const seen = new Set();
|
|
80
|
+
const validated = [];
|
|
81
|
+
for (const entry of input.entries) {
|
|
82
|
+
const normalized = normalizeLocaleTag(entry.hreflang);
|
|
83
|
+
if (normalized === null)
|
|
84
|
+
continue;
|
|
85
|
+
if (normalized === 'x-default')
|
|
86
|
+
continue; // reserved
|
|
87
|
+
if (seen.has(normalized))
|
|
88
|
+
continue;
|
|
89
|
+
const href = validateAbsoluteUrl(entry.href);
|
|
90
|
+
if (href === null)
|
|
91
|
+
continue;
|
|
92
|
+
seen.add(normalized);
|
|
93
|
+
validated.push({ rel: 'alternate', hreflang: normalized, href });
|
|
94
|
+
}
|
|
95
|
+
// Short-circuit: one (or zero) surviving entries is not a meaningful
|
|
96
|
+
// hreflang set. Emit nothing rather than a self-referential single
|
|
97
|
+
// tag. Callers who want that behaviour can skip the helper.
|
|
98
|
+
if (validated.length < 2)
|
|
99
|
+
return [];
|
|
100
|
+
// Pick the x-default target. defaultLocale is matched
|
|
101
|
+
// case-insensitively against the normalized tags.
|
|
102
|
+
let defaultEntry = validated[0];
|
|
103
|
+
if (input.defaultLocale !== undefined) {
|
|
104
|
+
const normalizedDefault = normalizeLocaleTag(input.defaultLocale);
|
|
105
|
+
if (normalizedDefault !== null) {
|
|
106
|
+
const match = validated.find((v) => v.hreflang === normalizedDefault);
|
|
107
|
+
if (match !== undefined)
|
|
108
|
+
defaultEntry = match;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return [...validated, { rel: 'alternate', hreflang: 'x-default', href: defaultEntry.href }];
|
|
112
|
+
}
|
|
113
|
+
//# sourceMappingURL=alternates.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"alternates.js","sourceRoot":"","sources":["../src/alternates.ts"],"names":[],"mappings":"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"}
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* here makes it unit-testable with vitest — Astro's renderer isn't
|
|
6
6
|
* easily accessible from vitest.
|
|
7
7
|
*/
|
|
8
|
+
import { type BuildAlternateLinksInput } from '../alternates.js';
|
|
8
9
|
/** Public props surface of the <Seo> component. */
|
|
9
10
|
export interface SeoProps {
|
|
10
11
|
/** Page title. Required. */
|
|
@@ -68,6 +69,28 @@ export interface SeoProps {
|
|
|
68
69
|
extraLinks?: ReadonlyArray<Record<string, string>>;
|
|
69
70
|
/** Extra `<meta>` tags (author, custom fields). */
|
|
70
71
|
extraMeta?: ReadonlyArray<Record<string, string>>;
|
|
72
|
+
/**
|
|
73
|
+
* hreflang alternate-language annotations.
|
|
74
|
+
*
|
|
75
|
+
* Emits one `<link rel="alternate" hreflang="…" href="…">` per
|
|
76
|
+
* entry, plus an `x-default` entry pointing at the default-locale
|
|
77
|
+
* sibling (or the first entry, if no default match is found).
|
|
78
|
+
*
|
|
79
|
+
* All `href` values MUST be absolute `http(s)://` URLs — relative
|
|
80
|
+
* or other-scheme values are dropped silently. BCP 47 tags are
|
|
81
|
+
* normalized on output (`fr-ca` → `fr-CA`, `zh-hant-hk` →
|
|
82
|
+
* `zh-Hant-HK`). On duplicate normalized tags, the first entry
|
|
83
|
+
* wins.
|
|
84
|
+
*
|
|
85
|
+
* **The caller must include the current page itself** in the
|
|
86
|
+
* entries list — Google treats self-referential hreflang as
|
|
87
|
+
* required, not optional.
|
|
88
|
+
*
|
|
89
|
+
* When fewer than 2 entries survive validation, nothing is emitted
|
|
90
|
+
* (a single-locale page has no meaningful alternates). If you
|
|
91
|
+
* prefer strict input checking, validate before passing in.
|
|
92
|
+
*/
|
|
93
|
+
alternates?: BuildAlternateLinksInput;
|
|
71
94
|
}
|
|
72
95
|
/**
|
|
73
96
|
* Shape consumed by `astro-seo`'s `<SEO>` component. We expose it as a
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"seo-props.d.ts","sourceRoot":"","sources":["../../src/components/seo-props.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,mDAAmD;AACnD,MAAM,WAAW,QAAQ;IACrB,4BAA4B;IAC5B,KAAK,EAAE,MAAM,CAAC;IACd;;;;;OAKG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,wBAAwB;IACxB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,0EAA0E;IAC1E,SAAS,CAAC,EAAE,MAAM,GAAG,GAAG,CAAC;IACzB,gDAAgD;IAChD,MAAM,CAAC,EAAE,SAAS,GAAG,SAAS,GAAG,SAAS,GAAG,MAAM,CAAC;IACpD,uCAAuC;IACvC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,oCAAoC;IACpC,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,0CAA0C;IAC1C,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,2CAA2C;IAC3C,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,uDAAuD;IACvD,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,wCAAwC;IACxC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,6BAA6B;IAC7B,OAAO,CAAC,EAAE;QACN,2DAA2D;QAC3D,IAAI,CAAC,EAAE,MAAM,CAAC;QACd,oCAAoC;QACpC,OAAO,CAAC,EAAE,MAAM,CAAC;QACjB,sDAAsD;QACtD,IAAI,CAAC,EAAE,SAAS,GAAG,qBAAqB,GAAG,KAAK,GAAG,QAAQ,CAAC;KAC/D,CAAC;IACF;;;OAGG;IACH,OAAO,CAAC,EAAE;QACN,aAAa,CAAC,EAAE,IAAI,GAAG,MAAM,CAAC;QAC9B,YAAY,CAAC,EAAE,IAAI,GAAG,MAAM,CAAC;QAC7B,cAAc,CAAC,EAAE,IAAI,GAAG,MAAM,CAAC;QAC/B,OAAO,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;QAC5B,IAAI,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;QACzB,OAAO,CAAC,EAAE,MAAM,CAAC;KACpB,CAAC;IACF,yEAAyE;IACzE,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB;;;;;OAKG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;IACvC,iEAAiE;IACjE,UAAU,CAAC,EAAE,aAAa,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;IACnD,mDAAmD;IACnD,SAAS,CAAC,EAAE,aAAa,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;
|
|
1
|
+
{"version":3,"file":"seo-props.d.ts","sourceRoot":"","sources":["../../src/components/seo-props.ts"],"names":[],"mappings":"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"}
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* here makes it unit-testable with vitest — Astro's renderer isn't
|
|
6
6
|
* easily accessible from vitest.
|
|
7
7
|
*/
|
|
8
|
+
import { buildAlternateLinks } from '../alternates.js';
|
|
8
9
|
function toIsoString(value) {
|
|
9
10
|
if (value === undefined)
|
|
10
11
|
return undefined;
|
|
@@ -99,13 +100,31 @@ export function buildAstroSeoProps(props, pageUrl) {
|
|
|
99
100
|
twitter.imageAlt = props.ogImageAlt;
|
|
100
101
|
astroSeo.twitter = twitter;
|
|
101
102
|
}
|
|
102
|
-
|
|
103
|
-
|
|
103
|
+
// Resolve hreflang alternates (if any). Returns [] when fewer than
|
|
104
|
+
// 2 entries survive validation, so this is cheap to call.
|
|
105
|
+
const alternateLinks = props.alternates !== undefined ? buildAlternateLinks(props.alternates) : [];
|
|
106
|
+
const hasExtraLinks = props.extraLinks !== undefined && props.extraLinks.length > 0;
|
|
107
|
+
const hasExtraMeta = props.extraMeta !== undefined && props.extraMeta.length > 0;
|
|
108
|
+
const hasAlternates = alternateLinks.length > 0;
|
|
109
|
+
if (hasExtraLinks || hasExtraMeta || hasAlternates) {
|
|
104
110
|
const extend = {};
|
|
105
|
-
|
|
106
|
-
|
|
111
|
+
const link = [];
|
|
112
|
+
if (hasExtraLinks) {
|
|
113
|
+
for (const entry of props.extraLinks)
|
|
114
|
+
link.push({ ...entry });
|
|
107
115
|
}
|
|
108
|
-
if (
|
|
116
|
+
if (hasAlternates) {
|
|
117
|
+
for (const entry of alternateLinks) {
|
|
118
|
+
link.push({
|
|
119
|
+
rel: entry.rel,
|
|
120
|
+
href: entry.href,
|
|
121
|
+
hreflang: entry.hreflang,
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
if (link.length > 0)
|
|
126
|
+
extend.link = link;
|
|
127
|
+
if (hasExtraMeta) {
|
|
109
128
|
extend.meta = props.extraMeta.map((meta) => ({ ...meta }));
|
|
110
129
|
}
|
|
111
130
|
astroSeo.extend = extend;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"seo-props.js","sourceRoot":"","sources":["../../src/components/seo-props.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;
|
|
1
|
+
{"version":3,"file":"seo-props.js","sourceRoot":"","sources":["../../src/components/seo-props.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
CHANGED
|
@@ -5,4 +5,6 @@ export type { SchemaEndpointOptions, SchemaMapEntry, SchemaMapOptions } from './
|
|
|
5
5
|
export { seoSchema, imageSchema } from './content-helpers.js';
|
|
6
6
|
export { buildAstroSeoProps } from './components/seo-props.js';
|
|
7
7
|
export type { SeoProps, AstroSeoProps } from './components/seo-props.js';
|
|
8
|
+
export { buildAlternateLinks } from './alternates.js';
|
|
9
|
+
export type { AlternateLink, BuildAlternateLinksInput } from './alternates.js';
|
|
8
10
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAMA,OAAO,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAC5C,YAAY,EAAE,iBAAiB,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AAE1E,OAAO,EAAE,oBAAoB,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AACpE,YAAY,EAAE,qBAAqB,EAAE,cAAc,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAE3F,OAAO,EAAE,SAAS,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AAE9D,OAAO,EAAE,kBAAkB,EAAE,MAAM,2BAA2B,CAAC;AAC/D,YAAY,EAAE,QAAQ,EAAE,aAAa,EAAE,MAAM,2BAA2B,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAMA,OAAO,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAC5C,YAAY,EAAE,iBAAiB,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AAE1E,OAAO,EAAE,oBAAoB,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AACpE,YAAY,EAAE,qBAAqB,EAAE,cAAc,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAE3F,OAAO,EAAE,SAAS,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AAE9D,OAAO,EAAE,kBAAkB,EAAE,MAAM,2BAA2B,CAAC;AAC/D,YAAY,EAAE,QAAQ,EAAE,aAAa,EAAE,MAAM,2BAA2B,CAAC;AAEzE,OAAO,EAAE,mBAAmB,EAAE,MAAM,iBAAiB,CAAC;AACtD,YAAY,EAAE,aAAa,EAAE,wBAAwB,EAAE,MAAM,iBAAiB,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -7,4 +7,5 @@ export { aggregate } from './aggregator.js';
|
|
|
7
7
|
export { createSchemaEndpoint, createSchemaMap } from './routes.js';
|
|
8
8
|
export { seoSchema, imageSchema } from './content-helpers.js';
|
|
9
9
|
export { buildAstroSeoProps } from './components/seo-props.js';
|
|
10
|
+
export { buildAlternateLinks } from './alternates.js';
|
|
10
11
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,+EAA+E;AAC/E,EAAE;AACF,wEAAwE;AACxE,8EAA8E;AAC9E,sDAAsD;AAEtD,OAAO,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAG5C,OAAO,EAAE,oBAAoB,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAGpE,OAAO,EAAE,SAAS,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AAE9D,OAAO,EAAE,kBAAkB,EAAE,MAAM,2BAA2B,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,+EAA+E;AAC/E,EAAE;AACF,wEAAwE;AACxE,8EAA8E;AAC9E,sDAAsD;AAEtD,OAAO,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAG5C,OAAO,EAAE,oBAAoB,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAGpE,OAAO,EAAE,SAAS,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AAE9D,OAAO,EAAE,kBAAkB,EAAE,MAAM,2BAA2B,CAAC;AAG/D,OAAO,EAAE,mBAAmB,EAAE,MAAM,iBAAiB,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,62 +1,62 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
2
|
+
"name": "@jdevalk/astro-seo-graph",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Astro integration for @jdevalk/seo-graph-core. Seo component, route factories, content-collection aggregator, Zod content helpers.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"astro",
|
|
7
|
+
"astro-integration",
|
|
8
|
+
"astro-component",
|
|
9
|
+
"schema.org",
|
|
10
|
+
"json-ld",
|
|
11
|
+
"seo",
|
|
12
|
+
"agent-ready",
|
|
13
|
+
"structured-data"
|
|
14
|
+
],
|
|
15
|
+
"author": "Joost de Valk <joost@joost.blog>",
|
|
16
|
+
"license": "MIT",
|
|
17
|
+
"type": "module",
|
|
18
|
+
"main": "./dist/index.js",
|
|
19
|
+
"types": "./dist/index.d.ts",
|
|
20
|
+
"exports": {
|
|
21
|
+
".": {
|
|
22
|
+
"types": "./dist/index.d.ts",
|
|
23
|
+
"import": "./dist/index.js"
|
|
24
|
+
},
|
|
25
|
+
"./Seo.astro": "./dist/components/Seo.astro"
|
|
24
26
|
},
|
|
25
|
-
"
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
"
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
"
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
"
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
"
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
"
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
}
|
|
62
|
-
}
|
|
27
|
+
"files": [
|
|
28
|
+
"dist",
|
|
29
|
+
"README.md",
|
|
30
|
+
"LICENSE"
|
|
31
|
+
],
|
|
32
|
+
"repository": {
|
|
33
|
+
"type": "git",
|
|
34
|
+
"url": "git+https://github.com/jdevalk/seo-graph.git",
|
|
35
|
+
"directory": "packages/astro-seo-graph"
|
|
36
|
+
},
|
|
37
|
+
"bugs": "https://github.com/jdevalk/seo-graph/issues",
|
|
38
|
+
"homepage": "https://github.com/jdevalk/seo-graph/tree/main/packages/astro-seo-graph#readme",
|
|
39
|
+
"publishConfig": {
|
|
40
|
+
"access": "public"
|
|
41
|
+
},
|
|
42
|
+
"scripts": {
|
|
43
|
+
"build": "tsc -p tsconfig.build.json && mkdir -p dist/components && cp src/components/*.astro dist/components/",
|
|
44
|
+
"typecheck": "tsc -p tsconfig.json",
|
|
45
|
+
"test": "vitest run"
|
|
46
|
+
},
|
|
47
|
+
"peerDependencies": {
|
|
48
|
+
"astro": "^5.0.0 || ^6.0.0"
|
|
49
|
+
},
|
|
50
|
+
"dependencies": {
|
|
51
|
+
"@jdevalk/seo-graph-core": "workspace:*",
|
|
52
|
+
"astro-seo": "^1.1.0",
|
|
53
|
+
"schema-dts": "^2.0.0",
|
|
54
|
+
"zod": "^3.24.0"
|
|
55
|
+
},
|
|
56
|
+
"devDependencies": {
|
|
57
|
+
"@types/node": "^22.0.0",
|
|
58
|
+
"astro": "^6.0.5",
|
|
59
|
+
"typescript": "^5.6.0",
|
|
60
|
+
"vitest": "^2.0.0"
|
|
61
|
+
}
|
|
62
|
+
}
|
package/LICENSE
DELETED
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
MIT License
|
|
2
|
-
|
|
3
|
-
Copyright (c) 2026 Joost de Valk
|
|
4
|
-
|
|
5
|
-
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
-
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
-
in the Software without restriction, including without limitation the rights
|
|
8
|
-
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
-
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
-
furnished to do so, subject to the following conditions:
|
|
11
|
-
|
|
12
|
-
The above copyright notice and this permission notice shall be included in all
|
|
13
|
-
copies or substantial portions of the Software.
|
|
14
|
-
|
|
15
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
-
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
-
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
-
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
-
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
-
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
-
SOFTWARE.
|