@jdevalk/astro-seo-graph 0.5.2 → 0.7.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 +88 -1
- 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/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/dist/indexnow.d.ts +25 -0
- package/dist/indexnow.d.ts.map +1 -0
- package/dist/indexnow.js +28 -0
- package/dist/indexnow.js.map +1 -0
- package/dist/integration.d.ts +88 -0
- package/dist/integration.d.ts.map +1 -0
- package/dist/integration.js +129 -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 +7 -3
package/README.md
CHANGED
|
@@ -24,6 +24,7 @@ schema.org best practices — see [AGENTS.md](https://github.com/jdevalk/seo-gra
|
|
|
24
24
|
| **`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). |
|
|
25
25
|
| **`breadcrumbsFromUrl`** | Derives a breadcrumb trail from an Astro URL. Splits path segments, supports custom display names and segment skipping. Returns `BreadcrumbItem[]` ready to pass to `buildBreadcrumbList`. |
|
|
26
26
|
| **`<FuzzyRedirect>`** | Drop-in 404 component. Fetches your sitemap, fuzzy-matches the current URL against known paths, and suggests or auto-redirects to the closest match. |
|
|
27
|
+
| **`createIndexNowKeyRoute`** | Factory returning an `APIRoute` that serves the IndexNow key-verification file at `/<key>.txt`. Pair with the `indexNow` option on the integration to auto-submit built URLs on `astro:build:done`. |
|
|
27
28
|
|
|
28
29
|
## Installation
|
|
29
30
|
|
|
@@ -76,6 +77,27 @@ const graph = buildSchemaGraph({
|
|
|
76
77
|
</html>
|
|
77
78
|
```
|
|
78
79
|
|
|
80
|
+
### `<Seo>` behavior notes
|
|
81
|
+
|
|
82
|
+
- **Robots defaults.** `max-snippet:-1`, `max-image-preview:large`, and
|
|
83
|
+
`max-video-preview:-1` are always emitted alongside any `noindex` /
|
|
84
|
+
`nofollow` directives, matching the Yoast-style defaults for maximum
|
|
85
|
+
snippet sizes.
|
|
86
|
+
- **Canonical + noindex.** When `noindex` is true the canonical link is
|
|
87
|
+
omitted per Google's recommendation.
|
|
88
|
+
- **Query params.** Canonical URLs strip query parameters by default. Pass
|
|
89
|
+
`preserveQueryParams` to keep them.
|
|
90
|
+
- **Twitter tag dedup.** `twitter:title`, `twitter:description`,
|
|
91
|
+
`twitter:image`, and `twitter:image:alt` are only emitted when the
|
|
92
|
+
caller explicitly overrides them via `twitter.title` / `description` /
|
|
93
|
+
`image` / `imageAlt`. Otherwise Twitter falls back to the `og:`
|
|
94
|
+
counterparts automatically.
|
|
95
|
+
- **`og:locale:alternate`.** Emitted automatically from the `alternates`
|
|
96
|
+
prop on multilingual pages.
|
|
97
|
+
- **Author / publisher.** Pass `author` for `<meta name="author">` (falls
|
|
98
|
+
back to `article.authors[0]`); pass `articlePublisher` for the
|
|
99
|
+
`article:publisher` Facebook URL.
|
|
100
|
+
|
|
79
101
|
## Breadcrumbs from URL
|
|
80
102
|
|
|
81
103
|
Derive a breadcrumb trail from an Astro page URL instead of computing it
|
|
@@ -292,18 +314,83 @@ export const GET = createSchemaMap({
|
|
|
292
314
|
```ts
|
|
293
315
|
// src/content.config.ts
|
|
294
316
|
import { defineCollection, z } from 'astro:content';
|
|
295
|
-
import { seoSchema } from '@jdevalk/astro-seo-graph';
|
|
317
|
+
import { seoSchema, imageSchema } from '@jdevalk/astro-seo-graph';
|
|
296
318
|
|
|
297
319
|
const blog = defineCollection({
|
|
298
320
|
schema: ({ image }) =>
|
|
299
321
|
z.object({
|
|
300
322
|
title: z.string(),
|
|
301
323
|
publishDate: z.coerce.date(),
|
|
324
|
+
featureImage: imageSchema(image).optional(),
|
|
302
325
|
seo: seoSchema(image).optional(),
|
|
303
326
|
}),
|
|
304
327
|
});
|
|
305
328
|
```
|
|
306
329
|
|
|
330
|
+
`imageSchema` **requires** `alt` — missing alt text is an accessibility
|
|
331
|
+
failure and an SEO failure. Decorative images should use `alt: ''`
|
|
332
|
+
explicitly. If you want the whole image to be optional, wrap the schema:
|
|
333
|
+
`imageSchema(image).optional()`.
|
|
334
|
+
|
|
335
|
+
## Astro integration
|
|
336
|
+
|
|
337
|
+
An Astro integration that runs build-time SEO checks and optional post-build
|
|
338
|
+
actions. Currently:
|
|
339
|
+
|
|
340
|
+
- Warns about built pages with zero or more than one `<h1>` element (a
|
|
341
|
+
common SEO and accessibility issue).
|
|
342
|
+
- Optionally submits built URLs to [IndexNow](https://www.indexnow.org)
|
|
343
|
+
after the build completes.
|
|
344
|
+
|
|
345
|
+
```js
|
|
346
|
+
// astro.config.mjs
|
|
347
|
+
import { defineConfig } from 'astro/config';
|
|
348
|
+
import seoGraph from '@jdevalk/astro-seo-graph/integration';
|
|
349
|
+
|
|
350
|
+
export default defineConfig({
|
|
351
|
+
integrations: [
|
|
352
|
+
seoGraph({
|
|
353
|
+
indexNow: {
|
|
354
|
+
key: process.env.INDEXNOW_KEY!,
|
|
355
|
+
host: 'example.com',
|
|
356
|
+
siteUrl: 'https://example.com',
|
|
357
|
+
},
|
|
358
|
+
}),
|
|
359
|
+
],
|
|
360
|
+
});
|
|
361
|
+
```
|
|
362
|
+
|
|
363
|
+
Options:
|
|
364
|
+
|
|
365
|
+
| Prop | Default | Description |
|
|
366
|
+
| ------------ | ------- | --------------------------------------------------------- |
|
|
367
|
+
| `validateH1` | `true` | Warn about pages without exactly one `<h1>` |
|
|
368
|
+
| `indexNow` | — | Submit built URLs to IndexNow. See below for sub-options. |
|
|
369
|
+
|
|
370
|
+
`indexNow` sub-options: `key` (8–128 hex chars), `host` (bare host, e.g.
|
|
371
|
+
`example.com`), `siteUrl` (absolute origin), `keyLocation?` (defaults to
|
|
372
|
+
`https://<host>/<key>.txt`), `endpoint?` (defaults to `api.indexnow.org`),
|
|
373
|
+
`filter?` (drop URLs for which the callback returns `false`).
|
|
374
|
+
|
|
375
|
+
`index.html` paths are rewritten to their trailing-slash form. Only static
|
|
376
|
+
pages are checked/submitted (SSR pages aren't on disk at build time).
|
|
377
|
+
|
|
378
|
+
## IndexNow key route
|
|
379
|
+
|
|
380
|
+
Serve the IndexNow key-verification file at `/<key>.txt` so participating
|
|
381
|
+
engines can confirm host ownership:
|
|
382
|
+
|
|
383
|
+
```ts
|
|
384
|
+
// src/pages/[your-key-here].txt.ts
|
|
385
|
+
import { createIndexNowKeyRoute } from '@jdevalk/astro-seo-graph';
|
|
386
|
+
|
|
387
|
+
export const GET = createIndexNowKeyRoute({ key: 'your-key-here' });
|
|
388
|
+
```
|
|
389
|
+
|
|
390
|
+
The filename (minus `.txt.ts`) must equal the key. Pair this with the
|
|
391
|
+
`indexNow` integration option above, or call `submitToIndexNow` from your
|
|
392
|
+
own deploy hook.
|
|
393
|
+
|
|
307
394
|
## License
|
|
308
395
|
|
|
309
396
|
MIT © Joost de Valk
|
|
@@ -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":"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"}
|
|
@@ -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"}
|
package/dist/index.d.ts
CHANGED
|
@@ -9,4 +9,6 @@ export { buildAlternateLinks } from './alternates.js';
|
|
|
9
9
|
export type { AlternateLink, BuildAlternateLinksInput } from './alternates.js';
|
|
10
10
|
export { breadcrumbsFromUrl } from './breadcrumbs.js';
|
|
11
11
|
export type { BreadcrumbsFromUrlInput } from './breadcrumbs.js';
|
|
12
|
+
export { createIndexNowKeyRoute, submitToIndexNow, validateIndexNowKey } from './indexnow.js';
|
|
13
|
+
export type { IndexNowKeyRouteOptions, IndexNowSubmitResult } from './indexnow.js';
|
|
12
14
|
//# 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;AAEzE,OAAO,EAAE,mBAAmB,EAAE,MAAM,iBAAiB,CAAC;AACtD,YAAY,EAAE,aAAa,EAAE,wBAAwB,EAAE,MAAM,iBAAiB,CAAC;AAE/E,OAAO,EAAE,kBAAkB,EAAE,MAAM,kBAAkB,CAAC;AACtD,YAAY,EAAE,uBAAuB,EAAE,MAAM,kBAAkB,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;AAE/E,OAAO,EAAE,kBAAkB,EAAE,MAAM,kBAAkB,CAAC;AACtD,YAAY,EAAE,uBAAuB,EAAE,MAAM,kBAAkB,CAAC;AAEhE,OAAO,EAAE,sBAAsB,EAAE,gBAAgB,EAAE,mBAAmB,EAAE,MAAM,eAAe,CAAC;AAC9F,YAAY,EAAE,uBAAuB,EAAE,oBAAoB,EAAE,MAAM,eAAe,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -9,4 +9,5 @@ export { seoSchema, imageSchema } from './content-helpers.js';
|
|
|
9
9
|
export { buildAstroSeoProps } from './components/seo-props.js';
|
|
10
10
|
export { buildAlternateLinks } from './alternates.js';
|
|
11
11
|
export { breadcrumbsFromUrl } from './breadcrumbs.js';
|
|
12
|
+
export { createIndexNowKeyRoute, submitToIndexNow, validateIndexNowKey } from './indexnow.js';
|
|
12
13
|
//# 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;AAG/D,OAAO,EAAE,mBAAmB,EAAE,MAAM,iBAAiB,CAAC;AAGtD,OAAO,EAAE,kBAAkB,EAAE,MAAM,kBAAkB,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;AAGtD,OAAO,EAAE,kBAAkB,EAAE,MAAM,kBAAkB,CAAC;AAGtD,OAAO,EAAE,sBAAsB,EAAE,gBAAgB,EAAE,mBAAmB,EAAE,MAAM,eAAe,CAAC"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { APIRoute } from 'astro';
|
|
2
|
+
import { submitToIndexNow, validateIndexNowKey, type IndexNowSubmitResult } from '@jdevalk/seo-graph-core';
|
|
3
|
+
export interface IndexNowKeyRouteOptions {
|
|
4
|
+
/** IndexNow key (8–128 hex characters). */
|
|
5
|
+
key: string;
|
|
6
|
+
/** Defaults to `public, max-age=86400`. Pass `null` to omit. */
|
|
7
|
+
cacheControl?: string | null;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Returns an Astro `APIRoute` that serves the IndexNow key verification
|
|
11
|
+
* file. Place this at `src/pages/[key].txt.ts` or `src/pages/<key>.txt.ts`
|
|
12
|
+
* so it resolves to `/<key>.txt` on the deployed site.
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* ```ts
|
|
16
|
+
* // src/pages/your-key-here.txt.ts
|
|
17
|
+
* import { createIndexNowKeyRoute } from '@jdevalk/astro-seo-graph';
|
|
18
|
+
*
|
|
19
|
+
* export const GET = createIndexNowKeyRoute({ key: 'your-key-here' });
|
|
20
|
+
* ```
|
|
21
|
+
*/
|
|
22
|
+
export declare function createIndexNowKeyRoute(options: IndexNowKeyRouteOptions): APIRoute;
|
|
23
|
+
export { submitToIndexNow, validateIndexNowKey };
|
|
24
|
+
export type { IndexNowSubmitResult };
|
|
25
|
+
//# sourceMappingURL=indexnow.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"indexnow.d.ts","sourceRoot":"","sources":["../src/indexnow.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAC;AACtC,OAAO,EAEH,gBAAgB,EAChB,mBAAmB,EACnB,KAAK,oBAAoB,EAC5B,MAAM,yBAAyB,CAAC;AAEjC,MAAM,WAAW,uBAAuB;IACpC,2CAA2C;IAC3C,GAAG,EAAE,MAAM,CAAC;IACZ,gEAAgE;IAChE,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAChC;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,sBAAsB,CAAC,OAAO,EAAE,uBAAuB,GAAG,QAAQ,CAYjF;AAED,OAAO,EAAE,gBAAgB,EAAE,mBAAmB,EAAE,CAAC;AACjD,YAAY,EAAE,oBAAoB,EAAE,CAAC"}
|
package/dist/indexnow.js
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { getIndexNowKeyFileContent, submitToIndexNow, validateIndexNowKey, } from '@jdevalk/seo-graph-core';
|
|
2
|
+
/**
|
|
3
|
+
* Returns an Astro `APIRoute` that serves the IndexNow key verification
|
|
4
|
+
* file. Place this at `src/pages/[key].txt.ts` or `src/pages/<key>.txt.ts`
|
|
5
|
+
* so it resolves to `/<key>.txt` on the deployed site.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```ts
|
|
9
|
+
* // src/pages/your-key-here.txt.ts
|
|
10
|
+
* import { createIndexNowKeyRoute } from '@jdevalk/astro-seo-graph';
|
|
11
|
+
*
|
|
12
|
+
* export const GET = createIndexNowKeyRoute({ key: 'your-key-here' });
|
|
13
|
+
* ```
|
|
14
|
+
*/
|
|
15
|
+
export function createIndexNowKeyRoute(options) {
|
|
16
|
+
const body = getIndexNowKeyFileContent(options.key);
|
|
17
|
+
const cacheControl = options.cacheControl === undefined ? 'public, max-age=86400' : options.cacheControl;
|
|
18
|
+
return async () => {
|
|
19
|
+
const headers = {
|
|
20
|
+
'Content-Type': 'text/plain; charset=utf-8',
|
|
21
|
+
};
|
|
22
|
+
if (cacheControl !== null)
|
|
23
|
+
headers['Cache-Control'] = cacheControl;
|
|
24
|
+
return new Response(body, { headers });
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
export { submitToIndexNow, validateIndexNowKey };
|
|
28
|
+
//# sourceMappingURL=indexnow.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"indexnow.js","sourceRoot":"","sources":["../src/indexnow.ts"],"names":[],"mappings":"AACA,OAAO,EACH,yBAAyB,EACzB,gBAAgB,EAChB,mBAAmB,GAEtB,MAAM,yBAAyB,CAAC;AASjC;;;;;;;;;;;;GAYG;AACH,MAAM,UAAU,sBAAsB,CAAC,OAAgC;IACnE,MAAM,IAAI,GAAG,yBAAyB,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;IACpD,MAAM,YAAY,GACd,OAAO,CAAC,YAAY,KAAK,SAAS,CAAC,CAAC,CAAC,uBAAuB,CAAC,CAAC,CAAC,OAAO,CAAC,YAAY,CAAC;IAExF,OAAO,KAAK,IAAI,EAAE;QACd,MAAM,OAAO,GAA2B;YACpC,cAAc,EAAE,2BAA2B;SAC9C,CAAC;QACF,IAAI,YAAY,KAAK,IAAI;YAAE,OAAO,CAAC,eAAe,CAAC,GAAG,YAAY,CAAC;QACnE,OAAO,IAAI,QAAQ,CAAC,IAAI,EAAE,EAAE,OAAO,EAAE,CAAC,CAAC;IAC3C,CAAC,CAAC;AACN,CAAC;AAED,OAAO,EAAE,gBAAgB,EAAE,mBAAmB,EAAE,CAAC"}
|
|
@@ -0,0 +1,88 @@
|
|
|
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 IndexNowIntegrationOptions {
|
|
16
|
+
/** IndexNow key (8–128 hex chars). Required to enable submission. */
|
|
17
|
+
key: string;
|
|
18
|
+
/** Bare host, e.g. `example.com`. */
|
|
19
|
+
host: string;
|
|
20
|
+
/**
|
|
21
|
+
* Absolute site origin used to resolve built HTML paths into URLs
|
|
22
|
+
* for submission. E.g. `https://example.com`. Trailing slash is
|
|
23
|
+
* tolerated.
|
|
24
|
+
*/
|
|
25
|
+
siteUrl: string;
|
|
26
|
+
/**
|
|
27
|
+
* Optional absolute URL to the key file. Defaults to
|
|
28
|
+
* `https://<host>/<key>.txt`.
|
|
29
|
+
*/
|
|
30
|
+
keyLocation?: string;
|
|
31
|
+
/**
|
|
32
|
+
* Override the IndexNow endpoint. Defaults to the neutral aggregator
|
|
33
|
+
* at `api.indexnow.org`.
|
|
34
|
+
*/
|
|
35
|
+
endpoint?: string;
|
|
36
|
+
/**
|
|
37
|
+
* Filter the list of URLs before submission. Return `false` to skip
|
|
38
|
+
* a URL. Useful for excluding 404 pages, drafts, etc.
|
|
39
|
+
*/
|
|
40
|
+
filter?: (url: string) => boolean;
|
|
41
|
+
}
|
|
42
|
+
export interface SeoGraphIntegrationOptions {
|
|
43
|
+
/**
|
|
44
|
+
* Warn when a built page has zero or more than one `<h1>` element.
|
|
45
|
+
* Defaults to `true`. Only static pages are checked (SSR pages are
|
|
46
|
+
* not present on disk at build time).
|
|
47
|
+
*/
|
|
48
|
+
validateH1?: boolean;
|
|
49
|
+
/**
|
|
50
|
+
* Submit built URLs to IndexNow after the build completes. Omit to
|
|
51
|
+
* disable. Only URLs on `host` are submitted; URLs with trailing
|
|
52
|
+
* `index.html` are rewritten to their directory form.
|
|
53
|
+
*/
|
|
54
|
+
indexNow?: IndexNowIntegrationOptions;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Turn a built HTML file path (relative to the outDir, e.g.
|
|
58
|
+
* `blog/post/index.html`) into an absolute URL on `siteUrl`. Rewrites
|
|
59
|
+
* `index.html` to a trailing slash and strips other `.html` extensions
|
|
60
|
+
* — matching Astro's default `trailingSlash: 'ignore'` output layout.
|
|
61
|
+
*/
|
|
62
|
+
export declare function htmlFileToUrl(relativePath: string, siteUrl: string): string;
|
|
63
|
+
/**
|
|
64
|
+
* Count `<h1>` elements in an HTML string. Matches the opening tag only;
|
|
65
|
+
* tolerant of attributes and whitespace. Doesn't parse — good enough for
|
|
66
|
+
* a lint warning.
|
|
67
|
+
*/
|
|
68
|
+
export declare function countH1s(html: string): number;
|
|
69
|
+
/**
|
|
70
|
+
* Astro integration for `@jdevalk/astro-seo-graph`.
|
|
71
|
+
*
|
|
72
|
+
* Currently:
|
|
73
|
+
* - Warns about built pages with zero or more than one `<h1>` element
|
|
74
|
+
* (a common SEO and accessibility issue).
|
|
75
|
+
*
|
|
76
|
+
* ```js
|
|
77
|
+
* // astro.config.mjs
|
|
78
|
+
* import { defineConfig } from 'astro/config';
|
|
79
|
+
* import seoGraph from '@jdevalk/astro-seo-graph/integration';
|
|
80
|
+
*
|
|
81
|
+
* export default defineConfig({
|
|
82
|
+
* integrations: [seoGraph()],
|
|
83
|
+
* });
|
|
84
|
+
* ```
|
|
85
|
+
*/
|
|
86
|
+
export default function seoGraph(options?: SeoGraphIntegrationOptions): AstroIntegrationLike;
|
|
87
|
+
export {};
|
|
88
|
+
//# sourceMappingURL=integration.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"integration.d.ts","sourceRoot":"","sources":["../src/integration.ts"],"names":[],"mappings":"AAQA,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,qEAAqE;IACrE,GAAG,EAAE,MAAM,CAAC;IACZ,qCAAqC;IACrC,IAAI,EAAE,MAAM,CAAC;IACb;;;;OAIG;IACH,OAAO,EAAE,MAAM,CAAC;IAChB;;;OAGG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;;OAGG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB;;;OAGG;IACH,MAAM,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC;CACrC;AAED,MAAM,WAAW,0BAA0B;IACvC;;;;OAIG;IACH,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB;;;;OAIG;IACH,QAAQ,CAAC,EAAE,0BAA0B,CAAC;CACzC;AAED;;;;;GAKG;AACH,wBAAgB,aAAa,CAAC,YAAY,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CAc3E;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,CAqE/F"}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { readFile, readdir } from 'node:fs/promises';
|
|
2
|
+
import { join, relative } from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
import { submitToIndexNow } from '@jdevalk/seo-graph-core';
|
|
5
|
+
/**
|
|
6
|
+
* Turn a built HTML file path (relative to the outDir, e.g.
|
|
7
|
+
* `blog/post/index.html`) into an absolute URL on `siteUrl`. Rewrites
|
|
8
|
+
* `index.html` to a trailing slash and strips other `.html` extensions
|
|
9
|
+
* — matching Astro's default `trailingSlash: 'ignore'` output layout.
|
|
10
|
+
*/
|
|
11
|
+
export function htmlFileToUrl(relativePath, siteUrl) {
|
|
12
|
+
const origin = siteUrl.replace(/\/+$/, '');
|
|
13
|
+
const normalized = relativePath.split(/[\\/]/).join('/');
|
|
14
|
+
let pathname;
|
|
15
|
+
if (normalized === 'index.html' || normalized === '/index.html') {
|
|
16
|
+
pathname = '/';
|
|
17
|
+
}
|
|
18
|
+
else if (normalized.endsWith('/index.html')) {
|
|
19
|
+
pathname = '/' + normalized.slice(0, -'index.html'.length);
|
|
20
|
+
}
|
|
21
|
+
else if (normalized.endsWith('.html')) {
|
|
22
|
+
pathname = '/' + normalized.slice(0, -'.html'.length);
|
|
23
|
+
}
|
|
24
|
+
else {
|
|
25
|
+
pathname = '/' + normalized;
|
|
26
|
+
}
|
|
27
|
+
return `${origin}${pathname}`;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Count `<h1>` elements in an HTML string. Matches the opening tag only;
|
|
31
|
+
* tolerant of attributes and whitespace. Doesn't parse — good enough for
|
|
32
|
+
* a lint warning.
|
|
33
|
+
*/
|
|
34
|
+
export function countH1s(html) {
|
|
35
|
+
const matches = html.match(/<h1[\s>]/gi);
|
|
36
|
+
return matches ? matches.length : 0;
|
|
37
|
+
}
|
|
38
|
+
async function collectHtmlFiles(dir, base = dir) {
|
|
39
|
+
const entries = await readdir(dir, { withFileTypes: true });
|
|
40
|
+
const files = [];
|
|
41
|
+
for (const entry of entries) {
|
|
42
|
+
const fullPath = join(dir, entry.name);
|
|
43
|
+
if (entry.isDirectory()) {
|
|
44
|
+
files.push(...(await collectHtmlFiles(fullPath, base)));
|
|
45
|
+
}
|
|
46
|
+
else if (entry.isFile() && entry.name.endsWith('.html')) {
|
|
47
|
+
files.push(relative(base, fullPath));
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return files;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Astro integration for `@jdevalk/astro-seo-graph`.
|
|
54
|
+
*
|
|
55
|
+
* Currently:
|
|
56
|
+
* - Warns about built pages with zero or more than one `<h1>` element
|
|
57
|
+
* (a common SEO and accessibility issue).
|
|
58
|
+
*
|
|
59
|
+
* ```js
|
|
60
|
+
* // astro.config.mjs
|
|
61
|
+
* import { defineConfig } from 'astro/config';
|
|
62
|
+
* import seoGraph from '@jdevalk/astro-seo-graph/integration';
|
|
63
|
+
*
|
|
64
|
+
* export default defineConfig({
|
|
65
|
+
* integrations: [seoGraph()],
|
|
66
|
+
* });
|
|
67
|
+
* ```
|
|
68
|
+
*/
|
|
69
|
+
export default function seoGraph(options = {}) {
|
|
70
|
+
const { validateH1 = true, indexNow } = options;
|
|
71
|
+
return {
|
|
72
|
+
name: '@jdevalk/astro-seo-graph',
|
|
73
|
+
hooks: {
|
|
74
|
+
'astro:build:done': async ({ dir, logger }) => {
|
|
75
|
+
const buildDir = fileURLToPath(dir);
|
|
76
|
+
const htmlFiles = validateH1 || indexNow ? await collectHtmlFiles(buildDir) : [];
|
|
77
|
+
if (validateH1) {
|
|
78
|
+
const missing = [];
|
|
79
|
+
const multiple = [];
|
|
80
|
+
for (const file of htmlFiles) {
|
|
81
|
+
const content = await readFile(join(buildDir, file), 'utf8');
|
|
82
|
+
const count = countH1s(content);
|
|
83
|
+
if (count === 0)
|
|
84
|
+
missing.push(file);
|
|
85
|
+
else if (count > 1)
|
|
86
|
+
multiple.push({ file, count });
|
|
87
|
+
}
|
|
88
|
+
if (missing.length === 0 && multiple.length === 0) {
|
|
89
|
+
logger.info(`H1 validation: ${htmlFiles.length} pages checked, all good.`);
|
|
90
|
+
}
|
|
91
|
+
else {
|
|
92
|
+
for (const file of missing) {
|
|
93
|
+
logger.warn(`H1 validation: ${file} has no <h1>.`);
|
|
94
|
+
}
|
|
95
|
+
for (const { file, count } of multiple) {
|
|
96
|
+
logger.warn(`H1 validation: ${file} has ${count} <h1> elements (expected 1).`);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
if (indexNow) {
|
|
101
|
+
const urls = htmlFiles
|
|
102
|
+
.map((f) => htmlFileToUrl(f, indexNow.siteUrl))
|
|
103
|
+
.filter((u) => (indexNow.filter ? indexNow.filter(u) : true));
|
|
104
|
+
if (urls.length === 0) {
|
|
105
|
+
logger.info('IndexNow: no URLs to submit.');
|
|
106
|
+
}
|
|
107
|
+
else {
|
|
108
|
+
const results = await submitToIndexNow({
|
|
109
|
+
host: indexNow.host,
|
|
110
|
+
key: indexNow.key,
|
|
111
|
+
keyLocation: indexNow.keyLocation,
|
|
112
|
+
endpoint: indexNow.endpoint,
|
|
113
|
+
urls,
|
|
114
|
+
});
|
|
115
|
+
for (const r of results) {
|
|
116
|
+
if (r.ok) {
|
|
117
|
+
logger.info(`IndexNow: submitted ${r.submitted} URLs (status ${r.status}).`);
|
|
118
|
+
}
|
|
119
|
+
else {
|
|
120
|
+
logger.warn(`IndexNow: submission failed (status ${r.status}): ${r.message}`);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
},
|
|
126
|
+
},
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
//# 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.7.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",
|
|
@@ -48,7 +52,7 @@
|
|
|
48
52
|
"astro-seo": "^1.1.0",
|
|
49
53
|
"schema-dts": "^2.0.0",
|
|
50
54
|
"zod": "^3.24.0",
|
|
51
|
-
"@jdevalk/seo-graph-core": "0.
|
|
55
|
+
"@jdevalk/seo-graph-core": "0.6.0"
|
|
52
56
|
},
|
|
53
57
|
"devDependencies": {
|
|
54
58
|
"@types/node": "^22.0.0",
|