@jdevalk/astro-seo-graph 0.9.0 → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +35 -17
- package/README.md +29 -9
- package/dist/components/Seo.astro +117 -6
- package/dist/components/seo-context.d.ts +105 -0
- package/dist/components/seo-context.d.ts.map +1 -0
- package/dist/components/seo-context.js +189 -0
- package/dist/components/seo-context.js.map +1 -0
- package/dist/components/seo-props.d.ts +4 -67
- package/dist/components/seo-props.d.ts.map +1 -1
- package/dist/components/seo-props.js +4 -233
- package/dist/components/seo-props.js.map +1 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/integration.d.ts +18 -1
- package/dist/integration.d.ts.map +1 -1
- package/dist/integration.js +26 -6
- package/dist/integration.js.map +1 -1
- package/package.json +2 -3
package/AGENTS.md
CHANGED
|
@@ -27,12 +27,14 @@ Two packages:
|
|
|
27
27
|
## Contents
|
|
28
28
|
|
|
29
29
|
**Schema core** — concepts and builders for the JSON-LD graph:
|
|
30
|
+
|
|
30
31
|
- [Architecture](#architecture)
|
|
31
32
|
- [Installation](#installation)
|
|
32
33
|
- [The @id system](#the-id-system)
|
|
33
34
|
- [Piece builders reference](#piece-builders-reference)
|
|
34
35
|
|
|
35
36
|
**Recipes and patterns** — how to model real sites:
|
|
37
|
+
|
|
36
38
|
- [Site type recipes](#site-type-recipes)
|
|
37
39
|
- [Trust and credibility signals](#trust-and-credibility-signals)
|
|
38
40
|
- [Choosing the right Article subtype](#choosing-the-right-article-subtype)
|
|
@@ -43,6 +45,7 @@ Two packages:
|
|
|
43
45
|
- [Reference implementations](#reference-implementations)
|
|
44
46
|
|
|
45
47
|
**Astro integration** — runtime component and build-time checks:
|
|
48
|
+
|
|
46
49
|
- [Astro integration guide](#astro-integration-guide) — the `<Seo>` component, hreflang, schema endpoints
|
|
47
50
|
- [Build-time integration](#build-time-integration) — `seoGraph()` hook, H1 validation, metadata uniqueness, IndexNow, `llms.txt`
|
|
48
51
|
- [Complete integration example](#complete-integration-example)
|
|
@@ -2573,18 +2576,24 @@ const blog = defineCollection({
|
|
|
2573
2576
|
}
|
|
2574
2577
|
```
|
|
2575
2578
|
|
|
2576
|
-
### `
|
|
2579
|
+
### `buildSeoContext`
|
|
2577
2580
|
|
|
2578
|
-
Pure-TS function that powers `<Seo>` internally.
|
|
2579
|
-
|
|
2581
|
+
Pure-TS function that powers `<Seo>` internally. Returns a flat,
|
|
2582
|
+
render-ready normalization of `SeoProps` (resolved title, canonical, OG
|
|
2583
|
+
fields, hreflang entries, robots directives, twitter overrides). Use when
|
|
2584
|
+
you want to render the head yourself:
|
|
2580
2585
|
|
|
2581
2586
|
```ts
|
|
2582
|
-
import {
|
|
2587
|
+
import { buildSeoContext } from '@jdevalk/astro-seo-graph';
|
|
2583
2588
|
|
|
2584
|
-
const
|
|
2585
|
-
//
|
|
2589
|
+
const ctx = buildSeoContext(mySeoProps, Astro.url.href);
|
|
2590
|
+
// ctx.title, ctx.canonical, ctx.og.*, ctx.twitter, ctx.hreflangs, ...
|
|
2586
2591
|
```
|
|
2587
2592
|
|
|
2593
|
+
The companion constant `ROBOTS_EXTRAS` exposes the
|
|
2594
|
+
`max-snippet:-1, max-image-preview:large, max-video-preview:-1`
|
|
2595
|
+
directives that `<Seo>` always appends to the robots tag.
|
|
2596
|
+
|
|
2588
2597
|
### `buildAlternateLinks`
|
|
2589
2598
|
|
|
2590
2599
|
Pure helper for hreflang link generation. No Astro runtime needed — safe
|
|
@@ -2634,12 +2643,12 @@ export default defineConfig({
|
|
|
2634
2643
|
|
|
2635
2644
|
### Options
|
|
2636
2645
|
|
|
2637
|
-
| Option | Default | Purpose
|
|
2638
|
-
| ------------------------ | ------- |
|
|
2639
|
-
| `validateH1` | `true` | Warn when a page has zero or >1 `<h1>` elements.
|
|
2640
|
-
| `validateUniqueMetadata` | `true` | Warn when two pages share the same `<title>` or meta description.
|
|
2641
|
-
| `indexNow` | — | Submit built URLs to IndexNow. Omit to disable.
|
|
2642
|
-
| `llmsTxt` | — | Generate `llms.txt` at the root of the build output. Omit to disable.
|
|
2646
|
+
| Option | Default | Purpose |
|
|
2647
|
+
| ------------------------ | ------- | --------------------------------------------------------------------- |
|
|
2648
|
+
| `validateH1` | `true` | Warn when a page has zero or >1 `<h1>` elements. |
|
|
2649
|
+
| `validateUniqueMetadata` | `true` | Warn when two pages share the same `<title>` or meta description. |
|
|
2650
|
+
| `indexNow` | — | Submit built URLs to IndexNow. Omit to disable. |
|
|
2651
|
+
| `llmsTxt` | — | Generate `llms.txt` at the root of the build output. Omit to disable. |
|
|
2643
2652
|
|
|
2644
2653
|
### H1 and metadata validation
|
|
2645
2654
|
|
|
@@ -2652,7 +2661,11 @@ can only be spotted across the whole corpus.
|
|
|
2652
2661
|
Both extractors are exported for reuse:
|
|
2653
2662
|
|
|
2654
2663
|
```ts
|
|
2655
|
-
import {
|
|
2664
|
+
import {
|
|
2665
|
+
countH1s,
|
|
2666
|
+
extractTitle,
|
|
2667
|
+
extractMetaDescription,
|
|
2668
|
+
} from '@jdevalk/astro-seo-graph/integration';
|
|
2656
2669
|
```
|
|
2657
2670
|
|
|
2658
2671
|
### IndexNow submission
|
|
@@ -2665,20 +2678,24 @@ seoGraph({
|
|
|
2665
2678
|
siteUrl: 'https://example.com',
|
|
2666
2679
|
// keyLocation?: defaults to https://<host>/<key>.txt
|
|
2667
2680
|
// endpoint?: defaults to api.indexnow.org
|
|
2668
|
-
// filter?: (url) => boolean —
|
|
2681
|
+
// filter?: (url) => boolean — composed on top of the built-in
|
|
2682
|
+
// /404 exclusion. Example skipping paginated archives:
|
|
2683
|
+
// filter: (url) => !/^\/blog\/\d+\/$/.test(new URL(url).pathname),
|
|
2669
2684
|
},
|
|
2670
2685
|
});
|
|
2671
2686
|
```
|
|
2672
2687
|
|
|
2673
2688
|
Only URLs on `host` are submitted. `index.html` paths are rewritten to
|
|
2674
|
-
their trailing-slash form.
|
|
2689
|
+
their trailing-slash form. `/404` (and `/404/`) are always excluded —
|
|
2690
|
+
search engines don't need to be notified about the 404 page and
|
|
2691
|
+
submitting it wastes daily IndexNow quota.
|
|
2675
2692
|
|
|
2676
2693
|
**Deploy the key file first.** IndexNow verifies ownership by fetching
|
|
2677
2694
|
`https://<host>/<key>.txt` on every submission. Submissions sent before
|
|
2678
2695
|
the key is reachable in production get rejected (HTTP 403) and the key
|
|
2679
2696
|
is treated as invalid — you'll have to rotate it. Serve the key via
|
|
2680
2697
|
`createIndexNowKeyRoute` (see below), deploy, confirm the `.txt` loads
|
|
2681
|
-
over HTTPS,
|
|
2698
|
+
over HTTPS, _then_ enable `indexNow` in the integration.
|
|
2682
2699
|
|
|
2683
2700
|
```ts
|
|
2684
2701
|
// src/pages/[your-key-here].txt.ts
|
|
@@ -3262,7 +3279,8 @@ seo-graph/
|
|
|
3262
3279
|
│ ├── alternates.ts # buildAlternateLinks
|
|
3263
3280
|
│ ├── content.ts # seoSchema, imageSchema
|
|
3264
3281
|
│ └── components/
|
|
3265
|
-
│
|
|
3282
|
+
│ ├── seo-props.ts # SeoProps interface
|
|
3283
|
+
│ └── seo-context.ts # buildSeoContext
|
|
3266
3284
|
├── AGENTS.md # This file
|
|
3267
3285
|
├── README.md # Project overview
|
|
3268
3286
|
└── pnpm-workspace.yaml
|
package/README.md
CHANGED
|
@@ -11,6 +11,11 @@ component, and Zod helpers for content schemas.
|
|
|
11
11
|
For detailed usage — including all builder signatures, site-type recipes, and
|
|
12
12
|
schema.org best practices — see [AGENTS.md](https://github.com/jdevalk/seo-graph/blob/main/AGENTS.md).
|
|
13
13
|
|
|
14
|
+
> **Using an AI coding assistant?** The [`astro-seo` skill](https://github.com/jdevalk/skills/tree/main/astro-seo)
|
|
15
|
+
> audits and improves the full SEO setup of an Astro site — technical
|
|
16
|
+
> foundation, structured data, sitemaps, IndexNow, agent discovery, and
|
|
17
|
+
> more — and produces drop-in code routed through this package.
|
|
18
|
+
|
|
14
19
|
## What you get
|
|
15
20
|
|
|
16
21
|
| API | Purpose |
|
|
@@ -20,11 +25,11 @@ schema.org best practices — see [AGENTS.md](https://github.com/jdevalk/seo-gra
|
|
|
20
25
|
| **`createSchemaMap`** | Factory returning an `APIRoute` handler that emits a sitemap-style XML listing of your site's schema endpoints — the discovery point for agent crawlers. |
|
|
21
26
|
| **`aggregate`** | Shared engine behind the endpoint factories. Walks a list of entries, runs a caller-supplied mapper, deduplicates by `@id`. |
|
|
22
27
|
| **`seoSchema`, `imageSchema`** | Zod schemas for the `seo` and `image` fields on content collections. Import them into `src/content.config.ts`. |
|
|
23
|
-
| **`
|
|
28
|
+
| **`buildSeoContext`** | Pure-TS logic that powers `<Seo>` — returns a flat, normalized projection of `SeoProps` (resolved title, canonical, OG fields, hreflang entries, robots directives). Exported for users who want to render the head themselves. |
|
|
24
29
|
| **`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
30
|
| **`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
31
|
| **`<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`.
|
|
32
|
+
| **`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`. |
|
|
28
33
|
|
|
29
34
|
## Installation
|
|
30
35
|
|
|
@@ -366,17 +371,32 @@ export default defineConfig({
|
|
|
366
371
|
|
|
367
372
|
Options:
|
|
368
373
|
|
|
369
|
-
| Prop | Default | Description
|
|
370
|
-
| ------------------------ | ------- |
|
|
371
|
-
| `validateH1` | `true` | Warn about pages without exactly one `<h1>`
|
|
372
|
-
| `validateUniqueMetadata` | `true` | Warn about duplicate `<title>` or meta description across pages
|
|
373
|
-
| `indexNow` | — | Submit built URLs to IndexNow. See below for sub-options.
|
|
374
|
+
| Prop | Default | Description |
|
|
375
|
+
| ------------------------ | ------- | ----------------------------------------------------------------- |
|
|
376
|
+
| `validateH1` | `true` | Warn about pages without exactly one `<h1>` |
|
|
377
|
+
| `validateUniqueMetadata` | `true` | Warn about duplicate `<title>` or meta description across pages |
|
|
378
|
+
| `indexNow` | — | Submit built URLs to IndexNow. See below for sub-options. |
|
|
374
379
|
| `llmsTxt` | — | Generate `llms.txt` at the build root. See below for sub-options. |
|
|
375
380
|
|
|
376
381
|
`indexNow` sub-options: `key` (8–128 hex chars), `host` (bare host, e.g.
|
|
377
382
|
`example.com`), `siteUrl` (absolute origin), `keyLocation?` (defaults to
|
|
378
383
|
`https://<host>/<key>.txt`), `endpoint?` (defaults to `api.indexnow.org`),
|
|
379
|
-
`filter?` (drop URLs for which the callback returns `false
|
|
384
|
+
`filter?` (drop URLs for which the callback returns `false`; composed on
|
|
385
|
+
top of the built-in `/404` exclusion).
|
|
386
|
+
|
|
387
|
+
The `/404` page is always excluded — no one needs search engines notified
|
|
388
|
+
about it, and submitting it wastes daily IndexNow quota. Use `filter` to
|
|
389
|
+
exclude site-specific utility pages:
|
|
390
|
+
|
|
391
|
+
```js
|
|
392
|
+
indexNow: {
|
|
393
|
+
key: process.env.INDEXNOW_KEY!,
|
|
394
|
+
host: 'example.com',
|
|
395
|
+
siteUrl: 'https://example.com',
|
|
396
|
+
// Skip paginated archives like /blog/2/, /videos/3/
|
|
397
|
+
filter: (url) => !/^\/(?:blog|videos)\/\d+\/$/.test(new URL(url).pathname),
|
|
398
|
+
},
|
|
399
|
+
```
|
|
380
400
|
|
|
381
401
|
`llmsTxt` sub-options: `title` (required H1), `siteUrl` (required, used to
|
|
382
402
|
resolve crawled HTML paths), `summary?` (rendered as a blockquote),
|
|
@@ -409,7 +429,7 @@ own deploy hook.
|
|
|
409
429
|
> sent before the key file is reachable in production get rejected
|
|
410
430
|
> (HTTP 403) and the key is treated as invalid going forward — you'll
|
|
411
431
|
> have to rotate it. Ship the route, deploy, confirm the `.txt` loads
|
|
412
|
-
> over HTTPS,
|
|
432
|
+
> over HTTPS, _then_ enable `indexNow` in the integration.
|
|
413
433
|
|
|
414
434
|
## Validating your output
|
|
415
435
|
|
|
@@ -1,14 +1,125 @@
|
|
|
1
1
|
---
|
|
2
|
-
|
|
3
|
-
|
|
2
|
+
/**
|
|
3
|
+
* First-party <Seo> component. Consumes the flat `SeoContext` from
|
|
4
|
+
* `buildSeoContext()` and emits every `<head>` tag directly. No external
|
|
5
|
+
* `astro-seo` dependency.
|
|
6
|
+
*
|
|
7
|
+
* Tag order:
|
|
8
|
+
* 1. <title>
|
|
9
|
+
* 2. <link rel="canonical"> (omitted when noindex)
|
|
10
|
+
* 3. <meta name="description">
|
|
11
|
+
* 4. <meta name="robots"> (single merged tag)
|
|
12
|
+
* 5. og:title/type/image/url
|
|
13
|
+
* 6. og:description/site_name/locale/locale:alternate
|
|
14
|
+
* 7. og:image:alt/width/height (when og:image is set)
|
|
15
|
+
* 8. article:published_time/etc. (when ogType === 'article')
|
|
16
|
+
* 9. twitter:card/site/creator/...overrides
|
|
17
|
+
* 10. <link rel="alternate" hreflang> (incl. x-default)
|
|
18
|
+
* 11. <meta name="author">
|
|
19
|
+
* 12. extraLinks passthrough
|
|
20
|
+
* 13. extraMeta passthrough
|
|
21
|
+
* 14. <script type="application/ld+json"> (when graph is truthy)
|
|
22
|
+
*/
|
|
23
|
+
import { buildSeoContext } from './seo-context.js';
|
|
24
|
+
import type { SeoProps } from './seo-props.js';
|
|
4
25
|
|
|
5
26
|
const props = Astro.props as SeoProps;
|
|
6
|
-
const
|
|
27
|
+
const ctx = buildSeoContext(props, Astro.url.href);
|
|
28
|
+
|
|
29
|
+
function robotsContent(r: typeof ctx.robots): string {
|
|
30
|
+
const parts: string[] = [];
|
|
31
|
+
parts.push(r.noindex ? 'noindex' : 'index');
|
|
32
|
+
parts.push(r.nofollow ? 'nofollow' : 'follow');
|
|
33
|
+
if (r.extras) parts.push(r.extras);
|
|
34
|
+
return parts.join(', ');
|
|
35
|
+
}
|
|
7
36
|
---
|
|
8
37
|
|
|
9
|
-
<
|
|
38
|
+
<title set:html={ctx.title} />
|
|
39
|
+
|
|
40
|
+
{ctx.canonical && <link rel="canonical" href={ctx.canonical} />}
|
|
41
|
+
|
|
42
|
+
{ctx.description && <meta name="description" content={ctx.description} />}
|
|
43
|
+
|
|
44
|
+
<meta name="robots" content={robotsContent(ctx.robots)} />
|
|
45
|
+
|
|
46
|
+
<meta property="og:title" content={ctx.og.title} />
|
|
47
|
+
<meta property="og:type" content={ctx.og.type} />
|
|
48
|
+
<meta property="og:image" content={ctx.og.image} />
|
|
49
|
+
<meta property="og:url" content={ctx.og.url} />
|
|
50
|
+
|
|
51
|
+
{ctx.og.description && <meta property="og:description" content={ctx.og.description} />}
|
|
52
|
+
{ctx.og.siteName && <meta property="og:site_name" content={ctx.og.siteName} />}
|
|
53
|
+
<meta property="og:locale" content={ctx.og.locale} />
|
|
54
|
+
{
|
|
55
|
+
ctx.og.localeAlternate?.map((entry) => (
|
|
56
|
+
<meta property="og:locale:alternate" content={entry} />
|
|
57
|
+
))
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
{ctx.og.imageAlt && <meta property="og:image:alt" content={ctx.og.imageAlt} />}
|
|
61
|
+
{
|
|
62
|
+
ctx.og.imageWidth !== undefined && (
|
|
63
|
+
<meta property="og:image:width" content={String(ctx.og.imageWidth)} />
|
|
64
|
+
)
|
|
65
|
+
}
|
|
10
66
|
{
|
|
11
|
-
|
|
12
|
-
<
|
|
67
|
+
ctx.og.imageHeight !== undefined && (
|
|
68
|
+
<meta property="og:image:height" content={String(ctx.og.imageHeight)} />
|
|
13
69
|
)
|
|
14
70
|
}
|
|
71
|
+
|
|
72
|
+
{
|
|
73
|
+
ctx.og.article && (
|
|
74
|
+
<>
|
|
75
|
+
{ctx.og.article.publishedTime && (
|
|
76
|
+
<meta property="article:published_time" content={ctx.og.article.publishedTime} />
|
|
77
|
+
)}
|
|
78
|
+
{ctx.og.article.modifiedTime && (
|
|
79
|
+
<meta property="article:modified_time" content={ctx.og.article.modifiedTime} />
|
|
80
|
+
)}
|
|
81
|
+
{ctx.og.article.expirationTime && (
|
|
82
|
+
<meta property="article:expiration_time" content={ctx.og.article.expirationTime} />
|
|
83
|
+
)}
|
|
84
|
+
{ctx.og.article.authors?.map((author) => (
|
|
85
|
+
<meta property="article:author" content={author} />
|
|
86
|
+
))}
|
|
87
|
+
{ctx.og.article.tags?.map((tag) => <meta property="article:tag" content={tag} />)}
|
|
88
|
+
{ctx.og.article.section && (
|
|
89
|
+
<meta property="article:section" content={ctx.og.article.section} />
|
|
90
|
+
)}
|
|
91
|
+
{ctx.og.article.publisher && (
|
|
92
|
+
<meta property="article:publisher" content={ctx.og.article.publisher} />
|
|
93
|
+
)}
|
|
94
|
+
</>
|
|
95
|
+
)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
{
|
|
99
|
+
ctx.twitter && (
|
|
100
|
+
<>
|
|
101
|
+
<meta name="twitter:card" content={ctx.twitter.card} />
|
|
102
|
+
{ctx.twitter.site && <meta name="twitter:site" content={ctx.twitter.site} />}
|
|
103
|
+
{ctx.twitter.creator && (
|
|
104
|
+
<meta name="twitter:creator" content={ctx.twitter.creator} />
|
|
105
|
+
)}
|
|
106
|
+
{ctx.twitter.title && <meta name="twitter:title" content={ctx.twitter.title} />}
|
|
107
|
+
{ctx.twitter.description && (
|
|
108
|
+
<meta name="twitter:description" content={ctx.twitter.description} />
|
|
109
|
+
)}
|
|
110
|
+
{ctx.twitter.image && <meta name="twitter:image" content={ctx.twitter.image} />}
|
|
111
|
+
{ctx.twitter.imageAlt && (
|
|
112
|
+
<meta name="twitter:image:alt" content={ctx.twitter.imageAlt} />
|
|
113
|
+
)}
|
|
114
|
+
</>
|
|
115
|
+
)
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
{ctx.hreflangs.map((entry) => <link rel="alternate" hreflang={entry.hreflang} href={entry.href} />)}
|
|
119
|
+
|
|
120
|
+
{ctx.authorName && <meta name="author" content={ctx.authorName} />}
|
|
121
|
+
|
|
122
|
+
{ctx.extraLinks.map((entry) => <link {...entry} />)}
|
|
123
|
+
{ctx.extraMeta.map((entry) => <meta {...entry} />)}
|
|
124
|
+
|
|
125
|
+
{ctx.graph && <script type="application/ld+json" set:html={JSON.stringify(ctx.graph)} />}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Normalized, flat, render-ready projection of `SeoProps`. The `<Seo>`
|
|
3
|
+
* template consumes this directly — no further reshaping, no
|
|
4
|
+
* conditionals beyond "field present?". Keeping normalization here (pure
|
|
5
|
+
* TypeScript) means vitest can assert on every decision without touching
|
|
6
|
+
* Astro's renderer.
|
|
7
|
+
*/
|
|
8
|
+
import { type AlternateLink } from '../alternates.js';
|
|
9
|
+
import type { SeoProps } from './seo-props.js';
|
|
10
|
+
/**
|
|
11
|
+
* Opted-in robots directives for maximum snippet and preview sizes in
|
|
12
|
+
* search results. Appended after any `noindex`/`nofollow` prefix.
|
|
13
|
+
*/
|
|
14
|
+
export declare const ROBOTS_EXTRAS = "max-snippet:-1, max-image-preview:large, max-video-preview:-1";
|
|
15
|
+
/**
|
|
16
|
+
* Render-ready normalization of `SeoProps`. Every field has been resolved
|
|
17
|
+
* against defaults, every `Date` coerced to ISO string, every optional
|
|
18
|
+
* block either populated or omitted. Templates just iterate.
|
|
19
|
+
*/
|
|
20
|
+
export interface SeoContext {
|
|
21
|
+
/** Final `<title>` text, with `titleTemplate` already applied. */
|
|
22
|
+
title: string;
|
|
23
|
+
/** Meta description. Omitted from output when undefined. */
|
|
24
|
+
description?: string;
|
|
25
|
+
/**
|
|
26
|
+
* Canonical URL. Already query-stripped (or preserved) per
|
|
27
|
+
* `preserveQueryParams`. `undefined` when the page is `noindex` —
|
|
28
|
+
* callers must not emit a canonical in that case.
|
|
29
|
+
*/
|
|
30
|
+
canonical?: string;
|
|
31
|
+
/** Resolved robots directives. Always present (extras always apply). */
|
|
32
|
+
robots: {
|
|
33
|
+
noindex: boolean;
|
|
34
|
+
nofollow: boolean;
|
|
35
|
+
/** Directives beyond noindex/nofollow — typically `ROBOTS_EXTRAS`. */
|
|
36
|
+
extras: string;
|
|
37
|
+
};
|
|
38
|
+
/**
|
|
39
|
+
* Open Graph fields. `title`, `type`, `url` are always populated.
|
|
40
|
+
* `image` may be an empty string when no share image was provided.
|
|
41
|
+
*/
|
|
42
|
+
og: {
|
|
43
|
+
title: string;
|
|
44
|
+
type: string;
|
|
45
|
+
image: string;
|
|
46
|
+
url: string;
|
|
47
|
+
description?: string;
|
|
48
|
+
siteName?: string;
|
|
49
|
+
locale: string;
|
|
50
|
+
localeAlternate?: readonly string[];
|
|
51
|
+
imageAlt?: string;
|
|
52
|
+
imageWidth?: number;
|
|
53
|
+
imageHeight?: number;
|
|
54
|
+
article?: {
|
|
55
|
+
publishedTime?: string;
|
|
56
|
+
modifiedTime?: string;
|
|
57
|
+
expirationTime?: string;
|
|
58
|
+
authors?: readonly string[];
|
|
59
|
+
tags?: readonly string[];
|
|
60
|
+
section?: string;
|
|
61
|
+
publisher?: string;
|
|
62
|
+
};
|
|
63
|
+
};
|
|
64
|
+
/** Twitter tags. Omitted entirely when the caller passed no `twitter` block. */
|
|
65
|
+
twitter?: {
|
|
66
|
+
card: 'summary' | 'summary_large_image' | 'app' | 'player';
|
|
67
|
+
site?: string;
|
|
68
|
+
creator?: string;
|
|
69
|
+
title?: string;
|
|
70
|
+
description?: string;
|
|
71
|
+
image?: string;
|
|
72
|
+
imageAlt?: string;
|
|
73
|
+
};
|
|
74
|
+
/**
|
|
75
|
+
* hreflang alternate entries (including `x-default`). Empty array when
|
|
76
|
+
* the caller passed no `alternates`, or when fewer than 2 entries
|
|
77
|
+
* survive validation.
|
|
78
|
+
*/
|
|
79
|
+
hreflangs: readonly AlternateLink[];
|
|
80
|
+
/** Resolved author name for `<meta name="author">`. */
|
|
81
|
+
authorName?: string;
|
|
82
|
+
/** Passthrough extras. Empty arrays when not provided. */
|
|
83
|
+
extraLinks: ReadonlyArray<Record<string, string>>;
|
|
84
|
+
extraMeta: ReadonlyArray<Record<string, string>>;
|
|
85
|
+
/** JSON-LD graph to inject. `null`/`undefined` → skip the script tag. */
|
|
86
|
+
graph?: Record<string, unknown> | null;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Build the flat `SeoContext` from user-provided `SeoProps` and the
|
|
90
|
+
* current page URL (usually `Astro.url.href`). Pure function; no Astro
|
|
91
|
+
* runtime access.
|
|
92
|
+
*
|
|
93
|
+
* Invariants worth knowing:
|
|
94
|
+
* - `canonical` is omitted when `noindex` is true (Google recommendation).
|
|
95
|
+
* - `og:url` falls back to the query-stripped page URL when canonical
|
|
96
|
+
* is omitted, so share previews stay stable even on noindex pages.
|
|
97
|
+
* - Twitter override fields are suppressed when they equal their OG
|
|
98
|
+
* counterparts (Twitter falls back to OG automatically, so emitting
|
|
99
|
+
* the duplicate is noise).
|
|
100
|
+
* - `og:locale:alternate` is derived from `alternates` by converting
|
|
101
|
+
* hyphenated BCP 47 to underscore form and skipping entries that
|
|
102
|
+
* match the primary locale.
|
|
103
|
+
*/
|
|
104
|
+
export declare function buildSeoContext(props: SeoProps, pageUrl: string): SeoContext;
|
|
105
|
+
//# sourceMappingURL=seo-context.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"seo-context.d.ts","sourceRoot":"","sources":["../../src/components/seo-context.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAuB,KAAK,aAAa,EAAE,MAAM,kBAAkB,CAAC;AAC3E,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAE/C;;;GAGG;AACH,eAAO,MAAM,aAAa,kEAAkE,CAAC;AAE7F;;;;GAIG;AACH,MAAM,WAAW,UAAU;IACvB,kEAAkE;IAClE,KAAK,EAAE,MAAM,CAAC;IACd,4DAA4D;IAC5D,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;;;OAIG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,wEAAwE;IACxE,MAAM,EAAE;QACJ,OAAO,EAAE,OAAO,CAAC;QACjB,QAAQ,EAAE,OAAO,CAAC;QAClB,sEAAsE;QACtE,MAAM,EAAE,MAAM,CAAC;KAClB,CAAC;IACF;;;OAGG;IACH,EAAE,EAAE;QACA,KAAK,EAAE,MAAM,CAAC;QACd,IAAI,EAAE,MAAM,CAAC;QACb,KAAK,EAAE,MAAM,CAAC;QACd,GAAG,EAAE,MAAM,CAAC;QACZ,WAAW,CAAC,EAAE,MAAM,CAAC;QACrB,QAAQ,CAAC,EAAE,MAAM,CAAC;QAClB,MAAM,EAAE,MAAM,CAAC;QACf,eAAe,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;QACpC,QAAQ,CAAC,EAAE,MAAM,CAAC;QAClB,UAAU,CAAC,EAAE,MAAM,CAAC;QACpB,WAAW,CAAC,EAAE,MAAM,CAAC;QACrB,OAAO,CAAC,EAAE;YACN,aAAa,CAAC,EAAE,MAAM,CAAC;YACvB,YAAY,CAAC,EAAE,MAAM,CAAC;YACtB,cAAc,CAAC,EAAE,MAAM,CAAC;YACxB,OAAO,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;YAC5B,IAAI,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;YACzB,OAAO,CAAC,EAAE,MAAM,CAAC;YACjB,SAAS,CAAC,EAAE,MAAM,CAAC;SACtB,CAAC;KACL,CAAC;IACF,gFAAgF;IAChF,OAAO,CAAC,EAAE;QACN,IAAI,EAAE,SAAS,GAAG,qBAAqB,GAAG,KAAK,GAAG,QAAQ,CAAC;QAC3D,IAAI,CAAC,EAAE,MAAM,CAAC;QACd,OAAO,CAAC,EAAE,MAAM,CAAC;QACjB,KAAK,CAAC,EAAE,MAAM,CAAC;QACf,WAAW,CAAC,EAAE,MAAM,CAAC;QACrB,KAAK,CAAC,EAAE,MAAM,CAAC;QACf,QAAQ,CAAC,EAAE,MAAM,CAAC;KACrB,CAAC;IACF;;;;OAIG;IACH,SAAS,EAAE,SAAS,aAAa,EAAE,CAAC;IACpC,uDAAuD;IACvD,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,0DAA0D;IAC1D,UAAU,EAAE,aAAa,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;IAClD,SAAS,EAAE,aAAa,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;IACjD,yEAAyE;IACzE,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;CAC1C;AAiBD;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,QAAQ,EAAE,OAAO,EAAE,MAAM,GAAG,UAAU,CAmI5E"}
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Normalized, flat, render-ready projection of `SeoProps`. The `<Seo>`
|
|
3
|
+
* template consumes this directly — no further reshaping, no
|
|
4
|
+
* conditionals beyond "field present?". Keeping normalization here (pure
|
|
5
|
+
* TypeScript) means vitest can assert on every decision without touching
|
|
6
|
+
* Astro's renderer.
|
|
7
|
+
*/
|
|
8
|
+
import { buildAlternateLinks } from '../alternates.js';
|
|
9
|
+
/**
|
|
10
|
+
* Opted-in robots directives for maximum snippet and preview sizes in
|
|
11
|
+
* search results. Appended after any `noindex`/`nofollow` prefix.
|
|
12
|
+
*/
|
|
13
|
+
export const ROBOTS_EXTRAS = 'max-snippet:-1, max-image-preview:large, max-video-preview:-1';
|
|
14
|
+
function toIsoString(value) {
|
|
15
|
+
if (value === undefined)
|
|
16
|
+
return undefined;
|
|
17
|
+
return value instanceof Date ? value.toISOString() : value;
|
|
18
|
+
}
|
|
19
|
+
function stripQueryParams(url) {
|
|
20
|
+
try {
|
|
21
|
+
const parsed = new URL(url);
|
|
22
|
+
parsed.search = '';
|
|
23
|
+
return parsed.toString();
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
return url;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Build the flat `SeoContext` from user-provided `SeoProps` and the
|
|
31
|
+
* current page URL (usually `Astro.url.href`). Pure function; no Astro
|
|
32
|
+
* runtime access.
|
|
33
|
+
*
|
|
34
|
+
* Invariants worth knowing:
|
|
35
|
+
* - `canonical` is omitted when `noindex` is true (Google recommendation).
|
|
36
|
+
* - `og:url` falls back to the query-stripped page URL when canonical
|
|
37
|
+
* is omitted, so share previews stay stable even on noindex pages.
|
|
38
|
+
* - Twitter override fields are suppressed when they equal their OG
|
|
39
|
+
* counterparts (Twitter falls back to OG automatically, so emitting
|
|
40
|
+
* the duplicate is noise).
|
|
41
|
+
* - `og:locale:alternate` is derived from `alternates` by converting
|
|
42
|
+
* hyphenated BCP 47 to underscore form and skipping entries that
|
|
43
|
+
* match the primary locale.
|
|
44
|
+
*/
|
|
45
|
+
export function buildSeoContext(props, pageUrl) {
|
|
46
|
+
const fullTitle = props.titleTemplate
|
|
47
|
+
? props.titleTemplate.replace('%s', props.title)
|
|
48
|
+
: props.title;
|
|
49
|
+
// Canonical: explicit > preserveQueryParams > strip query. Omitted
|
|
50
|
+
// entirely when noindex (Google recommendation).
|
|
51
|
+
let canonical;
|
|
52
|
+
if (props.canonical !== undefined) {
|
|
53
|
+
canonical = props.canonical.toString();
|
|
54
|
+
}
|
|
55
|
+
else if (props.preserveQueryParams) {
|
|
56
|
+
canonical = pageUrl;
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
59
|
+
canonical = stripQueryParams(pageUrl);
|
|
60
|
+
}
|
|
61
|
+
if (props.noindex)
|
|
62
|
+
canonical = undefined;
|
|
63
|
+
// og:url still points at the canonical or stripped page URL even
|
|
64
|
+
// when canonical is omitted — share previews shouldn't carry query
|
|
65
|
+
// params.
|
|
66
|
+
const ogUrl = canonical ?? stripQueryParams(pageUrl);
|
|
67
|
+
const ogType = props.ogType ?? 'website';
|
|
68
|
+
const ogImage = props.ogImage ?? '';
|
|
69
|
+
const locale = props.locale ?? 'en_US';
|
|
70
|
+
const og = {
|
|
71
|
+
title: fullTitle,
|
|
72
|
+
type: ogType,
|
|
73
|
+
image: ogImage,
|
|
74
|
+
url: ogUrl,
|
|
75
|
+
locale,
|
|
76
|
+
};
|
|
77
|
+
if (props.description !== undefined)
|
|
78
|
+
og.description = props.description;
|
|
79
|
+
if (props.siteName !== undefined)
|
|
80
|
+
og.siteName = props.siteName;
|
|
81
|
+
// og:locale:alternate: convert hyphens → underscores, skip dupes, skip
|
|
82
|
+
// the primary locale, and skip language-only entries whose language
|
|
83
|
+
// already matches the primary.
|
|
84
|
+
if (props.alternates?.entries && props.alternates.entries.length > 1) {
|
|
85
|
+
const primaryLang = locale.split('_')[0]?.toLowerCase();
|
|
86
|
+
const localeAlternate = [];
|
|
87
|
+
const seen = new Set();
|
|
88
|
+
for (const entry of props.alternates.entries) {
|
|
89
|
+
const ogLocale = entry.hreflang.replace('-', '_');
|
|
90
|
+
const entryLang = entry.hreflang.split('-')[0]?.toLowerCase();
|
|
91
|
+
if (seen.has(ogLocale))
|
|
92
|
+
continue;
|
|
93
|
+
if (ogLocale === locale)
|
|
94
|
+
continue;
|
|
95
|
+
if (!entry.hreflang.includes('-') && entryLang === primaryLang)
|
|
96
|
+
continue;
|
|
97
|
+
seen.add(ogLocale);
|
|
98
|
+
localeAlternate.push(ogLocale);
|
|
99
|
+
}
|
|
100
|
+
if (localeAlternate.length > 0)
|
|
101
|
+
og.localeAlternate = localeAlternate;
|
|
102
|
+
}
|
|
103
|
+
// OG image metadata: only when at least one dimension/alt is set, and
|
|
104
|
+
// only when an image URL is actually present (width/height without a
|
|
105
|
+
// URL is meaningless).
|
|
106
|
+
if (ogImage) {
|
|
107
|
+
if (props.ogImageAlt !== undefined)
|
|
108
|
+
og.imageAlt = props.ogImageAlt;
|
|
109
|
+
if (props.ogImageWidth !== undefined)
|
|
110
|
+
og.imageWidth = props.ogImageWidth;
|
|
111
|
+
if (props.ogImageHeight !== undefined)
|
|
112
|
+
og.imageHeight = props.ogImageHeight;
|
|
113
|
+
}
|
|
114
|
+
// Article metadata: only when ogType === 'article'.
|
|
115
|
+
if (ogType === 'article' && (props.article || props.articlePublisher)) {
|
|
116
|
+
const article = {};
|
|
117
|
+
if (props.article) {
|
|
118
|
+
const published = toIsoString(props.article.publishedTime);
|
|
119
|
+
if (published !== undefined)
|
|
120
|
+
article.publishedTime = published;
|
|
121
|
+
const modified = toIsoString(props.article.modifiedTime);
|
|
122
|
+
if (modified !== undefined)
|
|
123
|
+
article.modifiedTime = modified;
|
|
124
|
+
const expiration = toIsoString(props.article.expirationTime);
|
|
125
|
+
if (expiration !== undefined)
|
|
126
|
+
article.expirationTime = expiration;
|
|
127
|
+
if (props.article.authors !== undefined)
|
|
128
|
+
article.authors = props.article.authors;
|
|
129
|
+
if (props.article.tags !== undefined)
|
|
130
|
+
article.tags = props.article.tags;
|
|
131
|
+
if (props.article.section !== undefined)
|
|
132
|
+
article.section = props.article.section;
|
|
133
|
+
}
|
|
134
|
+
if (props.articlePublisher !== undefined)
|
|
135
|
+
article.publisher = props.articlePublisher;
|
|
136
|
+
og.article = article;
|
|
137
|
+
}
|
|
138
|
+
// Twitter: suppress overrides that equal their OG counterpart.
|
|
139
|
+
let twitter;
|
|
140
|
+
if (props.twitter !== undefined) {
|
|
141
|
+
const t = {
|
|
142
|
+
card: props.twitter.card ?? 'summary_large_image',
|
|
143
|
+
};
|
|
144
|
+
if (props.twitter.site !== undefined)
|
|
145
|
+
t.site = props.twitter.site;
|
|
146
|
+
if (props.twitter.creator !== undefined)
|
|
147
|
+
t.creator = props.twitter.creator;
|
|
148
|
+
if (props.twitter.title !== undefined && props.twitter.title !== fullTitle) {
|
|
149
|
+
t.title = props.twitter.title;
|
|
150
|
+
}
|
|
151
|
+
if (props.twitter.description !== undefined &&
|
|
152
|
+
props.twitter.description !== props.description) {
|
|
153
|
+
t.description = props.twitter.description;
|
|
154
|
+
}
|
|
155
|
+
if (props.twitter.image !== undefined && props.twitter.image !== ogImage) {
|
|
156
|
+
t.image = props.twitter.image;
|
|
157
|
+
}
|
|
158
|
+
if (props.twitter.imageAlt !== undefined && props.twitter.imageAlt !== props.ogImageAlt) {
|
|
159
|
+
t.imageAlt = props.twitter.imageAlt;
|
|
160
|
+
}
|
|
161
|
+
twitter = t;
|
|
162
|
+
}
|
|
163
|
+
const hreflangs = props.alternates !== undefined ? buildAlternateLinks(props.alternates) : [];
|
|
164
|
+
const authorName = props.author ?? props.article?.authors?.[0];
|
|
165
|
+
const ctx = {
|
|
166
|
+
title: fullTitle,
|
|
167
|
+
robots: {
|
|
168
|
+
noindex: props.noindex === true,
|
|
169
|
+
nofollow: props.nofollow === true,
|
|
170
|
+
extras: ROBOTS_EXTRAS,
|
|
171
|
+
},
|
|
172
|
+
og,
|
|
173
|
+
hreflangs,
|
|
174
|
+
extraLinks: props.extraLinks ?? [],
|
|
175
|
+
extraMeta: props.extraMeta ?? [],
|
|
176
|
+
};
|
|
177
|
+
if (canonical !== undefined)
|
|
178
|
+
ctx.canonical = canonical;
|
|
179
|
+
if (props.description !== undefined)
|
|
180
|
+
ctx.description = props.description;
|
|
181
|
+
if (twitter !== undefined)
|
|
182
|
+
ctx.twitter = twitter;
|
|
183
|
+
if (authorName !== undefined)
|
|
184
|
+
ctx.authorName = authorName;
|
|
185
|
+
if (props.graph !== undefined)
|
|
186
|
+
ctx.graph = props.graph;
|
|
187
|
+
return ctx;
|
|
188
|
+
}
|
|
189
|
+
//# sourceMappingURL=seo-context.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"seo-context.js","sourceRoot":"","sources":["../../src/components/seo-context.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,11 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
* here makes it unit-testable with vitest — Astro's renderer isn't
|
|
6
|
-
* easily accessible from vitest.
|
|
2
|
+
* Public props surface of the <Seo> component. The `.astro` template
|
|
3
|
+
* normalizes these via `buildSeoContext` (in `seo-context.ts`) and
|
|
4
|
+
* renders the resulting flat shape directly — no intermediate adapter.
|
|
7
5
|
*/
|
|
8
|
-
import {
|
|
6
|
+
import type { BuildAlternateLinksInput } from '../alternates.js';
|
|
9
7
|
/** Public props surface of the <Seo> component. */
|
|
10
8
|
export interface SeoProps {
|
|
11
9
|
/** Page title. Required. */
|
|
@@ -118,65 +116,4 @@ export interface SeoProps {
|
|
|
118
116
|
*/
|
|
119
117
|
alternates?: BuildAlternateLinksInput;
|
|
120
118
|
}
|
|
121
|
-
/**
|
|
122
|
-
* Shape consumed by `astro-seo`'s `<SEO>` component. We expose it as a
|
|
123
|
-
* type so tests can assert on exact keys / nesting rather than having
|
|
124
|
-
* to parse rendered HTML.
|
|
125
|
-
*/
|
|
126
|
-
export interface AstroSeoProps {
|
|
127
|
-
title: string;
|
|
128
|
-
description?: string;
|
|
129
|
-
canonical?: string;
|
|
130
|
-
openGraph: {
|
|
131
|
-
basic: {
|
|
132
|
-
title: string;
|
|
133
|
-
type: string;
|
|
134
|
-
image: string;
|
|
135
|
-
url: string;
|
|
136
|
-
};
|
|
137
|
-
optional?: {
|
|
138
|
-
description?: string;
|
|
139
|
-
siteName?: string;
|
|
140
|
-
locale?: string;
|
|
141
|
-
localeAlternate?: string[];
|
|
142
|
-
};
|
|
143
|
-
image?: {
|
|
144
|
-
alt?: string;
|
|
145
|
-
width?: number;
|
|
146
|
-
height?: number;
|
|
147
|
-
};
|
|
148
|
-
article?: {
|
|
149
|
-
publishedTime?: string;
|
|
150
|
-
modifiedTime?: string;
|
|
151
|
-
expirationTime?: string;
|
|
152
|
-
authors?: string[];
|
|
153
|
-
tags?: string[];
|
|
154
|
-
section?: string;
|
|
155
|
-
publisher?: string;
|
|
156
|
-
};
|
|
157
|
-
};
|
|
158
|
-
twitter?: {
|
|
159
|
-
card: 'summary' | 'summary_large_image' | 'app' | 'player';
|
|
160
|
-
site?: string;
|
|
161
|
-
creator?: string;
|
|
162
|
-
title?: string;
|
|
163
|
-
description?: string;
|
|
164
|
-
image?: string;
|
|
165
|
-
imageAlt?: string;
|
|
166
|
-
};
|
|
167
|
-
extend?: {
|
|
168
|
-
link?: Array<Record<string, string>>;
|
|
169
|
-
meta?: Array<Record<string, string>>;
|
|
170
|
-
};
|
|
171
|
-
}
|
|
172
|
-
/**
|
|
173
|
-
* Project the public `SeoProps` onto the shape `astro-seo`'s `<SEO>`
|
|
174
|
-
* component expects. Pure function; no Astro runtime access.
|
|
175
|
-
*
|
|
176
|
-
* @param props Props as provided by the user.
|
|
177
|
-
* @param pageUrl The current page URL — usually `Astro.url.href`.
|
|
178
|
-
* Used as the canonical URL when `props.canonical` is
|
|
179
|
-
* not provided, and as the base for the OG `url`.
|
|
180
|
-
*/
|
|
181
|
-
export declare function buildAstroSeoProps(props: SeoProps, pageUrl: string): AstroSeoProps;
|
|
182
119
|
//# sourceMappingURL=seo-props.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"seo-props.d.ts","sourceRoot":"","sources":["../../src/components/seo-props.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"seo-props.d.ts","sourceRoot":"","sources":["../../src/components/seo-props.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,kBAAkB,CAAC;AAEjE,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;;;;;;OAMG;IACH,SAAS,CAAC,EAAE,MAAM,GAAG,GAAG,CAAC;IACzB;;;;OAIG;IACH,mBAAmB,CAAC,EAAE,OAAO,CAAC;IAC9B,gDAAgD;IAChD,MAAM,CAAC,EAAE,SAAS,GAAG,SAAS,GAAG,SAAS,GAAG,MAAM,CAAC;IACpD,uCAAuC;IACvC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,oCAAoC;IACpC,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,0CAA0C;IAC1C,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,2CAA2C;IAC3C,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,uDAAuD;IACvD,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,wCAAwC;IACxC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB;;;;;;;OAOG;IACH,OAAO,CAAC,EAAE;QACN,2DAA2D;QAC3D,IAAI,CAAC,EAAE,MAAM,CAAC;QACd,oCAAoC;QACpC,OAAO,CAAC,EAAE,MAAM,CAAC;QACjB,sDAAsD;QACtD,IAAI,CAAC,EAAE,SAAS,GAAG,qBAAqB,GAAG,KAAK,GAAG,QAAQ,CAAC;QAC5D,6DAA6D;QAC7D,KAAK,CAAC,EAAE,MAAM,CAAC;QACf,yEAAyE;QACzE,WAAW,CAAC,EAAE,MAAM,CAAC;QACrB,6DAA6D;QAC7D,KAAK,CAAC,EAAE,MAAM,CAAC;QACf,qEAAqE;QACrE,QAAQ,CAAC,EAAE,MAAM,CAAC;KACrB,CAAC;IACF;;;OAGG;IACH,OAAO,CAAC,EAAE;QACN,aAAa,CAAC,EAAE,IAAI,GAAG,MAAM,CAAC;QAC9B,YAAY,CAAC,EAAE,IAAI,GAAG,MAAM,CAAC;QAC7B,cAAc,CAAC,EAAE,IAAI,GAAG,MAAM,CAAC;QAC/B,OAAO,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;QAC5B,IAAI,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;QACzB,OAAO,CAAC,EAAE,MAAM,CAAC;KACpB,CAAC;IACF,oEAAoE;IACpE,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,0CAA0C;IAC1C,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB;;;OAGG;IACH,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B;;;OAGG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB;;;;;OAKG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;IACvC,iEAAiE;IACjE,UAAU,CAAC,EAAE,aAAa,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;IACnD,mDAAmD;IACnD,SAAS,CAAC,EAAE,aAAa,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;IAClD;;;;;;;OAOG;IACH,UAAU,CAAC,EAAE,wBAAwB,CAAC;CACzC"}
|
|
@@ -1,236 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
* here makes it unit-testable with vitest — Astro's renderer isn't
|
|
6
|
-
* easily accessible from vitest.
|
|
2
|
+
* Public props surface of the <Seo> component. The `.astro` template
|
|
3
|
+
* normalizes these via `buildSeoContext` (in `seo-context.ts`) and
|
|
4
|
+
* renders the resulting flat shape directly — no intermediate adapter.
|
|
7
5
|
*/
|
|
8
|
-
|
|
9
|
-
function toIsoString(value) {
|
|
10
|
-
if (value === undefined)
|
|
11
|
-
return undefined;
|
|
12
|
-
return value instanceof Date ? value.toISOString() : value;
|
|
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
|
-
}
|
|
48
|
-
/**
|
|
49
|
-
* Project the public `SeoProps` onto the shape `astro-seo`'s `<SEO>`
|
|
50
|
-
* component expects. Pure function; no Astro runtime access.
|
|
51
|
-
*
|
|
52
|
-
* @param props Props as provided by the user.
|
|
53
|
-
* @param pageUrl The current page URL — usually `Astro.url.href`.
|
|
54
|
-
* Used as the canonical URL when `props.canonical` is
|
|
55
|
-
* not provided, and as the base for the OG `url`.
|
|
56
|
-
*/
|
|
57
|
-
export function buildAstroSeoProps(props, pageUrl) {
|
|
58
|
-
const fullTitle = props.titleTemplate
|
|
59
|
-
? props.titleTemplate.replace('%s', props.title)
|
|
60
|
-
: props.title;
|
|
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);
|
|
81
|
-
const ogType = props.ogType ?? 'website';
|
|
82
|
-
const ogImage = props.ogImage ?? '';
|
|
83
|
-
const openGraph = {
|
|
84
|
-
basic: {
|
|
85
|
-
title: fullTitle,
|
|
86
|
-
type: ogType,
|
|
87
|
-
image: ogImage,
|
|
88
|
-
url: ogUrl,
|
|
89
|
-
},
|
|
90
|
-
};
|
|
91
|
-
const optional = {};
|
|
92
|
-
if (props.description !== undefined)
|
|
93
|
-
optional.description = props.description;
|
|
94
|
-
if (props.siteName !== undefined)
|
|
95
|
-
optional.siteName = props.siteName;
|
|
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
|
-
}
|
|
121
|
-
openGraph.optional = optional;
|
|
122
|
-
const hasImageMeta = props.ogImageAlt !== undefined ||
|
|
123
|
-
props.ogImageWidth !== undefined ||
|
|
124
|
-
props.ogImageHeight !== undefined;
|
|
125
|
-
if (ogImage && hasImageMeta) {
|
|
126
|
-
const image = {};
|
|
127
|
-
if (props.ogImageAlt !== undefined)
|
|
128
|
-
image.alt = props.ogImageAlt;
|
|
129
|
-
if (props.ogImageWidth !== undefined)
|
|
130
|
-
image.width = props.ogImageWidth;
|
|
131
|
-
if (props.ogImageHeight !== undefined)
|
|
132
|
-
image.height = props.ogImageHeight;
|
|
133
|
-
openGraph.image = image;
|
|
134
|
-
}
|
|
135
|
-
if (ogType === 'article' && (props.article || props.articlePublisher)) {
|
|
136
|
-
const article = {};
|
|
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;
|
|
156
|
-
openGraph.article = article;
|
|
157
|
-
}
|
|
158
|
-
const astroSeo = {
|
|
159
|
-
title: fullTitle,
|
|
160
|
-
openGraph,
|
|
161
|
-
};
|
|
162
|
-
if (canonical !== undefined)
|
|
163
|
-
astroSeo.canonical = canonical;
|
|
164
|
-
if (props.description !== undefined)
|
|
165
|
-
astroSeo.description = props.description;
|
|
166
|
-
if (props.twitter !== undefined) {
|
|
167
|
-
const twitter = {
|
|
168
|
-
card: props.twitter.card ?? 'summary_large_image',
|
|
169
|
-
};
|
|
170
|
-
if (props.twitter.site !== undefined)
|
|
171
|
-
twitter.site = props.twitter.site;
|
|
172
|
-
if (props.twitter.creator !== undefined)
|
|
173
|
-
twitter.creator = props.twitter.creator;
|
|
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
|
-
}
|
|
191
|
-
astroSeo.twitter = twitter;
|
|
192
|
-
}
|
|
193
|
-
// Resolve hreflang alternates (if any). Returns [] when fewer than
|
|
194
|
-
// 2 entries survive validation, so this is cheap to call.
|
|
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
|
-
}
|
|
210
|
-
const hasExtraLinks = props.extraLinks !== undefined && props.extraLinks.length > 0;
|
|
211
|
-
const hasAlternates = alternateLinks.length > 0;
|
|
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
|
-
});
|
|
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) {
|
|
232
|
-
astroSeo.extend = extend;
|
|
233
|
-
}
|
|
234
|
-
return astroSeo;
|
|
235
|
-
}
|
|
6
|
+
export {};
|
|
236
7
|
//# sourceMappingURL=seo-props.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"seo-props.js","sourceRoot":"","sources":["../../src/components/seo-props.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"seo-props.js","sourceRoot":"","sources":["../../src/components/seo-props.ts"],"names":[],"mappings":"AAAA;;;;GAIG"}
|
package/dist/index.d.ts
CHANGED
|
@@ -3,8 +3,9 @@ export type { AggregatorOptions, AggregatedGraph } from './aggregator.js';
|
|
|
3
3
|
export { createSchemaEndpoint, createSchemaMap } from './routes.js';
|
|
4
4
|
export type { SchemaEndpointOptions, SchemaMapEntry, SchemaMapOptions } from './routes.js';
|
|
5
5
|
export { seoSchema, imageSchema } from './content-helpers.js';
|
|
6
|
-
export {
|
|
7
|
-
export
|
|
6
|
+
export type { SeoProps } from './components/seo-props.js';
|
|
7
|
+
export { buildSeoContext, ROBOTS_EXTRAS } from './components/seo-context.js';
|
|
8
|
+
export type { SeoContext } from './components/seo-context.js';
|
|
8
9
|
export { buildAlternateLinks } from './alternates.js';
|
|
9
10
|
export type { AlternateLink, BuildAlternateLinksInput } from './alternates.js';
|
|
10
11
|
export { breadcrumbsFromUrl } from './breadcrumbs.js';
|
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,
|
|
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,YAAY,EAAE,QAAQ,EAAE,MAAM,2BAA2B,CAAC;AAC1D,OAAO,EAAE,eAAe,EAAE,aAAa,EAAE,MAAM,6BAA6B,CAAC;AAC7E,YAAY,EAAE,UAAU,EAAE,MAAM,6BAA6B,CAAC;AAE9D,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;AAEnF,OAAO,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAC9C,YAAY,EAAE,YAAY,EAAE,cAAc,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
export { aggregate } from './aggregator.js';
|
|
7
7
|
export { createSchemaEndpoint, createSchemaMap } from './routes.js';
|
|
8
8
|
export { seoSchema, imageSchema } from './content-helpers.js';
|
|
9
|
-
export {
|
|
9
|
+
export { buildSeoContext, ROBOTS_EXTRAS } from './components/seo-context.js';
|
|
10
10
|
export { buildAlternateLinks } from './alternates.js';
|
|
11
11
|
export { breadcrumbsFromUrl } from './breadcrumbs.js';
|
|
12
12
|
export { createIndexNowKeyRoute, submitToIndexNow, validateIndexNowKey } from './indexnow.js';
|
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;
|
|
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;AAG9D,OAAO,EAAE,eAAe,EAAE,aAAa,EAAE,MAAM,6BAA6B,CAAC;AAG7E,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;AAG9F,OAAO,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC"}
|
package/dist/integration.d.ts
CHANGED
|
@@ -36,10 +36,27 @@ export interface IndexNowIntegrationOptions {
|
|
|
36
36
|
endpoint?: string;
|
|
37
37
|
/**
|
|
38
38
|
* Filter the list of URLs before submission. Return `false` to skip
|
|
39
|
-
* a URL.
|
|
39
|
+
* a URL. Composed on top of the built-in `/404` exclusion — you
|
|
40
|
+
* don't need to re-exclude that yourself.
|
|
41
|
+
*
|
|
42
|
+
* @example excluding paginated archives in addition to the default 404:
|
|
43
|
+
* ```ts
|
|
44
|
+
* filter: (url) => !/^\/blog\/\d+\/$/.test(new URL(url).pathname),
|
|
45
|
+
* ```
|
|
40
46
|
*/
|
|
41
47
|
filter?: (url: string) => boolean;
|
|
42
48
|
}
|
|
49
|
+
/**
|
|
50
|
+
* Pathnames that `indexNow` excludes by default, regardless of any
|
|
51
|
+
* caller-supplied `filter`. The 404 page is the only universal case —
|
|
52
|
+
* search engines don't need to be notified about it, and submitting it
|
|
53
|
+
* wastes daily IndexNow quota.
|
|
54
|
+
*
|
|
55
|
+
* Matches both `/404` (from an Astro `src/pages/404.astro` → `404.html`
|
|
56
|
+
* that `htmlFileToUrl` strips to `/404`) and `/404/` (the directory form
|
|
57
|
+
* when the build emits `404/index.html`).
|
|
58
|
+
*/
|
|
59
|
+
export declare function isDefaultExcludedFromIndexNow(url: string): boolean;
|
|
43
60
|
export interface LlmsTxtIntegrationOptions {
|
|
44
61
|
/** H1 of the generated `llms.txt`. */
|
|
45
62
|
title: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"integration.d.ts","sourceRoot":"","sources":["../src/integration.ts"],"names":[],"mappings":"AAIA,OAAO,EAAiB,KAAK,cAAc,EAAE,MAAM,eAAe,CAAC;AAKnE,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
|
|
1
|
+
{"version":3,"file":"integration.d.ts","sourceRoot":"","sources":["../src/integration.ts"],"names":[],"mappings":"AAIA,OAAO,EAAiB,KAAK,cAAc,EAAE,MAAM,eAAe,CAAC;AAKnE,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;;;;;;;;;OASG;IACH,MAAM,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC;CACrC;AAED;;;;;;;;;GASG;AACH,wBAAgB,6BAA6B,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAQlE;AAED,MAAM,WAAW,yBAAyB;IACtC,sCAAsC;IACtC,KAAK,EAAE,MAAM,CAAC;IACd,uEAAuE;IACvE,OAAO,EAAE,MAAM,CAAC;IAChB,yDAAyD;IACzD,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,mEAAmE;IACnE,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB;;;;OAIG;IACH,QAAQ,CAAC,EAAE,cAAc,EAAE,CAAC;IAC5B;;;OAGG;IACH,MAAM,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC;IAClC;;;OAGG;IACH,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB;;;OAGG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,0BAA0B;IACvC;;;;OAIG;IACH,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB;;;;;;;;OAQG;IACH,sBAAsB,CAAC,EAAE,OAAO,CAAC;IACjC;;;;OAIG;IACH,QAAQ,CAAC,EAAE,0BAA0B,CAAC;IACtC;;;OAGG;IACH,OAAO,CAAC,EAAE,yBAAyB,CAAC;CACvC;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;;;;GAIG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAKxD;AAED;;;GAGG;AACH,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAQlE;AAgBD;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,CAAC,OAAO,UAAU,QAAQ,CAAC,OAAO,GAAE,0BAA+B,GAAG,oBAAoB,CA6J/F"}
|
package/dist/integration.js
CHANGED
|
@@ -3,6 +3,26 @@ import { join, relative } from 'node:path';
|
|
|
3
3
|
import { fileURLToPath } from 'node:url';
|
|
4
4
|
import { submitToIndexNow } from '@jdevalk/seo-graph-core';
|
|
5
5
|
import { renderLlmsTxt } from './llms-txt.js';
|
|
6
|
+
/**
|
|
7
|
+
* Pathnames that `indexNow` excludes by default, regardless of any
|
|
8
|
+
* caller-supplied `filter`. The 404 page is the only universal case —
|
|
9
|
+
* search engines don't need to be notified about it, and submitting it
|
|
10
|
+
* wastes daily IndexNow quota.
|
|
11
|
+
*
|
|
12
|
+
* Matches both `/404` (from an Astro `src/pages/404.astro` → `404.html`
|
|
13
|
+
* that `htmlFileToUrl` strips to `/404`) and `/404/` (the directory form
|
|
14
|
+
* when the build emits `404/index.html`).
|
|
15
|
+
*/
|
|
16
|
+
export function isDefaultExcludedFromIndexNow(url) {
|
|
17
|
+
let pathname;
|
|
18
|
+
try {
|
|
19
|
+
pathname = new URL(url).pathname;
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
return pathname === '/404' || pathname === '/404/';
|
|
25
|
+
}
|
|
6
26
|
/**
|
|
7
27
|
* Turn a built HTML file path (relative to the outDir, e.g.
|
|
8
28
|
* `blog/post/index.html`) into an absolute URL on `siteUrl`. Rewrites
|
|
@@ -106,7 +126,7 @@ async function collectHtmlFiles(dir, base = dir) {
|
|
|
106
126
|
* ```
|
|
107
127
|
*/
|
|
108
128
|
export default function seoGraph(options = {}) {
|
|
109
|
-
const { validateH1 = true, validateUniqueMetadata = true, indexNow, llmsTxt
|
|
129
|
+
const { validateH1 = true, validateUniqueMetadata = true, indexNow, llmsTxt } = options;
|
|
110
130
|
const autoLlmsTxt = llmsTxt && !llmsTxt.sections;
|
|
111
131
|
return {
|
|
112
132
|
name: '@jdevalk/astro-seo-graph',
|
|
@@ -114,9 +134,7 @@ export default function seoGraph(options = {}) {
|
|
|
114
134
|
'astro:build:done': async ({ dir, logger }) => {
|
|
115
135
|
const buildDir = fileURLToPath(dir);
|
|
116
136
|
const needsContentScan = validateH1 || validateUniqueMetadata || autoLlmsTxt;
|
|
117
|
-
const htmlFiles = needsContentScan || indexNow || llmsTxt
|
|
118
|
-
? await collectHtmlFiles(buildDir)
|
|
119
|
-
: [];
|
|
137
|
+
const htmlFiles = needsContentScan || indexNow || llmsTxt ? await collectHtmlFiles(buildDir) : [];
|
|
120
138
|
const autoLinks = [];
|
|
121
139
|
const h1Missing = [];
|
|
122
140
|
const h1Multiple = [];
|
|
@@ -194,6 +212,7 @@ export default function seoGraph(options = {}) {
|
|
|
194
212
|
if (indexNow) {
|
|
195
213
|
const urls = htmlFiles
|
|
196
214
|
.map((f) => htmlFileToUrl(f, indexNow.siteUrl))
|
|
215
|
+
.filter((u) => !isDefaultExcludedFromIndexNow(u))
|
|
197
216
|
.filter((u) => (indexNow.filter ? indexNow.filter(u) : true));
|
|
198
217
|
if (urls.length === 0) {
|
|
199
218
|
logger.info('IndexNow: no URLs to submit.');
|
|
@@ -217,8 +236,9 @@ export default function seoGraph(options = {}) {
|
|
|
217
236
|
}
|
|
218
237
|
}
|
|
219
238
|
if (llmsTxt) {
|
|
220
|
-
const sections = llmsTxt.sections ??
|
|
221
|
-
|
|
239
|
+
const sections = llmsTxt.sections ?? [
|
|
240
|
+
{ name: llmsTxt.autoSectionName ?? 'Pages', links: autoLinks },
|
|
241
|
+
];
|
|
222
242
|
const rendered = renderLlmsTxt({
|
|
223
243
|
title: llmsTxt.title,
|
|
224
244
|
summary: llmsTxt.summary,
|
package/dist/integration.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"integration.js","sourceRoot":"","sources":["../src/integration.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,OAAO,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAChE,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,WAAW,CAAC;AAC3C,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AACzC,OAAO,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAC3D,OAAO,EAAE,aAAa,EAAuB,MAAM,eAAe,CAAC;
|
|
1
|
+
{"version":3,"file":"integration.js","sourceRoot":"","sources":["../src/integration.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jdevalk/astro-seo-graph",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "1.0.1",
|
|
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",
|
|
@@ -49,7 +49,6 @@
|
|
|
49
49
|
"astro": "^5.0.0 || ^6.0.0"
|
|
50
50
|
},
|
|
51
51
|
"dependencies": {
|
|
52
|
-
"astro-seo": "^1.1.0",
|
|
53
52
|
"schema-dts": "^2.0.0",
|
|
54
53
|
"zod": "^3.24.0",
|
|
55
54
|
"@jdevalk/seo-graph-core": "0.6.1"
|
|
@@ -58,7 +57,7 @@
|
|
|
58
57
|
"@types/node": "^22.0.0",
|
|
59
58
|
"astro": "^6.0.5",
|
|
60
59
|
"typescript": "^5.6.0",
|
|
61
|
-
"vitest": "^2.
|
|
60
|
+
"vitest": "^3.2.4"
|
|
62
61
|
},
|
|
63
62
|
"scripts": {
|
|
64
63
|
"build": "tsc -p tsconfig.build.json && mkdir -p dist/components && cp src/components/*.astro dist/components/ && cp ../../AGENTS.md .",
|