@jdevalk/astro-seo-graph 0.5.1 → 0.6.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 +32 -1
- package/dist/components/FuzzyRedirect.astro +12 -0
- package/dist/components/seo-props.d.ts +48 -21
- package/dist/components/seo-props.d.ts.map +1 -1
- package/dist/components/seo-props.js +151 -49
- package/dist/components/seo-props.js.map +1 -1
- package/dist/content-helpers.d.ts +13 -8
- package/dist/content-helpers.d.ts.map +1 -1
- package/dist/content-helpers.js +6 -1
- package/dist/content-helpers.js.map +1 -1
- package/dist/integration.d.ts +48 -0
- package/dist/integration.d.ts.map +1 -0
- package/dist/integration.js +78 -0
- package/dist/integration.js.map +1 -0
- package/dist/routes.d.ts +0 -4
- package/dist/routes.d.ts.map +1 -1
- package/dist/routes.js +8 -6
- package/dist/routes.js.map +1 -1
- package/package.json +6 -2
package/README.md
CHANGED
|
@@ -292,18 +292,49 @@ export const GET = createSchemaMap({
|
|
|
292
292
|
```ts
|
|
293
293
|
// src/content.config.ts
|
|
294
294
|
import { defineCollection, z } from 'astro:content';
|
|
295
|
-
import { seoSchema } from '@jdevalk/astro-seo-graph';
|
|
295
|
+
import { seoSchema, imageSchema } from '@jdevalk/astro-seo-graph';
|
|
296
296
|
|
|
297
297
|
const blog = defineCollection({
|
|
298
298
|
schema: ({ image }) =>
|
|
299
299
|
z.object({
|
|
300
300
|
title: z.string(),
|
|
301
301
|
publishDate: z.coerce.date(),
|
|
302
|
+
featureImage: imageSchema(image).optional(),
|
|
302
303
|
seo: seoSchema(image).optional(),
|
|
303
304
|
}),
|
|
304
305
|
});
|
|
305
306
|
```
|
|
306
307
|
|
|
308
|
+
`imageSchema` **requires** `alt` — missing alt text is an accessibility
|
|
309
|
+
failure and an SEO failure. Decorative images should use `alt: ''`
|
|
310
|
+
explicitly. If you want the whole image to be optional, wrap the schema:
|
|
311
|
+
`imageSchema(image).optional()`.
|
|
312
|
+
|
|
313
|
+
## Astro integration
|
|
314
|
+
|
|
315
|
+
An Astro integration that runs build-time SEO checks. Currently:
|
|
316
|
+
|
|
317
|
+
- Warns about built pages with zero or more than one `<h1>` element (a
|
|
318
|
+
common SEO and accessibility issue).
|
|
319
|
+
|
|
320
|
+
```js
|
|
321
|
+
// astro.config.mjs
|
|
322
|
+
import { defineConfig } from 'astro/config';
|
|
323
|
+
import seoGraph from '@jdevalk/astro-seo-graph/integration';
|
|
324
|
+
|
|
325
|
+
export default defineConfig({
|
|
326
|
+
integrations: [seoGraph()],
|
|
327
|
+
});
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
Options:
|
|
331
|
+
|
|
332
|
+
| Prop | Default | Description |
|
|
333
|
+
| ------------ | ------- | ------------------------------------------- |
|
|
334
|
+
| `validateH1` | `true` | Warn about pages without exactly one `<h1>` |
|
|
335
|
+
|
|
336
|
+
Only static pages are checked (SSR pages aren't on disk at build time).
|
|
337
|
+
|
|
307
338
|
## License
|
|
308
339
|
|
|
309
340
|
MIT © Joost de Valk
|
|
@@ -102,6 +102,18 @@ const {
|
|
|
102
102
|
const suggestionText = container.dataset.suggestionText ?? 'Did you mean';
|
|
103
103
|
|
|
104
104
|
const currentPath = window.location.pathname;
|
|
105
|
+
|
|
106
|
+
// Out-of-bounds pagination: /something/page/N/ → /something/
|
|
107
|
+
const paginationMatch = currentPath.match(/^(.+?)\/page\/\d+\/?$/);
|
|
108
|
+
if (paginationMatch) {
|
|
109
|
+
const basePath = paginationMatch[1]!.replace(/\/$/, '') + '/';
|
|
110
|
+
console.log(
|
|
111
|
+
`[FuzzyRedirect] Pagination redirect: "${currentPath}" → "${basePath}"`,
|
|
112
|
+
);
|
|
113
|
+
window.location.replace(basePath);
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
|
|
105
117
|
const urls = await fetchSitemapUrls(sitemapUrl);
|
|
106
118
|
|
|
107
119
|
// Extract paths from full URLs.
|
|
@@ -19,8 +19,20 @@ export interface SeoProps {
|
|
|
19
19
|
titleTemplate?: string;
|
|
20
20
|
/** Meta description. */
|
|
21
21
|
description?: string;
|
|
22
|
-
/**
|
|
22
|
+
/**
|
|
23
|
+
* Canonical URL. Defaults to the current page URL with the query
|
|
24
|
+
* string stripped. Set `preserveQueryParams` to keep the query
|
|
25
|
+
* string, or pass an explicit `canonical` to override entirely.
|
|
26
|
+
*
|
|
27
|
+
* Omitted from the output when `noindex` is true.
|
|
28
|
+
*/
|
|
23
29
|
canonical?: string | URL;
|
|
30
|
+
/**
|
|
31
|
+
* Keep the query string in the default canonical URL. Defaults to
|
|
32
|
+
* `false` (query params are stripped — the SEO-correct behavior for
|
|
33
|
+
* most cases). Has no effect when `canonical` is explicitly set.
|
|
34
|
+
*/
|
|
35
|
+
preserveQueryParams?: boolean;
|
|
24
36
|
/** Open Graph type. Defaults to `'website'`. */
|
|
25
37
|
ogType?: 'website' | 'article' | 'profile' | 'book';
|
|
26
38
|
/** Absolute URL of the share image. */
|
|
@@ -35,7 +47,14 @@ export interface SeoProps {
|
|
|
35
47
|
siteName?: string;
|
|
36
48
|
/** OG locale. Defaults to `'en_US'`. */
|
|
37
49
|
locale?: string;
|
|
38
|
-
/**
|
|
50
|
+
/**
|
|
51
|
+
* Twitter card metadata. Only Twitter-specific tags (card, site,
|
|
52
|
+
* creator) are emitted by default — twitter:title, :description,
|
|
53
|
+
* :image, and :image:alt fall back to their `og:` counterparts
|
|
54
|
+
* automatically. Set `title`, `description`, `image`, or `imageAlt`
|
|
55
|
+
* here only when you want Twitter-specific content that differs
|
|
56
|
+
* from the OG values.
|
|
57
|
+
*/
|
|
39
58
|
twitter?: {
|
|
40
59
|
/** Twitter handle of the site owner, e.g. `'@jdevalk'`. */
|
|
41
60
|
site?: string;
|
|
@@ -43,6 +62,14 @@ export interface SeoProps {
|
|
|
43
62
|
creator?: string;
|
|
44
63
|
/** Card type. Defaults to `'summary_large_image'`. */
|
|
45
64
|
card?: 'summary' | 'summary_large_image' | 'app' | 'player';
|
|
65
|
+
/** Override twitter:title. Omit to fall back to og:title. */
|
|
66
|
+
title?: string;
|
|
67
|
+
/** Override twitter:description. Omit to fall back to og:description. */
|
|
68
|
+
description?: string;
|
|
69
|
+
/** Override twitter:image. Omit to fall back to og:image. */
|
|
70
|
+
image?: string;
|
|
71
|
+
/** Override twitter:image:alt. Omit to fall back to og:image:alt. */
|
|
72
|
+
imageAlt?: string;
|
|
46
73
|
};
|
|
47
74
|
/**
|
|
48
75
|
* Article-specific OG metadata. Only emitted when `ogType` is
|
|
@@ -56,8 +83,20 @@ export interface SeoProps {
|
|
|
56
83
|
tags?: readonly string[];
|
|
57
84
|
section?: string;
|
|
58
85
|
};
|
|
59
|
-
/** Emit `<meta name="robots" content="noindex, follow"
|
|
86
|
+
/** Emit `<meta name="robots" content="noindex, follow, max-*">`. */
|
|
60
87
|
noindex?: boolean;
|
|
88
|
+
/** Emit `nofollow` in the robots meta. */
|
|
89
|
+
nofollow?: boolean;
|
|
90
|
+
/**
|
|
91
|
+
* Facebook page URL of the publisher. Emitted as `article:publisher`
|
|
92
|
+
* when `ogType` is `'article'`.
|
|
93
|
+
*/
|
|
94
|
+
articlePublisher?: string;
|
|
95
|
+
/**
|
|
96
|
+
* Author name for the `<meta name="author">` tag. If omitted but
|
|
97
|
+
* `article.authors` is set, the first author is used.
|
|
98
|
+
*/
|
|
99
|
+
author?: string;
|
|
61
100
|
/**
|
|
62
101
|
* JSON-LD `@graph` envelope to inject as
|
|
63
102
|
* `<script type="application/ld+json">`. Typically the output of
|
|
@@ -74,21 +113,8 @@ export interface SeoProps {
|
|
|
74
113
|
*
|
|
75
114
|
* Emits one `<link rel="alternate" hreflang="…" href="…">` per
|
|
76
115
|
* 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.
|
|
116
|
+
* sibling (or the first entry, if no default match is found). Also
|
|
117
|
+
* emits matching `og:locale:alternate` tags.
|
|
92
118
|
*/
|
|
93
119
|
alternates?: BuildAlternateLinksInput;
|
|
94
120
|
}
|
|
@@ -100,8 +126,7 @@ export interface SeoProps {
|
|
|
100
126
|
export interface AstroSeoProps {
|
|
101
127
|
title: string;
|
|
102
128
|
description?: string;
|
|
103
|
-
canonical
|
|
104
|
-
noindex?: boolean;
|
|
129
|
+
canonical?: string;
|
|
105
130
|
openGraph: {
|
|
106
131
|
basic: {
|
|
107
132
|
title: string;
|
|
@@ -113,6 +138,7 @@ export interface AstroSeoProps {
|
|
|
113
138
|
description?: string;
|
|
114
139
|
siteName?: string;
|
|
115
140
|
locale?: string;
|
|
141
|
+
localeAlternate?: string[];
|
|
116
142
|
};
|
|
117
143
|
image?: {
|
|
118
144
|
alt?: string;
|
|
@@ -126,13 +152,14 @@ export interface AstroSeoProps {
|
|
|
126
152
|
authors?: string[];
|
|
127
153
|
tags?: string[];
|
|
128
154
|
section?: string;
|
|
155
|
+
publisher?: string;
|
|
129
156
|
};
|
|
130
157
|
};
|
|
131
158
|
twitter?: {
|
|
132
159
|
card: 'summary' | 'summary_large_image' | 'app' | 'player';
|
|
133
160
|
site?: string;
|
|
134
161
|
creator?: string;
|
|
135
|
-
title
|
|
162
|
+
title?: string;
|
|
136
163
|
description?: string;
|
|
137
164
|
image?: string;
|
|
138
165
|
imageAlt?: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"seo-props.d.ts","sourceRoot":"","sources":["../../src/components/seo-props.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAuB,KAAK,wBAAwB,EAAE,MAAM,kBAAkB,CAAC;AAEtF,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
|
|
1
|
+
{"version":3,"file":"seo-props.d.ts","sourceRoot":"","sources":["../../src/components/seo-props.ts"],"names":[],"mappings":"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"}
|
|
@@ -11,6 +11,40 @@ function toIsoString(value) {
|
|
|
11
11
|
return undefined;
|
|
12
12
|
return value instanceof Date ? value.toISOString() : value;
|
|
13
13
|
}
|
|
14
|
+
/**
|
|
15
|
+
* Build the `content` value of the `<meta name="robots">` tag. Always
|
|
16
|
+
* includes `max-snippet:-1, max-image-preview:large, max-video-preview:-1`
|
|
17
|
+
* (opt into maximum snippet/preview sizes in search results). Prepends
|
|
18
|
+
* `noindex`, `nofollow`, or `noindex, follow` as applicable.
|
|
19
|
+
*/
|
|
20
|
+
function buildRobotsContent(noindex, nofollow) {
|
|
21
|
+
const directives = [];
|
|
22
|
+
if (noindex && nofollow) {
|
|
23
|
+
directives.push('noindex', 'nofollow');
|
|
24
|
+
}
|
|
25
|
+
else if (noindex) {
|
|
26
|
+
directives.push('noindex', 'follow');
|
|
27
|
+
}
|
|
28
|
+
else if (nofollow) {
|
|
29
|
+
directives.push('nofollow');
|
|
30
|
+
}
|
|
31
|
+
directives.push('max-snippet:-1', 'max-image-preview:large', 'max-video-preview:-1');
|
|
32
|
+
return directives.join(', ');
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Strip the query string from a URL. Fragments are preserved. Falls
|
|
36
|
+
* back to returning the input unchanged if URL parsing fails.
|
|
37
|
+
*/
|
|
38
|
+
function stripQueryParams(url) {
|
|
39
|
+
try {
|
|
40
|
+
const parsed = new URL(url);
|
|
41
|
+
parsed.search = '';
|
|
42
|
+
return parsed.toString();
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
return url;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
14
48
|
/**
|
|
15
49
|
* Project the public `SeoProps` onto the shape `astro-seo`'s `<SEO>`
|
|
16
50
|
* component expects. Pure function; no Astro runtime access.
|
|
@@ -24,7 +58,26 @@ export function buildAstroSeoProps(props, pageUrl) {
|
|
|
24
58
|
const fullTitle = props.titleTemplate
|
|
25
59
|
? props.titleTemplate.replace('%s', props.title)
|
|
26
60
|
: props.title;
|
|
27
|
-
|
|
61
|
+
// Default canonical strips query params (SEO-correct for most
|
|
62
|
+
// cases). `preserveQueryParams` opts out. Explicit `canonical`
|
|
63
|
+
// overrides entirely.
|
|
64
|
+
let canonical;
|
|
65
|
+
if (props.canonical !== undefined) {
|
|
66
|
+
canonical = props.canonical.toString();
|
|
67
|
+
}
|
|
68
|
+
else if (props.preserveQueryParams) {
|
|
69
|
+
canonical = pageUrl;
|
|
70
|
+
}
|
|
71
|
+
else {
|
|
72
|
+
canonical = stripQueryParams(pageUrl);
|
|
73
|
+
}
|
|
74
|
+
// Omit canonical when the page is noindex (Google recommendation).
|
|
75
|
+
if (props.noindex) {
|
|
76
|
+
canonical = undefined;
|
|
77
|
+
}
|
|
78
|
+
// og:url uses the canonical when available, otherwise the (possibly
|
|
79
|
+
// query-stripped) page URL.
|
|
80
|
+
const ogUrl = canonical ?? stripQueryParams(pageUrl);
|
|
28
81
|
const ogType = props.ogType ?? 'website';
|
|
29
82
|
const ogImage = props.ogImage ?? '';
|
|
30
83
|
const openGraph = {
|
|
@@ -32,7 +85,7 @@ export function buildAstroSeoProps(props, pageUrl) {
|
|
|
32
85
|
title: fullTitle,
|
|
33
86
|
type: ogType,
|
|
34
87
|
image: ogImage,
|
|
35
|
-
url:
|
|
88
|
+
url: ogUrl,
|
|
36
89
|
},
|
|
37
90
|
};
|
|
38
91
|
const optional = {};
|
|
@@ -41,6 +94,30 @@ export function buildAstroSeoProps(props, pageUrl) {
|
|
|
41
94
|
if (props.siteName !== undefined)
|
|
42
95
|
optional.siteName = props.siteName;
|
|
43
96
|
optional.locale = props.locale ?? 'en_US';
|
|
97
|
+
// og:locale:alternate from hreflang entries. Convert hyphenated
|
|
98
|
+
// BCP 47 (fr-CA) to OG-style underscores (fr_CA), and skip any
|
|
99
|
+
// entry that matches the primary og:locale (including language-only
|
|
100
|
+
// matches like hreflang='en' when og:locale='en_US').
|
|
101
|
+
if (props.alternates?.entries && props.alternates.entries.length > 1) {
|
|
102
|
+
const primaryLang = optional.locale?.split('_')[0]?.toLowerCase();
|
|
103
|
+
const localeAlternate = [];
|
|
104
|
+
const seen = new Set();
|
|
105
|
+
for (const entry of props.alternates.entries) {
|
|
106
|
+
const ogLocale = entry.hreflang.replace('-', '_');
|
|
107
|
+
const entryLang = entry.hreflang.split('-')[0]?.toLowerCase();
|
|
108
|
+
if (seen.has(ogLocale))
|
|
109
|
+
continue;
|
|
110
|
+
if (ogLocale === optional.locale)
|
|
111
|
+
continue;
|
|
112
|
+
// Skip language-only hreflang when it matches the primary locale's language.
|
|
113
|
+
if (!entry.hreflang.includes('-') && entryLang === primaryLang)
|
|
114
|
+
continue;
|
|
115
|
+
seen.add(ogLocale);
|
|
116
|
+
localeAlternate.push(ogLocale);
|
|
117
|
+
}
|
|
118
|
+
if (localeAlternate.length > 0)
|
|
119
|
+
optional.localeAlternate = localeAlternate;
|
|
120
|
+
}
|
|
44
121
|
openGraph.optional = optional;
|
|
45
122
|
const hasImageMeta = props.ogImageAlt !== undefined ||
|
|
46
123
|
props.ogImageWidth !== undefined ||
|
|
@@ -55,78 +132,103 @@ export function buildAstroSeoProps(props, pageUrl) {
|
|
|
55
132
|
image.height = props.ogImageHeight;
|
|
56
133
|
openGraph.image = image;
|
|
57
134
|
}
|
|
58
|
-
if (ogType === 'article' && props.article) {
|
|
135
|
+
if (ogType === 'article' && (props.article || props.articlePublisher)) {
|
|
59
136
|
const article = {};
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
137
|
+
if (props.article) {
|
|
138
|
+
const published = toIsoString(props.article.publishedTime);
|
|
139
|
+
if (published !== undefined)
|
|
140
|
+
article.publishedTime = published;
|
|
141
|
+
const modified = toIsoString(props.article.modifiedTime);
|
|
142
|
+
if (modified !== undefined)
|
|
143
|
+
article.modifiedTime = modified;
|
|
144
|
+
const expiration = toIsoString(props.article.expirationTime);
|
|
145
|
+
if (expiration !== undefined)
|
|
146
|
+
article.expirationTime = expiration;
|
|
147
|
+
if (props.article.authors !== undefined)
|
|
148
|
+
article.authors = [...props.article.authors];
|
|
149
|
+
if (props.article.tags !== undefined)
|
|
150
|
+
article.tags = [...props.article.tags];
|
|
151
|
+
if (props.article.section !== undefined)
|
|
152
|
+
article.section = props.article.section;
|
|
153
|
+
}
|
|
154
|
+
if (props.articlePublisher !== undefined)
|
|
155
|
+
article.publisher = props.articlePublisher;
|
|
75
156
|
openGraph.article = article;
|
|
76
157
|
}
|
|
77
158
|
const astroSeo = {
|
|
78
159
|
title: fullTitle,
|
|
79
|
-
canonical,
|
|
80
160
|
openGraph,
|
|
81
161
|
};
|
|
162
|
+
if (canonical !== undefined)
|
|
163
|
+
astroSeo.canonical = canonical;
|
|
82
164
|
if (props.description !== undefined)
|
|
83
165
|
astroSeo.description = props.description;
|
|
84
|
-
if (props.noindex)
|
|
85
|
-
astroSeo.noindex = true;
|
|
86
166
|
if (props.twitter !== undefined) {
|
|
87
167
|
const twitter = {
|
|
88
168
|
card: props.twitter.card ?? 'summary_large_image',
|
|
89
|
-
title: fullTitle,
|
|
90
169
|
};
|
|
91
170
|
if (props.twitter.site !== undefined)
|
|
92
171
|
twitter.site = props.twitter.site;
|
|
93
172
|
if (props.twitter.creator !== undefined)
|
|
94
173
|
twitter.creator = props.twitter.creator;
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
if (props.
|
|
100
|
-
twitter.
|
|
174
|
+
// Only emit twitter:title/description/image/imageAlt when the
|
|
175
|
+
// caller explicitly provided Twitter-specific overrides.
|
|
176
|
+
// Otherwise Twitter falls back to the og: counterparts
|
|
177
|
+
// automatically — emitting duplicates is noisy.
|
|
178
|
+
if (props.twitter.title !== undefined && props.twitter.title !== fullTitle) {
|
|
179
|
+
twitter.title = props.twitter.title;
|
|
180
|
+
}
|
|
181
|
+
if (props.twitter.description !== undefined &&
|
|
182
|
+
props.twitter.description !== props.description) {
|
|
183
|
+
twitter.description = props.twitter.description;
|
|
184
|
+
}
|
|
185
|
+
if (props.twitter.image !== undefined && props.twitter.image !== ogImage) {
|
|
186
|
+
twitter.image = props.twitter.image;
|
|
187
|
+
}
|
|
188
|
+
if (props.twitter.imageAlt !== undefined && props.twitter.imageAlt !== props.ogImageAlt) {
|
|
189
|
+
twitter.imageAlt = props.twitter.imageAlt;
|
|
190
|
+
}
|
|
101
191
|
astroSeo.twitter = twitter;
|
|
102
192
|
}
|
|
103
193
|
// Resolve hreflang alternates (if any). Returns [] when fewer than
|
|
104
194
|
// 2 entries survive validation, so this is cheap to call.
|
|
105
195
|
const alternateLinks = props.alternates !== undefined ? buildAlternateLinks(props.alternates) : [];
|
|
196
|
+
// Build our own robots meta (astro-seo's doesn't support max-*).
|
|
197
|
+
const robotsContent = buildRobotsContent(props.noindex, props.nofollow);
|
|
198
|
+
// Resolve author name: explicit prop takes precedence, falls back
|
|
199
|
+
// to first entry in article.authors.
|
|
200
|
+
const authorName = props.author ?? props.article?.authors?.[0];
|
|
201
|
+
const meta = [];
|
|
202
|
+
meta.push({ name: 'robots', content: robotsContent });
|
|
203
|
+
if (authorName !== undefined) {
|
|
204
|
+
meta.push({ name: 'author', content: authorName });
|
|
205
|
+
}
|
|
206
|
+
if (props.extraMeta !== undefined) {
|
|
207
|
+
for (const entry of props.extraMeta)
|
|
208
|
+
meta.push({ ...entry });
|
|
209
|
+
}
|
|
106
210
|
const hasExtraLinks = props.extraLinks !== undefined && props.extraLinks.length > 0;
|
|
107
|
-
const hasExtraMeta = props.extraMeta !== undefined && props.extraMeta.length > 0;
|
|
108
211
|
const hasAlternates = alternateLinks.length > 0;
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
const
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
hreflang: entry.hreflang,
|
|
122
|
-
});
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
if (link.length > 0)
|
|
126
|
-
extend.link = link;
|
|
127
|
-
if (hasExtraMeta) {
|
|
128
|
-
extend.meta = props.extraMeta.map((meta) => ({ ...meta }));
|
|
212
|
+
const link = [];
|
|
213
|
+
if (hasExtraLinks) {
|
|
214
|
+
for (const entry of props.extraLinks)
|
|
215
|
+
link.push({ ...entry });
|
|
216
|
+
}
|
|
217
|
+
if (hasAlternates) {
|
|
218
|
+
for (const entry of alternateLinks) {
|
|
219
|
+
link.push({
|
|
220
|
+
rel: entry.rel,
|
|
221
|
+
href: entry.href,
|
|
222
|
+
hreflang: entry.hreflang,
|
|
223
|
+
});
|
|
129
224
|
}
|
|
225
|
+
}
|
|
226
|
+
const extend = {};
|
|
227
|
+
if (link.length > 0)
|
|
228
|
+
extend.link = link;
|
|
229
|
+
if (meta.length > 0)
|
|
230
|
+
extend.meta = meta;
|
|
231
|
+
if (extend.link !== undefined || extend.meta !== undefined) {
|
|
130
232
|
astroSeo.extend = extend;
|
|
131
233
|
}
|
|
132
234
|
return astroSeo;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"seo-props.js","sourceRoot":"","sources":["../../src/components/seo-props.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAE,mBAAmB,EAAiC,MAAM,kBAAkB,CAAC;
|
|
1
|
+
{"version":3,"file":"seo-props.js","sourceRoot":"","sources":["../../src/components/seo-props.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAE,mBAAmB,EAAiC,MAAM,kBAAkB,CAAC;AAuKtF,SAAS,WAAW,CAAC,KAAgC;IACjD,IAAI,KAAK,KAAK,SAAS;QAAE,OAAO,SAAS,CAAC;IAC1C,OAAO,KAAK,YAAY,IAAI,CAAC,CAAC,CAAC,KAAK,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC;AAC/D,CAAC;AAED;;;;;GAKG;AACH,SAAS,kBAAkB,CAAC,OAAiB,EAAE,QAAkB;IAC7D,MAAM,UAAU,GAAa,EAAE,CAAC;IAChC,IAAI,OAAO,IAAI,QAAQ,EAAE,CAAC;QACtB,UAAU,CAAC,IAAI,CAAC,SAAS,EAAE,UAAU,CAAC,CAAC;IAC3C,CAAC;SAAM,IAAI,OAAO,EAAE,CAAC;QACjB,UAAU,CAAC,IAAI,CAAC,SAAS,EAAE,QAAQ,CAAC,CAAC;IACzC,CAAC;SAAM,IAAI,QAAQ,EAAE,CAAC;QAClB,UAAU,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;IAChC,CAAC;IACD,UAAU,CAAC,IAAI,CAAC,gBAAgB,EAAE,yBAAyB,EAAE,sBAAsB,CAAC,CAAC;IACrF,OAAO,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AACjC,CAAC;AAED;;;GAGG;AACH,SAAS,gBAAgB,CAAC,GAAW;IACjC,IAAI,CAAC;QACD,MAAM,MAAM,GAAG,IAAI,GAAG,CAAC,GAAG,CAAC,CAAC;QAC5B,MAAM,CAAC,MAAM,GAAG,EAAE,CAAC;QACnB,OAAO,MAAM,CAAC,QAAQ,EAAE,CAAC;IAC7B,CAAC;IAAC,MAAM,CAAC;QACL,OAAO,GAAG,CAAC;IACf,CAAC;AACL,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,UAAU,kBAAkB,CAAC,KAAe,EAAE,OAAe;IAC/D,MAAM,SAAS,GAAG,KAAK,CAAC,aAAa;QACjC,CAAC,CAAC,KAAK,CAAC,aAAa,CAAC,OAAO,CAAC,IAAI,EAAE,KAAK,CAAC,KAAK,CAAC;QAChD,CAAC,CAAC,KAAK,CAAC,KAAK,CAAC;IAElB,8DAA8D;IAC9D,+DAA+D;IAC/D,sBAAsB;IACtB,IAAI,SAA6B,CAAC;IAClC,IAAI,KAAK,CAAC,SAAS,KAAK,SAAS,EAAE,CAAC;QAChC,SAAS,GAAG,KAAK,CAAC,SAAS,CAAC,QAAQ,EAAE,CAAC;IAC3C,CAAC;SAAM,IAAI,KAAK,CAAC,mBAAmB,EAAE,CAAC;QACnC,SAAS,GAAG,OAAO,CAAC;IACxB,CAAC;SAAM,CAAC;QACJ,SAAS,GAAG,gBAAgB,CAAC,OAAO,CAAC,CAAC;IAC1C,CAAC;IAED,mEAAmE;IACnE,IAAI,KAAK,CAAC,OAAO,EAAE,CAAC;QAChB,SAAS,GAAG,SAAS,CAAC;IAC1B,CAAC;IAED,oEAAoE;IACpE,4BAA4B;IAC5B,MAAM,KAAK,GAAG,SAAS,IAAI,gBAAgB,CAAC,OAAO,CAAC,CAAC;IAErD,MAAM,MAAM,GAAG,KAAK,CAAC,MAAM,IAAI,SAAS,CAAC;IACzC,MAAM,OAAO,GAAG,KAAK,CAAC,OAAO,IAAI,EAAE,CAAC;IAEpC,MAAM,SAAS,GAA+B;QAC1C,KAAK,EAAE;YACH,KAAK,EAAE,SAAS;YAChB,IAAI,EAAE,MAAM;YACZ,KAAK,EAAE,OAAO;YACd,GAAG,EAAE,KAAK;SACb;KACJ,CAAC;IAEF,MAAM,QAAQ,GAAwD,EAAE,CAAC;IACzE,IAAI,KAAK,CAAC,WAAW,KAAK,SAAS;QAAE,QAAQ,CAAC,WAAW,GAAG,KAAK,CAAC,WAAW,CAAC;IAC9E,IAAI,KAAK,CAAC,QAAQ,KAAK,SAAS;QAAE,QAAQ,CAAC,QAAQ,GAAG,KAAK,CAAC,QAAQ,CAAC;IACrE,QAAQ,CAAC,MAAM,GAAG,KAAK,CAAC,MAAM,IAAI,OAAO,CAAC;IAE1C,gEAAgE;IAChE,+DAA+D;IAC/D,oEAAoE;IACpE,sDAAsD;IACtD,IAAI,KAAK,CAAC,UAAU,EAAE,OAAO,IAAI,KAAK,CAAC,UAAU,CAAC,OAAO,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACnE,MAAM,WAAW,GAAG,QAAQ,CAAC,MAAM,EAAE,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE,WAAW,EAAE,CAAC;QAClE,MAAM,eAAe,GAAa,EAAE,CAAC;QACrC,MAAM,IAAI,GAAG,IAAI,GAAG,EAAU,CAAC;QAC/B,KAAK,MAAM,KAAK,IAAI,KAAK,CAAC,UAAU,CAAC,OAAO,EAAE,CAAC;YAC3C,MAAM,QAAQ,GAAG,KAAK,CAAC,QAAQ,CAAC,OAAO,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC;YAClD,MAAM,SAAS,GAAG,KAAK,CAAC,QAAQ,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE,WAAW,EAAE,CAAC;YAC9D,IAAI,IAAI,CAAC,GAAG,CAAC,QAAQ,CAAC;gBAAE,SAAS;YACjC,IAAI,QAAQ,KAAK,QAAQ,CAAC,MAAM;gBAAE,SAAS;YAC3C,6EAA6E;YAC7E,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,QAAQ,CAAC,GAAG,CAAC,IAAI,SAAS,KAAK,WAAW;gBAAE,SAAS;YACzE,IAAI,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;YACnB,eAAe,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;QACnC,CAAC;QACD,IAAI,eAAe,CAAC,MAAM,GAAG,CAAC;YAAE,QAAQ,CAAC,eAAe,GAAG,eAAe,CAAC;IAC/E,CAAC;IAED,SAAS,CAAC,QAAQ,GAAG,QAAQ,CAAC;IAE9B,MAAM,YAAY,GACd,KAAK,CAAC,UAAU,KAAK,SAAS;QAC9B,KAAK,CAAC,YAAY,KAAK,SAAS;QAChC,KAAK,CAAC,aAAa,KAAK,SAAS,CAAC;IACtC,IAAI,OAAO,IAAI,YAAY,EAAE,CAAC;QAC1B,MAAM,KAAK,GAAqD,EAAE,CAAC;QACnE,IAAI,KAAK,CAAC,UAAU,KAAK,SAAS;YAAE,KAAK,CAAC,GAAG,GAAG,KAAK,CAAC,UAAU,CAAC;QACjE,IAAI,KAAK,CAAC,YAAY,KAAK,SAAS;YAAE,KAAK,CAAC,KAAK,GAAG,KAAK,CAAC,YAAY,CAAC;QACvE,IAAI,KAAK,CAAC,aAAa,KAAK,SAAS;YAAE,KAAK,CAAC,MAAM,GAAG,KAAK,CAAC,aAAa,CAAC;QAC1E,SAAS,CAAC,KAAK,GAAG,KAAK,CAAC;IAC5B,CAAC;IAED,IAAI,MAAM,KAAK,SAAS,IAAI,CAAC,KAAK,CAAC,OAAO,IAAI,KAAK,CAAC,gBAAgB,CAAC,EAAE,CAAC;QACpE,MAAM,OAAO,GAAuD,EAAE,CAAC;QACvE,IAAI,KAAK,CAAC,OAAO,EAAE,CAAC;YAChB,MAAM,SAAS,GAAG,WAAW,CAAC,KAAK,CAAC,OAAO,CAAC,aAAa,CAAC,CAAC;YAC3D,IAAI,SAAS,KAAK,SAAS;gBAAE,OAAO,CAAC,aAAa,GAAG,SAAS,CAAC;YAC/D,MAAM,QAAQ,GAAG,WAAW,CAAC,KAAK,CAAC,OAAO,CAAC,YAAY,CAAC,CAAC;YACzD,IAAI,QAAQ,KAAK,SAAS;gBAAE,OAAO,CAAC,YAAY,GAAG,QAAQ,CAAC;YAC5D,MAAM,UAAU,GAAG,WAAW,CAAC,KAAK,CAAC,OAAO,CAAC,cAAc,CAAC,CAAC;YAC7D,IAAI,UAAU,KAAK,SAAS;gBAAE,OAAO,CAAC,cAAc,GAAG,UAAU,CAAC;YAClE,IAAI,KAAK,CAAC,OAAO,CAAC,OAAO,KAAK,SAAS;gBAAE,OAAO,CAAC,OAAO,GAAG,CAAC,GAAG,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC;YACtF,IAAI,KAAK,CAAC,OAAO,CAAC,IAAI,KAAK,SAAS;gBAAE,OAAO,CAAC,IAAI,GAAG,CAAC,GAAG,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;YAC7E,IAAI,KAAK,CAAC,OAAO,CAAC,OAAO,KAAK,SAAS;gBAAE,OAAO,CAAC,OAAO,GAAG,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC;QACrF,CAAC;QACD,IAAI,KAAK,CAAC,gBAAgB,KAAK,SAAS;YAAE,OAAO,CAAC,SAAS,GAAG,KAAK,CAAC,gBAAgB,CAAC;QACrF,SAAS,CAAC,OAAO,GAAG,OAAO,CAAC;IAChC,CAAC;IAED,MAAM,QAAQ,GAAkB;QAC5B,KAAK,EAAE,SAAS;QAChB,SAAS;KACZ,CAAC;IAEF,IAAI,SAAS,KAAK,SAAS;QAAE,QAAQ,CAAC,SAAS,GAAG,SAAS,CAAC;IAC5D,IAAI,KAAK,CAAC,WAAW,KAAK,SAAS;QAAE,QAAQ,CAAC,WAAW,GAAG,KAAK,CAAC,WAAW,CAAC;IAE9E,IAAI,KAAK,CAAC,OAAO,KAAK,SAAS,EAAE,CAAC;QAC9B,MAAM,OAAO,GAA0C;YACnD,IAAI,EAAE,KAAK,CAAC,OAAO,CAAC,IAAI,IAAI,qBAAqB;SACpD,CAAC;QACF,IAAI,KAAK,CAAC,OAAO,CAAC,IAAI,KAAK,SAAS;YAAE,OAAO,CAAC,IAAI,GAAG,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC;QACxE,IAAI,KAAK,CAAC,OAAO,CAAC,OAAO,KAAK,SAAS;YAAE,OAAO,CAAC,OAAO,GAAG,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC;QACjF,8DAA8D;QAC9D,yDAAyD;QACzD,uDAAuD;QACvD,gDAAgD;QAChD,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,KAAK,SAAS,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,KAAK,SAAS,EAAE,CAAC;YACzE,OAAO,CAAC,KAAK,GAAG,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC;QACxC,CAAC;QACD,IACI,KAAK,CAAC,OAAO,CAAC,WAAW,KAAK,SAAS;YACvC,KAAK,CAAC,OAAO,CAAC,WAAW,KAAK,KAAK,CAAC,WAAW,EACjD,CAAC;YACC,OAAO,CAAC,WAAW,GAAG,KAAK,CAAC,OAAO,CAAC,WAAW,CAAC;QACpD,CAAC;QACD,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,KAAK,SAAS,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,KAAK,OAAO,EAAE,CAAC;YACvE,OAAO,CAAC,KAAK,GAAG,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC;QACxC,CAAC;QACD,IAAI,KAAK,CAAC,OAAO,CAAC,QAAQ,KAAK,SAAS,IAAI,KAAK,CAAC,OAAO,CAAC,QAAQ,KAAK,KAAK,CAAC,UAAU,EAAE,CAAC;YACtF,OAAO,CAAC,QAAQ,GAAG,KAAK,CAAC,OAAO,CAAC,QAAQ,CAAC;QAC9C,CAAC;QACD,QAAQ,CAAC,OAAO,GAAG,OAAO,CAAC;IAC/B,CAAC;IAED,mEAAmE;IACnE,0DAA0D;IAC1D,MAAM,cAAc,GAChB,KAAK,CAAC,UAAU,KAAK,SAAS,CAAC,CAAC,CAAC,mBAAmB,CAAC,KAAK,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;IAEhF,iEAAiE;IACjE,MAAM,aAAa,GAAG,kBAAkB,CAAC,KAAK,CAAC,OAAO,EAAE,KAAK,CAAC,QAAQ,CAAC,CAAC;IAExE,kEAAkE;IAClE,qCAAqC;IACrC,MAAM,UAAU,GAAG,KAAK,CAAC,MAAM,IAAI,KAAK,CAAC,OAAO,EAAE,OAAO,EAAE,CAAC,CAAC,CAAC,CAAC;IAE/D,MAAM,IAAI,GAAkC,EAAE,CAAC;IAC/C,IAAI,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,OAAO,EAAE,aAAa,EAAE,CAAC,CAAC;IACtD,IAAI,UAAU,KAAK,SAAS,EAAE,CAAC;QAC3B,IAAI,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,OAAO,EAAE,UAAU,EAAE,CAAC,CAAC;IACvD,CAAC;IACD,IAAI,KAAK,CAAC,SAAS,KAAK,SAAS,EAAE,CAAC;QAChC,KAAK,MAAM,KAAK,IAAI,KAAK,CAAC,SAAS;YAAE,IAAI,CAAC,IAAI,CAAC,EAAE,GAAG,KAAK,EAAE,CAAC,CAAC;IACjE,CAAC;IAED,MAAM,aAAa,GAAG,KAAK,CAAC,UAAU,KAAK,SAAS,IAAI,KAAK,CAAC,UAAU,CAAC,MAAM,GAAG,CAAC,CAAC;IACpF,MAAM,aAAa,GAAG,cAAc,CAAC,MAAM,GAAG,CAAC,CAAC;IAChD,MAAM,IAAI,GAAkC,EAAE,CAAC;IAC/C,IAAI,aAAa,EAAE,CAAC;QAChB,KAAK,MAAM,KAAK,IAAI,KAAK,CAAC,UAAW;YAAE,IAAI,CAAC,IAAI,CAAC,EAAE,GAAG,KAAK,EAAE,CAAC,CAAC;IACnE,CAAC;IACD,IAAI,aAAa,EAAE,CAAC;QAChB,KAAK,MAAM,KAAK,IAAI,cAAc,EAAE,CAAC;YACjC,IAAI,CAAC,IAAI,CAAC;gBACN,GAAG,EAAE,KAAK,CAAC,GAAG;gBACd,IAAI,EAAE,KAAK,CAAC,IAAI;gBAChB,QAAQ,EAAE,KAAK,CAAC,QAAQ;aAC3B,CAAC,CAAC;QACP,CAAC;IACL,CAAC;IAED,MAAM,MAAM,GAAyC,EAAE,CAAC;IACxD,IAAI,IAAI,CAAC,MAAM,GAAG,CAAC;QAAE,MAAM,CAAC,IAAI,GAAG,IAAI,CAAC;IACxC,IAAI,IAAI,CAAC,MAAM,GAAG,CAAC;QAAE,MAAM,CAAC,IAAI,GAAG,IAAI,CAAC;IACxC,IAAI,MAAM,CAAC,IAAI,KAAK,SAAS,IAAI,MAAM,CAAC,IAAI,KAAK,SAAS,EAAE,CAAC;QACzD,QAAQ,CAAC,MAAM,GAAG,MAAM,CAAC;IAC7B,CAAC;IAED,OAAO,QAAQ,CAAC;AACpB,CAAC"}
|
|
@@ -11,6 +11,11 @@ type AstroImageFunction = any;
|
|
|
11
11
|
* `image()` from the collection schema factory so the image is processed
|
|
12
12
|
* through Astro's asset pipeline.
|
|
13
13
|
*
|
|
14
|
+
* `alt` is **required** — missing alt text is an accessibility failure
|
|
15
|
+
* and an SEO failure. Decorative images should use `alt: ''` explicitly.
|
|
16
|
+
* If you want to make the whole image optional, wrap the schema:
|
|
17
|
+
* `imageSchema(image).optional()`.
|
|
18
|
+
*
|
|
14
19
|
* @example
|
|
15
20
|
* ```ts
|
|
16
21
|
* import { defineCollection, z } from 'astro:content';
|
|
@@ -27,13 +32,13 @@ type AstroImageFunction = any;
|
|
|
27
32
|
*/
|
|
28
33
|
export declare function imageSchema(image: AstroImageFunction): z.ZodObject<{
|
|
29
34
|
src: any;
|
|
30
|
-
alt: z.
|
|
35
|
+
alt: z.ZodString;
|
|
31
36
|
}, "strip", z.ZodTypeAny, {
|
|
37
|
+
alt: string;
|
|
32
38
|
src?: any;
|
|
33
|
-
alt?: string | undefined;
|
|
34
39
|
}, {
|
|
40
|
+
alt: string;
|
|
35
41
|
src?: any;
|
|
36
|
-
alt?: string | undefined;
|
|
37
42
|
}>;
|
|
38
43
|
/**
|
|
39
44
|
* Zod schema for a nested `seo` field holding per-entry SEO overrides:
|
|
@@ -57,13 +62,13 @@ export declare function seoSchema(image: AstroImageFunction): z.ZodObject<{
|
|
|
57
62
|
description: z.ZodOptional<z.ZodString>;
|
|
58
63
|
image: z.ZodOptional<z.ZodObject<{
|
|
59
64
|
src: any;
|
|
60
|
-
alt: z.
|
|
65
|
+
alt: z.ZodString;
|
|
61
66
|
}, "strip", z.ZodTypeAny, {
|
|
67
|
+
alt: string;
|
|
62
68
|
src?: any;
|
|
63
|
-
alt?: string | undefined;
|
|
64
69
|
}, {
|
|
70
|
+
alt: string;
|
|
65
71
|
src?: any;
|
|
66
|
-
alt?: string | undefined;
|
|
67
72
|
}>>;
|
|
68
73
|
pageType: z.ZodDefault<z.ZodEnum<["website", "article"]>>;
|
|
69
74
|
}, "strip", z.ZodTypeAny, {
|
|
@@ -71,15 +76,15 @@ export declare function seoSchema(image: AstroImageFunction): z.ZodObject<{
|
|
|
71
76
|
title?: string | undefined;
|
|
72
77
|
description?: string | undefined;
|
|
73
78
|
image?: {
|
|
79
|
+
alt: string;
|
|
74
80
|
src?: any;
|
|
75
|
-
alt?: string | undefined;
|
|
76
81
|
} | undefined;
|
|
77
82
|
}, {
|
|
78
83
|
title?: string | undefined;
|
|
79
84
|
description?: string | undefined;
|
|
80
85
|
image?: {
|
|
86
|
+
alt: string;
|
|
81
87
|
src?: any;
|
|
82
|
-
alt?: string | undefined;
|
|
83
88
|
} | undefined;
|
|
84
89
|
pageType?: "website" | "article" | undefined;
|
|
85
90
|
}>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"content-helpers.d.ts","sourceRoot":"","sources":["../src/content-helpers.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB;;;;;GAKG;AAEH,KAAK,kBAAkB,GAAG,GAAG,CAAC;AAE9B
|
|
1
|
+
{"version":3,"file":"content-helpers.d.ts","sourceRoot":"","sources":["../src/content-helpers.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB;;;;;GAKG;AAEH,KAAK,kBAAkB,GAAG,GAAG,CAAC;AAE9B;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,kBAAkB;;;;;;;;;GAKpD;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,SAAS,CAAC,KAAK,EAAE,kBAAkB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAOlD"}
|
package/dist/content-helpers.js
CHANGED
|
@@ -4,6 +4,11 @@ import { z } from 'zod';
|
|
|
4
4
|
* `image()` from the collection schema factory so the image is processed
|
|
5
5
|
* through Astro's asset pipeline.
|
|
6
6
|
*
|
|
7
|
+
* `alt` is **required** — missing alt text is an accessibility failure
|
|
8
|
+
* and an SEO failure. Decorative images should use `alt: ''` explicitly.
|
|
9
|
+
* If you want to make the whole image optional, wrap the schema:
|
|
10
|
+
* `imageSchema(image).optional()`.
|
|
11
|
+
*
|
|
7
12
|
* @example
|
|
8
13
|
* ```ts
|
|
9
14
|
* import { defineCollection, z } from 'astro:content';
|
|
@@ -21,7 +26,7 @@ import { z } from 'zod';
|
|
|
21
26
|
export function imageSchema(image) {
|
|
22
27
|
return z.object({
|
|
23
28
|
src: image(),
|
|
24
|
-
alt: z.string()
|
|
29
|
+
alt: z.string(),
|
|
25
30
|
});
|
|
26
31
|
}
|
|
27
32
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"content-helpers.js","sourceRoot":"","sources":["../src/content-helpers.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAWxB
|
|
1
|
+
{"version":3,"file":"content-helpers.js","sourceRoot":"","sources":["../src/content-helpers.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAWxB;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,MAAM,UAAU,WAAW,CAAC,KAAyB;IACjD,OAAO,CAAC,CAAC,MAAM,CAAC;QACZ,GAAG,EAAE,KAAK,EAAE;QACZ,GAAG,EAAE,CAAC,CAAC,MAAM,EAAE;KAClB,CAAC,CAAC;AACP,CAAC;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,UAAU,SAAS,CAAC,KAAyB;IAC/C,OAAO,CAAC,CAAC,MAAM,CAAC;QACZ,KAAK,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,QAAQ,EAAE;QAC5C,WAAW,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,QAAQ,EAAE;QACnD,KAAK,EAAE,WAAW,CAAC,KAAK,CAAC,CAAC,QAAQ,EAAE;QACpC,QAAQ,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,SAAS,EAAE,SAAS,CAAC,CAAC,CAAC,OAAO,CAAC,SAAS,CAAC;KAC9D,CAAC,CAAC;AACP,CAAC"}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
interface BuildDoneHook {
|
|
2
|
+
dir: URL;
|
|
3
|
+
logger: {
|
|
4
|
+
warn: (msg: string) => void;
|
|
5
|
+
info: (msg: string) => void;
|
|
6
|
+
[key: string]: any;
|
|
7
|
+
};
|
|
8
|
+
}
|
|
9
|
+
interface AstroIntegrationLike {
|
|
10
|
+
name: string;
|
|
11
|
+
hooks: {
|
|
12
|
+
'astro:build:done'?: (args: BuildDoneHook) => void | Promise<void>;
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
export interface SeoGraphIntegrationOptions {
|
|
16
|
+
/**
|
|
17
|
+
* Warn when a built page has zero or more than one `<h1>` element.
|
|
18
|
+
* Defaults to `true`. Only static pages are checked (SSR pages are
|
|
19
|
+
* not present on disk at build time).
|
|
20
|
+
*/
|
|
21
|
+
validateH1?: boolean;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Count `<h1>` elements in an HTML string. Matches the opening tag only;
|
|
25
|
+
* tolerant of attributes and whitespace. Doesn't parse — good enough for
|
|
26
|
+
* a lint warning.
|
|
27
|
+
*/
|
|
28
|
+
export declare function countH1s(html: string): number;
|
|
29
|
+
/**
|
|
30
|
+
* Astro integration for `@jdevalk/astro-seo-graph`.
|
|
31
|
+
*
|
|
32
|
+
* Currently:
|
|
33
|
+
* - Warns about built pages with zero or more than one `<h1>` element
|
|
34
|
+
* (a common SEO and accessibility issue).
|
|
35
|
+
*
|
|
36
|
+
* ```js
|
|
37
|
+
* // astro.config.mjs
|
|
38
|
+
* import { defineConfig } from 'astro/config';
|
|
39
|
+
* import seoGraph from '@jdevalk/astro-seo-graph/integration';
|
|
40
|
+
*
|
|
41
|
+
* export default defineConfig({
|
|
42
|
+
* integrations: [seoGraph()],
|
|
43
|
+
* });
|
|
44
|
+
* ```
|
|
45
|
+
*/
|
|
46
|
+
export default function seoGraph(options?: SeoGraphIntegrationOptions): AstroIntegrationLike;
|
|
47
|
+
export {};
|
|
48
|
+
//# sourceMappingURL=integration.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"integration.d.ts","sourceRoot":"","sources":["../src/integration.ts"],"names":[],"mappings":"AAOA,UAAU,aAAa;IACnB,GAAG,EAAE,GAAG,CAAC;IAET,MAAM,EAAE;QAAE,IAAI,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;QAAC,IAAI,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;QAAC,CAAC,GAAG,EAAE,MAAM,GAAG,GAAG,CAAA;KAAE,CAAC;CAC5F;AAED,UAAU,oBAAoB;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE;QACH,kBAAkB,CAAC,EAAE,CAAC,IAAI,EAAE,aAAa,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;KACtE,CAAC;CACL;AAED,MAAM,WAAW,0BAA0B;IACvC;;;;OAIG;IACH,UAAU,CAAC,EAAE,OAAO,CAAC;CACxB;AAED;;;;GAIG;AACH,wBAAgB,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAG7C;AAgBD;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,CAAC,OAAO,UAAU,QAAQ,CAAC,OAAO,GAAE,0BAA+B,GAAG,oBAAoB,CAoC/F"}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { readFile, readdir } from 'node:fs/promises';
|
|
2
|
+
import { join, relative } from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
/**
|
|
5
|
+
* Count `<h1>` elements in an HTML string. Matches the opening tag only;
|
|
6
|
+
* tolerant of attributes and whitespace. Doesn't parse — good enough for
|
|
7
|
+
* a lint warning.
|
|
8
|
+
*/
|
|
9
|
+
export function countH1s(html) {
|
|
10
|
+
const matches = html.match(/<h1[\s>]/gi);
|
|
11
|
+
return matches ? matches.length : 0;
|
|
12
|
+
}
|
|
13
|
+
async function collectHtmlFiles(dir, base = dir) {
|
|
14
|
+
const entries = await readdir(dir, { withFileTypes: true });
|
|
15
|
+
const files = [];
|
|
16
|
+
for (const entry of entries) {
|
|
17
|
+
const fullPath = join(dir, entry.name);
|
|
18
|
+
if (entry.isDirectory()) {
|
|
19
|
+
files.push(...(await collectHtmlFiles(fullPath, base)));
|
|
20
|
+
}
|
|
21
|
+
else if (entry.isFile() && entry.name.endsWith('.html')) {
|
|
22
|
+
files.push(relative(base, fullPath));
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return files;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Astro integration for `@jdevalk/astro-seo-graph`.
|
|
29
|
+
*
|
|
30
|
+
* Currently:
|
|
31
|
+
* - Warns about built pages with zero or more than one `<h1>` element
|
|
32
|
+
* (a common SEO and accessibility issue).
|
|
33
|
+
*
|
|
34
|
+
* ```js
|
|
35
|
+
* // astro.config.mjs
|
|
36
|
+
* import { defineConfig } from 'astro/config';
|
|
37
|
+
* import seoGraph from '@jdevalk/astro-seo-graph/integration';
|
|
38
|
+
*
|
|
39
|
+
* export default defineConfig({
|
|
40
|
+
* integrations: [seoGraph()],
|
|
41
|
+
* });
|
|
42
|
+
* ```
|
|
43
|
+
*/
|
|
44
|
+
export default function seoGraph(options = {}) {
|
|
45
|
+
const { validateH1 = true } = options;
|
|
46
|
+
return {
|
|
47
|
+
name: '@jdevalk/astro-seo-graph',
|
|
48
|
+
hooks: {
|
|
49
|
+
'astro:build:done': async ({ dir, logger }) => {
|
|
50
|
+
if (!validateH1)
|
|
51
|
+
return;
|
|
52
|
+
const buildDir = fileURLToPath(dir);
|
|
53
|
+
const htmlFiles = await collectHtmlFiles(buildDir);
|
|
54
|
+
const missing = [];
|
|
55
|
+
const multiple = [];
|
|
56
|
+
for (const file of htmlFiles) {
|
|
57
|
+
const content = await readFile(join(buildDir, file), 'utf8');
|
|
58
|
+
const count = countH1s(content);
|
|
59
|
+
if (count === 0)
|
|
60
|
+
missing.push(file);
|
|
61
|
+
else if (count > 1)
|
|
62
|
+
multiple.push({ file, count });
|
|
63
|
+
}
|
|
64
|
+
if (missing.length === 0 && multiple.length === 0) {
|
|
65
|
+
logger.info(`H1 validation: ${htmlFiles.length} pages checked, all good.`);
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
for (const file of missing) {
|
|
69
|
+
logger.warn(`H1 validation: ${file} has no <h1>.`);
|
|
70
|
+
}
|
|
71
|
+
for (const { file, count } of multiple) {
|
|
72
|
+
logger.warn(`H1 validation: ${file} has ${count} <h1> elements (expected 1).`);
|
|
73
|
+
}
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
//# sourceMappingURL=integration.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"integration.js","sourceRoot":"","sources":["../src/integration.ts"],"names":[],"mappings":"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"}
|
package/dist/routes.d.ts
CHANGED
|
@@ -58,10 +58,6 @@ export interface SchemaMapEntry {
|
|
|
58
58
|
path: string;
|
|
59
59
|
/** When the resource at this path was last modified. */
|
|
60
60
|
lastModified: Date;
|
|
61
|
-
/** Defaults to `'daily'`. */
|
|
62
|
-
changeFreq?: 'always' | 'hourly' | 'daily' | 'weekly' | 'monthly' | 'yearly' | 'never';
|
|
63
|
-
/** 0.0–1.0. Defaults to `0.8`. */
|
|
64
|
-
priority?: number;
|
|
65
61
|
}
|
|
66
62
|
export interface SchemaMapOptions {
|
|
67
63
|
/** Canonical site URL. Trailing slash is stripped. */
|
package/dist/routes.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"routes.d.ts","sourceRoot":"","sources":["../src/routes.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAC;AACtC,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAC;AAG3D,MAAM,WAAW,qBAAqB,CAAC,KAAK;IACxC;;;;OAIG;IACH,OAAO,EAAE,MAAM,OAAO,CAAC,SAAS,KAAK,EAAE,CAAC,CAAC;IACzC;;;;OAIG;IACH,MAAM,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,aAAa,CAAC,WAAW,CAAC,CAAC;IACrD;;;OAGG;IACH,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B;;OAEG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;;OAGG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;CACnB;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,OAAO,EAAE,qBAAqB,CAAC,KAAK,CAAC,GAAG,QAAQ,
|
|
1
|
+
{"version":3,"file":"routes.d.ts","sourceRoot":"","sources":["../src/routes.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAC;AACtC,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAC;AAG3D,MAAM,WAAW,qBAAqB,CAAC,KAAK;IACxC;;;;OAIG;IACH,OAAO,EAAE,MAAM,OAAO,CAAC,SAAS,KAAK,EAAE,CAAC,CAAC;IACzC;;;;OAIG;IACH,MAAM,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,aAAa,CAAC,WAAW,CAAC,CAAC;IACrD;;;OAGG;IACH,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B;;OAEG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;;OAGG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;CACnB;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,OAAO,EAAE,qBAAqB,CAAC,KAAK,CAAC,GAAG,QAAQ,CAe3F;AAED,MAAM,WAAW,cAAc;IAC3B;;;OAGG;IACH,IAAI,EAAE,MAAM,CAAC;IACb,wDAAwD;IACxD,YAAY,EAAE,IAAI,CAAC;CACtB;AAED,MAAM,WAAW,gBAAgB;IAC7B,sDAAsD;IACtD,OAAO,EAAE,MAAM,CAAC;IAChB,qCAAqC;IACrC,OAAO,EAAE,SAAS,cAAc,EAAE,CAAC;IACnC,sDAAsD;IACtD,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAChC;AAWD;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,eAAe,CAAC,OAAO,EAAE,gBAAgB,GAAG,QAAQ,CA6BnE"}
|
package/dist/routes.js
CHANGED
|
@@ -27,7 +27,10 @@ export function createSchemaEndpoint(options) {
|
|
|
27
27
|
return async () => {
|
|
28
28
|
const entries = await options.entries();
|
|
29
29
|
const graph = aggregate({ entries, mapper: options.mapper });
|
|
30
|
-
const headers = {
|
|
30
|
+
const headers = {
|
|
31
|
+
'Content-Type': contentType,
|
|
32
|
+
'X-Robots-Tag': 'noindex, follow',
|
|
33
|
+
};
|
|
31
34
|
if (cacheControl !== null)
|
|
32
35
|
headers['Cache-Control'] = cacheControl;
|
|
33
36
|
return new Response(JSON.stringify(graph, null, indent), { headers });
|
|
@@ -69,13 +72,9 @@ export function createSchemaMap(options) {
|
|
|
69
72
|
const pathPart = entry.path.startsWith('/') ? entry.path : `/${entry.path}`;
|
|
70
73
|
const loc = escapeXml(`${site}${pathPart}`);
|
|
71
74
|
const lastmod = entry.lastModified.toISOString().split('T')[0];
|
|
72
|
-
const changefreq = entry.changeFreq ?? 'daily';
|
|
73
|
-
const priority = entry.priority ?? 0.8;
|
|
74
75
|
return ` <url contentType="structuredData/schema.org">
|
|
75
76
|
<loc>${loc}</loc>
|
|
76
77
|
<lastmod>${lastmod}</lastmod>
|
|
77
|
-
<changefreq>${changefreq}</changefreq>
|
|
78
|
-
<priority>${priority}</priority>
|
|
79
78
|
</url>`;
|
|
80
79
|
})
|
|
81
80
|
.join('\n');
|
|
@@ -83,7 +82,10 @@ export function createSchemaMap(options) {
|
|
|
83
82
|
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
|
84
83
|
${urls}
|
|
85
84
|
</urlset>`;
|
|
86
|
-
const headers = {
|
|
85
|
+
const headers = {
|
|
86
|
+
'Content-Type': 'application/xml',
|
|
87
|
+
'X-Robots-Tag': 'noindex, follow',
|
|
88
|
+
};
|
|
87
89
|
if (cacheControl !== null)
|
|
88
90
|
headers['Cache-Control'] = cacheControl;
|
|
89
91
|
return new Response(xml, { headers });
|
package/dist/routes.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"routes.js","sourceRoot":"","sources":["../src/routes.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AA+B5C;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,MAAM,UAAU,oBAAoB,CAAQ,OAAqC;IAC7E,MAAM,YAAY,GAAG,OAAO,CAAC,YAAY,KAAK,SAAS,CAAC,CAAC,CAAC,aAAa,CAAC,CAAC,CAAC,OAAO,CAAC,YAAY,CAAC;IAC/F,MAAM,WAAW,GAAG,OAAO,CAAC,WAAW,IAAI,qBAAqB,CAAC;IACjE,MAAM,MAAM,GAAG,OAAO,CAAC,MAAM,IAAI,CAAC,CAAC;IAEnC,OAAO,KAAK,IAAI,EAAE;QACd,MAAM,OAAO,GAAG,MAAM,OAAO,CAAC,OAAO,EAAE,CAAC;QACxC,MAAM,KAAK,GAAG,SAAS,CAAC,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;QAC7D,MAAM,OAAO,GAA2B,
|
|
1
|
+
{"version":3,"file":"routes.js","sourceRoot":"","sources":["../src/routes.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AA+B5C;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,MAAM,UAAU,oBAAoB,CAAQ,OAAqC;IAC7E,MAAM,YAAY,GAAG,OAAO,CAAC,YAAY,KAAK,SAAS,CAAC,CAAC,CAAC,aAAa,CAAC,CAAC,CAAC,OAAO,CAAC,YAAY,CAAC;IAC/F,MAAM,WAAW,GAAG,OAAO,CAAC,WAAW,IAAI,qBAAqB,CAAC;IACjE,MAAM,MAAM,GAAG,OAAO,CAAC,MAAM,IAAI,CAAC,CAAC;IAEnC,OAAO,KAAK,IAAI,EAAE;QACd,MAAM,OAAO,GAAG,MAAM,OAAO,CAAC,OAAO,EAAE,CAAC;QACxC,MAAM,KAAK,GAAG,SAAS,CAAC,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;QAC7D,MAAM,OAAO,GAA2B;YACpC,cAAc,EAAE,WAAW;YAC3B,cAAc,EAAE,iBAAiB;SACpC,CAAC;QACF,IAAI,YAAY,KAAK,IAAI;YAAE,OAAO,CAAC,eAAe,CAAC,GAAG,YAAY,CAAC;QACnE,OAAO,IAAI,QAAQ,CAAC,IAAI,CAAC,SAAS,CAAC,KAAK,EAAE,IAAI,EAAE,MAAM,CAAC,EAAE,EAAE,OAAO,EAAE,CAAC,CAAC;IAC1E,CAAC,CAAC;AACN,CAAC;AAqBD,SAAS,SAAS,CAAC,KAAa;IAC5B,OAAO,KAAK;SACP,OAAO,CAAC,IAAI,EAAE,OAAO,CAAC;SACtB,OAAO,CAAC,IAAI,EAAE,MAAM,CAAC;SACrB,OAAO,CAAC,IAAI,EAAE,MAAM,CAAC;SACrB,OAAO,CAAC,IAAI,EAAE,QAAQ,CAAC;SACvB,OAAO,CAAC,IAAI,EAAE,QAAQ,CAAC,CAAC;AACjC,CAAC;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,MAAM,UAAU,eAAe,CAAC,OAAyB;IACrD,MAAM,YAAY,GAAG,OAAO,CAAC,YAAY,KAAK,SAAS,CAAC,CAAC,CAAC,aAAa,CAAC,CAAC,CAAC,OAAO,CAAC,YAAY,CAAC;IAC/F,MAAM,IAAI,GAAG,OAAO,CAAC,OAAO,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC;IAEjD,OAAO,KAAK,IAAI,EAAE;QACd,MAAM,IAAI,GAAG,OAAO,CAAC,OAAO;aACvB,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE;YACX,MAAM,QAAQ,GAAG,KAAK,CAAC,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,KAAK,CAAC,IAAI,EAAE,CAAC;YAC5E,MAAM,GAAG,GAAG,SAAS,CAAC,GAAG,IAAI,GAAG,QAAQ,EAAE,CAAC,CAAC;YAC5C,MAAM,OAAO,GAAG,KAAK,CAAC,YAAY,CAAC,WAAW,EAAE,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC;YAC/D,OAAO;WACZ,GAAG;eACC,OAAO;SACb,CAAC;QACE,CAAC,CAAC;aACD,IAAI,CAAC,IAAI,CAAC,CAAC;QAEhB,MAAM,GAAG,GAAG;;EAElB,IAAI;UACI,CAAC;QAEH,MAAM,OAAO,GAA2B;YACpC,cAAc,EAAE,iBAAiB;YACjC,cAAc,EAAE,iBAAiB;SACpC,CAAC;QACF,IAAI,YAAY,KAAK,IAAI;YAAE,OAAO,CAAC,eAAe,CAAC,GAAG,YAAY,CAAC;QACnE,OAAO,IAAI,QAAQ,CAAC,GAAG,EAAE,EAAE,OAAO,EAAE,CAAC,CAAC;IAC1C,CAAC,CAAC;AACN,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jdevalk/astro-seo-graph",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "Astro integration for @jdevalk/seo-graph-core. Seo component, route factories, content-collection aggregator, Zod content helpers.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"astro",
|
|
@@ -23,7 +23,11 @@
|
|
|
23
23
|
"import": "./dist/index.js"
|
|
24
24
|
},
|
|
25
25
|
"./Seo.astro": "./dist/components/Seo.astro",
|
|
26
|
-
"./FuzzyRedirect.astro": "./dist/components/FuzzyRedirect.astro"
|
|
26
|
+
"./FuzzyRedirect.astro": "./dist/components/FuzzyRedirect.astro",
|
|
27
|
+
"./integration": {
|
|
28
|
+
"types": "./dist/integration.d.ts",
|
|
29
|
+
"import": "./dist/integration.js"
|
|
30
|
+
}
|
|
27
31
|
},
|
|
28
32
|
"files": [
|
|
29
33
|
"dist",
|