@jdevalk/astro-seo-graph 0.1.0-alpha.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/LICENSE +21 -0
- package/README.md +154 -0
- package/dist/aggregator.d.ts +27 -0
- package/dist/aggregator.d.ts.map +1 -0
- package/dist/aggregator.js +23 -0
- package/dist/aggregator.js.map +1 -0
- package/dist/components/Seo.astro +14 -0
- package/dist/components/seo-props.d.ts +132 -0
- package/dist/components/seo-props.d.ts.map +1 -0
- package/dist/components/seo-props.js +115 -0
- package/dist/components/seo-props.js.map +1 -0
- package/dist/content-helpers.d.ts +87 -0
- package/dist/content-helpers.d.ts.map +1 -0
- package/dist/content-helpers.js +52 -0
- package/dist/content-helpers.js.map +1 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +10 -0
- package/dist/index.js.map +1 -0
- package/dist/routes.d.ts +94 -0
- package/dist/routes.d.ts.map +1 -0
- package/dist/routes.js +92 -0
- package/dist/routes.js.map +1 -0
- package/package.json +62 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Joost de Valk
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
# @jdevalk/astro-seo-graph
|
|
2
|
+
|
|
3
|
+
Astro integration for [`@jdevalk/seo-graph-core`](../seo-graph-core). Ships a
|
|
4
|
+
`<Seo>` component, route factories for agent-ready schema endpoints, a
|
|
5
|
+
content-collection aggregator, and Zod helpers for content schemas.
|
|
6
|
+
|
|
7
|
+
> **Status:** pre-v1, alpha track. The API may still shift. See the
|
|
8
|
+
> [roadmap](https://github.com/jdevalk/seo-graph#roadmap) in the monorepo root.
|
|
9
|
+
|
|
10
|
+
## What's in v0.1
|
|
11
|
+
|
|
12
|
+
| API | Purpose |
|
|
13
|
+
| ------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
14
|
+
| **`<Seo>`** (`./Seo.astro`) | Single head component covering `<title>`, meta description, canonical, Open Graph, Twitter card, and optional JSON-LD `@graph`. Wraps [`astro-seo`](https://github.com/jonasmerlin/astro-seo) for the meta tags. |
|
|
15
|
+
| **`createSchemaEndpoint`** | Factory returning an Astro `APIRoute` handler that serves a corpus-wide JSON-LD `@graph` for a content collection. |
|
|
16
|
+
| **`createSchemaMap`** | Factory returning an `APIRoute` handler that emits a sitemap-style XML listing of your site's schema endpoints — the discovery point for agent crawlers. |
|
|
17
|
+
| **`aggregate`** | Shared engine behind the endpoint factories. Walks a list of entries, runs a caller-supplied mapper, deduplicates by `@id`. |
|
|
18
|
+
| **`seoSchema`, `imageSchema`** | Zod schemas for the `seo` and `image` fields on content collections. Import them into `src/content.config.ts`. |
|
|
19
|
+
| **`buildAstroSeoProps`** | Pure-TS logic that powers `<Seo>` — exported for users who want to feed a different head component. |
|
|
20
|
+
|
|
21
|
+
## Not in v0.1 (deferred to v0.2)
|
|
22
|
+
|
|
23
|
+
- **`createOgRenderer`** — a themeable `satori` + `sharp` wrapper for generating
|
|
24
|
+
Open Graph images at build time. Pulled out of v0.1 to keep the dep tree
|
|
25
|
+
free of native binaries. Keep using your own `og-image.ts` for now.
|
|
26
|
+
|
|
27
|
+
## Installation
|
|
28
|
+
|
|
29
|
+
```sh
|
|
30
|
+
pnpm add @jdevalk/astro-seo-graph @jdevalk/seo-graph-core
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
`@jdevalk/seo-graph-core` is a direct dep of this package so it's installed
|
|
34
|
+
transitively, but depending on it explicitly lets you pin the version and
|
|
35
|
+
import piece builders directly.
|
|
36
|
+
|
|
37
|
+
## `<Seo>` component
|
|
38
|
+
|
|
39
|
+
```astro
|
|
40
|
+
---
|
|
41
|
+
import Seo from '@jdevalk/astro-seo-graph/Seo.astro';
|
|
42
|
+
import { buildSchemaGraph } from '../utils/schema';
|
|
43
|
+
|
|
44
|
+
const graph = buildSchemaGraph({
|
|
45
|
+
pageType: 'blogPost',
|
|
46
|
+
canonicalUrl: Astro.url.href,
|
|
47
|
+
title: 'My Post',
|
|
48
|
+
description: '…',
|
|
49
|
+
publishDate: new Date('2026-04-07'),
|
|
50
|
+
});
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
<html>
|
|
54
|
+
<head>
|
|
55
|
+
<Seo
|
|
56
|
+
title="My Post"
|
|
57
|
+
titleTemplate="%s | Example"
|
|
58
|
+
description="…"
|
|
59
|
+
ogType="article"
|
|
60
|
+
ogImage="https://example.com/og/my-post.jpg"
|
|
61
|
+
ogImageAlt="My Post"
|
|
62
|
+
ogImageWidth={1200}
|
|
63
|
+
ogImageHeight={675}
|
|
64
|
+
siteName="Example"
|
|
65
|
+
twitter={{ site: '@example', creator: '@author' }}
|
|
66
|
+
article={{ publishedTime: new Date('2026-04-07'), tags: ['tech'] }}
|
|
67
|
+
graph={graph}
|
|
68
|
+
extraLinks={[
|
|
69
|
+
{ rel: 'icon', type: 'image/svg+xml', href: '/favicon.svg' },
|
|
70
|
+
{ rel: 'sitemap', href: '/sitemap-index.xml' },
|
|
71
|
+
]}
|
|
72
|
+
/>
|
|
73
|
+
</head>
|
|
74
|
+
<body>...</body>
|
|
75
|
+
</html>
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## Schema endpoints
|
|
79
|
+
|
|
80
|
+
```ts
|
|
81
|
+
// src/pages/schema/post.json.ts
|
|
82
|
+
import { getCollection } from 'astro:content';
|
|
83
|
+
import { createSchemaEndpoint } from '@jdevalk/astro-seo-graph';
|
|
84
|
+
import { buildArticle, buildWebPage, makeIds } from '@jdevalk/seo-graph-core';
|
|
85
|
+
|
|
86
|
+
const ids = makeIds({ siteUrl: 'https://example.com' });
|
|
87
|
+
|
|
88
|
+
export const GET = createSchemaEndpoint({
|
|
89
|
+
entries: () => getCollection('blog'),
|
|
90
|
+
mapper: (post) => {
|
|
91
|
+
const url = `https://example.com/${post.id}/`;
|
|
92
|
+
return [
|
|
93
|
+
buildWebPage(
|
|
94
|
+
{
|
|
95
|
+
url,
|
|
96
|
+
name: post.data.title,
|
|
97
|
+
isPartOf: { '@id': ids.website },
|
|
98
|
+
breadcrumb: { '@id': ids.breadcrumb(url) },
|
|
99
|
+
datePublished: post.data.publishDate,
|
|
100
|
+
},
|
|
101
|
+
ids,
|
|
102
|
+
),
|
|
103
|
+
buildArticle(
|
|
104
|
+
{
|
|
105
|
+
url,
|
|
106
|
+
isPartOf: { '@id': ids.webPage(url) },
|
|
107
|
+
author: { '@id': ids.person },
|
|
108
|
+
publisher: { '@id': ids.person },
|
|
109
|
+
headline: post.data.title,
|
|
110
|
+
description: post.data.excerpt ?? '',
|
|
111
|
+
datePublished: post.data.publishDate,
|
|
112
|
+
},
|
|
113
|
+
ids,
|
|
114
|
+
),
|
|
115
|
+
];
|
|
116
|
+
},
|
|
117
|
+
});
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
## Schema map discovery
|
|
121
|
+
|
|
122
|
+
```ts
|
|
123
|
+
// src/pages/schemamap.xml.ts
|
|
124
|
+
import { createSchemaMap } from '@jdevalk/astro-seo-graph';
|
|
125
|
+
|
|
126
|
+
export const GET = createSchemaMap({
|
|
127
|
+
siteUrl: 'https://example.com',
|
|
128
|
+
entries: [
|
|
129
|
+
{ path: '/schema/post.json', lastModified: new Date('2026-04-07') },
|
|
130
|
+
{ path: '/schema/video.json', lastModified: new Date('2026-03-13') },
|
|
131
|
+
],
|
|
132
|
+
});
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
## Zod content helpers
|
|
136
|
+
|
|
137
|
+
```ts
|
|
138
|
+
// src/content.config.ts
|
|
139
|
+
import { defineCollection, z } from 'astro:content';
|
|
140
|
+
import { seoSchema } from '@jdevalk/astro-seo-graph';
|
|
141
|
+
|
|
142
|
+
const blog = defineCollection({
|
|
143
|
+
schema: ({ image }) =>
|
|
144
|
+
z.object({
|
|
145
|
+
title: z.string(),
|
|
146
|
+
publishDate: z.coerce.date(),
|
|
147
|
+
seo: seoSchema(image).optional(),
|
|
148
|
+
}),
|
|
149
|
+
});
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
## License
|
|
153
|
+
|
|
154
|
+
MIT © Joost de Valk
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { type GraphEntity } from '@jdevalk/seo-graph-core';
|
|
2
|
+
export interface AggregatorOptions<Entry> {
|
|
3
|
+
/** Content entries to walk, typically from Astro's `getCollection`. */
|
|
4
|
+
entries: readonly Entry[];
|
|
5
|
+
/**
|
|
6
|
+
* Map a single entry to an array of schema.org pieces. Return an
|
|
7
|
+
* empty array to skip the entry entirely (e.g. drafts). Pieces
|
|
8
|
+
* returned here are concatenated across entries; the aggregator
|
|
9
|
+
* deduplicates by `@id` with first-occurrence-wins semantics.
|
|
10
|
+
*/
|
|
11
|
+
mapper: (entry: Entry) => ReadonlyArray<GraphEntity>;
|
|
12
|
+
}
|
|
13
|
+
export interface AggregatedGraph {
|
|
14
|
+
'@context': 'https://schema.org';
|
|
15
|
+
'@graph': GraphEntity[];
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Walk a list of content entries, run the caller-supplied mapper over
|
|
19
|
+
* each one, concatenate all resulting pieces, and deduplicate by
|
|
20
|
+
* `@id`. Returns a ready-to-serialize `@graph` envelope.
|
|
21
|
+
*
|
|
22
|
+
* This is the shared engine behind `createSchemaEndpoint` — use it
|
|
23
|
+
* directly if you need custom wrapping, caching, or multiple-collection
|
|
24
|
+
* merging that the endpoint factories don't expose.
|
|
25
|
+
*/
|
|
26
|
+
export declare function aggregate<Entry>(options: AggregatorOptions<Entry>): AggregatedGraph;
|
|
27
|
+
//# sourceMappingURL=aggregator.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"aggregator.d.ts","sourceRoot":"","sources":["../src/aggregator.ts"],"names":[],"mappings":"AAAA,OAAO,EAAwB,KAAK,WAAW,EAAE,MAAM,yBAAyB,CAAC;AAEjF,MAAM,WAAW,iBAAiB,CAAC,KAAK;IACpC,uEAAuE;IACvE,OAAO,EAAE,SAAS,KAAK,EAAE,CAAC;IAC1B;;;;;OAKG;IACH,MAAM,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,aAAa,CAAC,WAAW,CAAC,CAAC;CACxD;AAED,MAAM,WAAW,eAAe;IAC5B,UAAU,EAAE,oBAAoB,CAAC;IACjC,QAAQ,EAAE,WAAW,EAAE,CAAC;CAC3B;AAED;;;;;;;;GAQG;AACH,wBAAgB,SAAS,CAAC,KAAK,EAAE,OAAO,EAAE,iBAAiB,CAAC,KAAK,CAAC,GAAG,eAAe,CAWnF"}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { deduplicateByGraphId } from '@jdevalk/seo-graph-core';
|
|
2
|
+
/**
|
|
3
|
+
* Walk a list of content entries, run the caller-supplied mapper over
|
|
4
|
+
* each one, concatenate all resulting pieces, and deduplicate by
|
|
5
|
+
* `@id`. Returns a ready-to-serialize `@graph` envelope.
|
|
6
|
+
*
|
|
7
|
+
* This is the shared engine behind `createSchemaEndpoint` — use it
|
|
8
|
+
* directly if you need custom wrapping, caching, or multiple-collection
|
|
9
|
+
* merging that the endpoint factories don't expose.
|
|
10
|
+
*/
|
|
11
|
+
export function aggregate(options) {
|
|
12
|
+
const pieces = [];
|
|
13
|
+
for (const entry of options.entries) {
|
|
14
|
+
for (const piece of options.mapper(entry)) {
|
|
15
|
+
pieces.push(piece);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
return {
|
|
19
|
+
'@context': 'https://schema.org',
|
|
20
|
+
'@graph': deduplicateByGraphId(pieces),
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
//# sourceMappingURL=aggregator.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"aggregator.js","sourceRoot":"","sources":["../src/aggregator.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,oBAAoB,EAAoB,MAAM,yBAAyB,CAAC;AAmBjF;;;;;;;;GAQG;AACH,MAAM,UAAU,SAAS,CAAQ,OAAiC;IAC9D,MAAM,MAAM,GAAkB,EAAE,CAAC;IACjC,KAAK,MAAM,KAAK,IAAI,OAAO,CAAC,OAAO,EAAE,CAAC;QAClC,KAAK,MAAM,KAAK,IAAI,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC;YACxC,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QACvB,CAAC;IACL,CAAC;IACD,OAAO;QACH,UAAU,EAAE,oBAAoB;QAChC,QAAQ,EAAE,oBAAoB,CAAC,MAAM,CAAC;KACzC,CAAC;AACN,CAAC"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
---
|
|
2
|
+
import { SEO } from 'astro-seo';
|
|
3
|
+
import { buildAstroSeoProps, type SeoProps } from './seo-props.js';
|
|
4
|
+
|
|
5
|
+
const props = Astro.props as SeoProps;
|
|
6
|
+
const astroSeo = buildAstroSeoProps(props, Astro.url.href);
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
<SEO {...astroSeo} />
|
|
10
|
+
{
|
|
11
|
+
props.graph && (
|
|
12
|
+
<script type="application/ld+json" set:html={JSON.stringify(props.graph)} />
|
|
13
|
+
)
|
|
14
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure TypeScript layer for the <Seo> component. The .astro file is a
|
|
3
|
+
* thin template that calls `buildAstroSeoProps` with the user's input
|
|
4
|
+
* and an Astro context (mostly `Astro.url.href`). Keeping the logic
|
|
5
|
+
* here makes it unit-testable with vitest — Astro's renderer isn't
|
|
6
|
+
* easily accessible from vitest.
|
|
7
|
+
*/
|
|
8
|
+
/** Public props surface of the <Seo> component. */
|
|
9
|
+
export interface SeoProps {
|
|
10
|
+
/** Page title. Required. */
|
|
11
|
+
title: string;
|
|
12
|
+
/**
|
|
13
|
+
* Template for composing the full `<title>`. Use `%s` where the
|
|
14
|
+
* title should be substituted. Defaults to the raw `title`.
|
|
15
|
+
*
|
|
16
|
+
* @example `"%s | Joost.blog"` → `"My Post | Joost.blog"`
|
|
17
|
+
*/
|
|
18
|
+
titleTemplate?: string;
|
|
19
|
+
/** Meta description. */
|
|
20
|
+
description?: string;
|
|
21
|
+
/** Canonical URL. Defaults to the current page URL (`Astro.url.href`). */
|
|
22
|
+
canonical?: string | URL;
|
|
23
|
+
/** Open Graph type. Defaults to `'website'`. */
|
|
24
|
+
ogType?: 'website' | 'article' | 'profile' | 'book';
|
|
25
|
+
/** Absolute URL of the share image. */
|
|
26
|
+
ogImage?: string;
|
|
27
|
+
/** Alt text for the share image. */
|
|
28
|
+
ogImageAlt?: string;
|
|
29
|
+
/** Width of the share image in pixels. */
|
|
30
|
+
ogImageWidth?: number;
|
|
31
|
+
/** Height of the share image in pixels. */
|
|
32
|
+
ogImageHeight?: number;
|
|
33
|
+
/** Site name shown in OG tags, e.g. `"Joost.blog"`. */
|
|
34
|
+
siteName?: string;
|
|
35
|
+
/** OG locale. Defaults to `'en_US'`. */
|
|
36
|
+
locale?: string;
|
|
37
|
+
/** Twitter card metadata. */
|
|
38
|
+
twitter?: {
|
|
39
|
+
/** Twitter handle of the site owner, e.g. `'@jdevalk'`. */
|
|
40
|
+
site?: string;
|
|
41
|
+
/** Twitter handle of the author. */
|
|
42
|
+
creator?: string;
|
|
43
|
+
/** Card type. Defaults to `'summary_large_image'`. */
|
|
44
|
+
card?: 'summary' | 'summary_large_image' | 'app' | 'player';
|
|
45
|
+
};
|
|
46
|
+
/**
|
|
47
|
+
* Article-specific OG metadata. Only emitted when `ogType` is
|
|
48
|
+
* `'article'`.
|
|
49
|
+
*/
|
|
50
|
+
article?: {
|
|
51
|
+
publishedTime?: Date | string;
|
|
52
|
+
modifiedTime?: Date | string;
|
|
53
|
+
expirationTime?: Date | string;
|
|
54
|
+
authors?: readonly string[];
|
|
55
|
+
tags?: readonly string[];
|
|
56
|
+
section?: string;
|
|
57
|
+
};
|
|
58
|
+
/** Emit `<meta name="robots" content="noindex, follow">` when `true`. */
|
|
59
|
+
noindex?: boolean;
|
|
60
|
+
/**
|
|
61
|
+
* JSON-LD `@graph` envelope to inject as
|
|
62
|
+
* `<script type="application/ld+json">`. Typically the output of
|
|
63
|
+
* `buildSchemaGraph(...)` or `assembleGraph(...)` from
|
|
64
|
+
* `@jdevalk/seo-graph-core`. Pass `null` or omit to skip JSON-LD.
|
|
65
|
+
*/
|
|
66
|
+
graph?: Record<string, unknown> | null;
|
|
67
|
+
/** Extra `<link>` tags (icons, sitemap, RSS alternate, etc.). */
|
|
68
|
+
extraLinks?: ReadonlyArray<Record<string, string>>;
|
|
69
|
+
/** Extra `<meta>` tags (author, custom fields). */
|
|
70
|
+
extraMeta?: ReadonlyArray<Record<string, string>>;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Shape consumed by `astro-seo`'s `<SEO>` component. We expose it as a
|
|
74
|
+
* type so tests can assert on exact keys / nesting rather than having
|
|
75
|
+
* to parse rendered HTML.
|
|
76
|
+
*/
|
|
77
|
+
export interface AstroSeoProps {
|
|
78
|
+
title: string;
|
|
79
|
+
description?: string;
|
|
80
|
+
canonical: string;
|
|
81
|
+
noindex?: boolean;
|
|
82
|
+
openGraph: {
|
|
83
|
+
basic: {
|
|
84
|
+
title: string;
|
|
85
|
+
type: string;
|
|
86
|
+
image: string;
|
|
87
|
+
url: string;
|
|
88
|
+
};
|
|
89
|
+
optional?: {
|
|
90
|
+
description?: string;
|
|
91
|
+
siteName?: string;
|
|
92
|
+
locale?: string;
|
|
93
|
+
};
|
|
94
|
+
image?: {
|
|
95
|
+
alt?: string;
|
|
96
|
+
width?: number;
|
|
97
|
+
height?: number;
|
|
98
|
+
};
|
|
99
|
+
article?: {
|
|
100
|
+
publishedTime?: string;
|
|
101
|
+
modifiedTime?: string;
|
|
102
|
+
expirationTime?: string;
|
|
103
|
+
authors?: string[];
|
|
104
|
+
tags?: string[];
|
|
105
|
+
section?: string;
|
|
106
|
+
};
|
|
107
|
+
};
|
|
108
|
+
twitter?: {
|
|
109
|
+
card: 'summary' | 'summary_large_image' | 'app' | 'player';
|
|
110
|
+
site?: string;
|
|
111
|
+
creator?: string;
|
|
112
|
+
title: string;
|
|
113
|
+
description?: string;
|
|
114
|
+
image?: string;
|
|
115
|
+
imageAlt?: string;
|
|
116
|
+
};
|
|
117
|
+
extend?: {
|
|
118
|
+
link?: Array<Record<string, string>>;
|
|
119
|
+
meta?: Array<Record<string, string>>;
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Project the public `SeoProps` onto the shape `astro-seo`'s `<SEO>`
|
|
124
|
+
* component expects. Pure function; no Astro runtime access.
|
|
125
|
+
*
|
|
126
|
+
* @param props Props as provided by the user.
|
|
127
|
+
* @param pageUrl The current page URL — usually `Astro.url.href`.
|
|
128
|
+
* Used as the canonical URL when `props.canonical` is
|
|
129
|
+
* not provided, and as the base for the OG `url`.
|
|
130
|
+
*/
|
|
131
|
+
export declare function buildAstroSeoProps(props: SeoProps, pageUrl: string): AstroSeoProps;
|
|
132
|
+
//# sourceMappingURL=seo-props.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"seo-props.d.ts","sourceRoot":"","sources":["../../src/components/seo-props.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure TypeScript layer for the <Seo> component. The .astro file is a
|
|
3
|
+
* thin template that calls `buildAstroSeoProps` with the user's input
|
|
4
|
+
* and an Astro context (mostly `Astro.url.href`). Keeping the logic
|
|
5
|
+
* here makes it unit-testable with vitest — Astro's renderer isn't
|
|
6
|
+
* easily accessible from vitest.
|
|
7
|
+
*/
|
|
8
|
+
function toIsoString(value) {
|
|
9
|
+
if (value === undefined)
|
|
10
|
+
return undefined;
|
|
11
|
+
return value instanceof Date ? value.toISOString() : value;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Project the public `SeoProps` onto the shape `astro-seo`'s `<SEO>`
|
|
15
|
+
* component expects. Pure function; no Astro runtime access.
|
|
16
|
+
*
|
|
17
|
+
* @param props Props as provided by the user.
|
|
18
|
+
* @param pageUrl The current page URL — usually `Astro.url.href`.
|
|
19
|
+
* Used as the canonical URL when `props.canonical` is
|
|
20
|
+
* not provided, and as the base for the OG `url`.
|
|
21
|
+
*/
|
|
22
|
+
export function buildAstroSeoProps(props, pageUrl) {
|
|
23
|
+
const fullTitle = props.titleTemplate
|
|
24
|
+
? props.titleTemplate.replace('%s', props.title)
|
|
25
|
+
: props.title;
|
|
26
|
+
const canonical = props.canonical !== undefined ? props.canonical.toString() : pageUrl;
|
|
27
|
+
const ogType = props.ogType ?? 'website';
|
|
28
|
+
const ogImage = props.ogImage ?? '';
|
|
29
|
+
const openGraph = {
|
|
30
|
+
basic: {
|
|
31
|
+
title: fullTitle,
|
|
32
|
+
type: ogType,
|
|
33
|
+
image: ogImage,
|
|
34
|
+
url: canonical,
|
|
35
|
+
},
|
|
36
|
+
};
|
|
37
|
+
const optional = {};
|
|
38
|
+
if (props.description !== undefined)
|
|
39
|
+
optional.description = props.description;
|
|
40
|
+
if (props.siteName !== undefined)
|
|
41
|
+
optional.siteName = props.siteName;
|
|
42
|
+
optional.locale = props.locale ?? 'en_US';
|
|
43
|
+
openGraph.optional = optional;
|
|
44
|
+
const hasImageMeta = props.ogImageAlt !== undefined ||
|
|
45
|
+
props.ogImageWidth !== undefined ||
|
|
46
|
+
props.ogImageHeight !== undefined;
|
|
47
|
+
if (ogImage && hasImageMeta) {
|
|
48
|
+
const image = {};
|
|
49
|
+
if (props.ogImageAlt !== undefined)
|
|
50
|
+
image.alt = props.ogImageAlt;
|
|
51
|
+
if (props.ogImageWidth !== undefined)
|
|
52
|
+
image.width = props.ogImageWidth;
|
|
53
|
+
if (props.ogImageHeight !== undefined)
|
|
54
|
+
image.height = props.ogImageHeight;
|
|
55
|
+
openGraph.image = image;
|
|
56
|
+
}
|
|
57
|
+
if (ogType === 'article' && props.article) {
|
|
58
|
+
const article = {};
|
|
59
|
+
const published = toIsoString(props.article.publishedTime);
|
|
60
|
+
if (published !== undefined)
|
|
61
|
+
article.publishedTime = published;
|
|
62
|
+
const modified = toIsoString(props.article.modifiedTime);
|
|
63
|
+
if (modified !== undefined)
|
|
64
|
+
article.modifiedTime = modified;
|
|
65
|
+
const expiration = toIsoString(props.article.expirationTime);
|
|
66
|
+
if (expiration !== undefined)
|
|
67
|
+
article.expirationTime = expiration;
|
|
68
|
+
if (props.article.authors !== undefined)
|
|
69
|
+
article.authors = [...props.article.authors];
|
|
70
|
+
if (props.article.tags !== undefined)
|
|
71
|
+
article.tags = [...props.article.tags];
|
|
72
|
+
if (props.article.section !== undefined)
|
|
73
|
+
article.section = props.article.section;
|
|
74
|
+
openGraph.article = article;
|
|
75
|
+
}
|
|
76
|
+
const astroSeo = {
|
|
77
|
+
title: fullTitle,
|
|
78
|
+
canonical,
|
|
79
|
+
openGraph,
|
|
80
|
+
};
|
|
81
|
+
if (props.description !== undefined)
|
|
82
|
+
astroSeo.description = props.description;
|
|
83
|
+
if (props.noindex)
|
|
84
|
+
astroSeo.noindex = true;
|
|
85
|
+
if (props.twitter !== undefined) {
|
|
86
|
+
const twitter = {
|
|
87
|
+
card: props.twitter.card ?? 'summary_large_image',
|
|
88
|
+
title: fullTitle,
|
|
89
|
+
};
|
|
90
|
+
if (props.twitter.site !== undefined)
|
|
91
|
+
twitter.site = props.twitter.site;
|
|
92
|
+
if (props.twitter.creator !== undefined)
|
|
93
|
+
twitter.creator = props.twitter.creator;
|
|
94
|
+
if (props.description !== undefined)
|
|
95
|
+
twitter.description = props.description;
|
|
96
|
+
if (ogImage)
|
|
97
|
+
twitter.image = ogImage;
|
|
98
|
+
if (props.ogImageAlt !== undefined)
|
|
99
|
+
twitter.imageAlt = props.ogImageAlt;
|
|
100
|
+
astroSeo.twitter = twitter;
|
|
101
|
+
}
|
|
102
|
+
if ((props.extraLinks !== undefined && props.extraLinks.length > 0) ||
|
|
103
|
+
(props.extraMeta !== undefined && props.extraMeta.length > 0)) {
|
|
104
|
+
const extend = {};
|
|
105
|
+
if (props.extraLinks !== undefined && props.extraLinks.length > 0) {
|
|
106
|
+
extend.link = props.extraLinks.map((link) => ({ ...link }));
|
|
107
|
+
}
|
|
108
|
+
if (props.extraMeta !== undefined && props.extraMeta.length > 0) {
|
|
109
|
+
extend.meta = props.extraMeta.map((meta) => ({ ...meta }));
|
|
110
|
+
}
|
|
111
|
+
astroSeo.extend = extend;
|
|
112
|
+
}
|
|
113
|
+
return astroSeo;
|
|
114
|
+
}
|
|
115
|
+
//# sourceMappingURL=seo-props.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"seo-props.js","sourceRoot":"","sources":["../../src/components/seo-props.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
/**
|
|
3
|
+
* Astro content-collection `image` function signature, captured as `any`
|
|
4
|
+
* to avoid importing from the `astro:content` virtual module (which
|
|
5
|
+
* only resolves inside an Astro project, not in a standalone package).
|
|
6
|
+
* Users pass through their own `image` from Astro's schema callback.
|
|
7
|
+
*/
|
|
8
|
+
type AstroImageFunction = any;
|
|
9
|
+
/**
|
|
10
|
+
* Zod schema for an image field on a content entry. Pair with Astro's
|
|
11
|
+
* `image()` from the collection schema factory so the image is processed
|
|
12
|
+
* through Astro's asset pipeline.
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* ```ts
|
|
16
|
+
* import { defineCollection, z } from 'astro:content';
|
|
17
|
+
* import { imageSchema } from '@jdevalk/astro-seo-graph';
|
|
18
|
+
*
|
|
19
|
+
* const blog = defineCollection({
|
|
20
|
+
* schema: ({ image }) =>
|
|
21
|
+
* z.object({
|
|
22
|
+
* title: z.string(),
|
|
23
|
+
* featureImage: imageSchema(image).optional(),
|
|
24
|
+
* }),
|
|
25
|
+
* });
|
|
26
|
+
* ```
|
|
27
|
+
*/
|
|
28
|
+
export declare function imageSchema(image: AstroImageFunction): z.ZodObject<{
|
|
29
|
+
src: any;
|
|
30
|
+
alt: z.ZodOptional<z.ZodString>;
|
|
31
|
+
}, "strip", z.ZodTypeAny, {
|
|
32
|
+
src?: any;
|
|
33
|
+
alt?: string | undefined;
|
|
34
|
+
}, {
|
|
35
|
+
src?: any;
|
|
36
|
+
alt?: string | undefined;
|
|
37
|
+
}>;
|
|
38
|
+
/**
|
|
39
|
+
* Zod schema for a nested `seo` field holding per-entry SEO overrides:
|
|
40
|
+
* title, description, share image, and page type. Enforces reasonable
|
|
41
|
+
* length limits (5–120 chars for title, 15–160 for description) as a
|
|
42
|
+
* lint.
|
|
43
|
+
*
|
|
44
|
+
* @example
|
|
45
|
+
* ```ts
|
|
46
|
+
* const blog = defineCollection({
|
|
47
|
+
* schema: ({ image }) =>
|
|
48
|
+
* z.object({
|
|
49
|
+
* title: z.string(),
|
|
50
|
+
* seo: seoSchema(image).optional(),
|
|
51
|
+
* }),
|
|
52
|
+
* });
|
|
53
|
+
* ```
|
|
54
|
+
*/
|
|
55
|
+
export declare function seoSchema(image: AstroImageFunction): z.ZodObject<{
|
|
56
|
+
title: z.ZodOptional<z.ZodString>;
|
|
57
|
+
description: z.ZodOptional<z.ZodString>;
|
|
58
|
+
image: z.ZodOptional<z.ZodObject<{
|
|
59
|
+
src: any;
|
|
60
|
+
alt: z.ZodOptional<z.ZodString>;
|
|
61
|
+
}, "strip", z.ZodTypeAny, {
|
|
62
|
+
src?: any;
|
|
63
|
+
alt?: string | undefined;
|
|
64
|
+
}, {
|
|
65
|
+
src?: any;
|
|
66
|
+
alt?: string | undefined;
|
|
67
|
+
}>>;
|
|
68
|
+
pageType: z.ZodDefault<z.ZodEnum<["website", "article"]>>;
|
|
69
|
+
}, "strip", z.ZodTypeAny, {
|
|
70
|
+
pageType: "website" | "article";
|
|
71
|
+
title?: string | undefined;
|
|
72
|
+
description?: string | undefined;
|
|
73
|
+
image?: {
|
|
74
|
+
src?: any;
|
|
75
|
+
alt?: string | undefined;
|
|
76
|
+
} | undefined;
|
|
77
|
+
}, {
|
|
78
|
+
title?: string | undefined;
|
|
79
|
+
description?: string | undefined;
|
|
80
|
+
image?: {
|
|
81
|
+
src?: any;
|
|
82
|
+
alt?: string | undefined;
|
|
83
|
+
} | undefined;
|
|
84
|
+
pageType?: "website" | "article" | undefined;
|
|
85
|
+
}>;
|
|
86
|
+
export {};
|
|
87
|
+
//# sourceMappingURL=content-helpers.d.ts.map
|
|
@@ -0,0 +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;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,kBAAkB;;;;;;;;;GAKpD;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,SAAS,CAAC,KAAK,EAAE,kBAAkB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAOlD"}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
/**
|
|
3
|
+
* Zod schema for an image field on a content entry. Pair with Astro's
|
|
4
|
+
* `image()` from the collection schema factory so the image is processed
|
|
5
|
+
* through Astro's asset pipeline.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```ts
|
|
9
|
+
* import { defineCollection, z } from 'astro:content';
|
|
10
|
+
* import { imageSchema } from '@jdevalk/astro-seo-graph';
|
|
11
|
+
*
|
|
12
|
+
* const blog = defineCollection({
|
|
13
|
+
* schema: ({ image }) =>
|
|
14
|
+
* z.object({
|
|
15
|
+
* title: z.string(),
|
|
16
|
+
* featureImage: imageSchema(image).optional(),
|
|
17
|
+
* }),
|
|
18
|
+
* });
|
|
19
|
+
* ```
|
|
20
|
+
*/
|
|
21
|
+
export function imageSchema(image) {
|
|
22
|
+
return z.object({
|
|
23
|
+
src: image(),
|
|
24
|
+
alt: z.string().optional(),
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Zod schema for a nested `seo` field holding per-entry SEO overrides:
|
|
29
|
+
* title, description, share image, and page type. Enforces reasonable
|
|
30
|
+
* length limits (5–120 chars for title, 15–160 for description) as a
|
|
31
|
+
* lint.
|
|
32
|
+
*
|
|
33
|
+
* @example
|
|
34
|
+
* ```ts
|
|
35
|
+
* const blog = defineCollection({
|
|
36
|
+
* schema: ({ image }) =>
|
|
37
|
+
* z.object({
|
|
38
|
+
* title: z.string(),
|
|
39
|
+
* seo: seoSchema(image).optional(),
|
|
40
|
+
* }),
|
|
41
|
+
* });
|
|
42
|
+
* ```
|
|
43
|
+
*/
|
|
44
|
+
export function seoSchema(image) {
|
|
45
|
+
return z.object({
|
|
46
|
+
title: z.string().min(5).max(120).optional(),
|
|
47
|
+
description: z.string().min(15).max(160).optional(),
|
|
48
|
+
image: imageSchema(image).optional(),
|
|
49
|
+
pageType: z.enum(['website', 'article']).default('website'),
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
//# sourceMappingURL=content-helpers.js.map
|
|
@@ -0,0 +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;;;;;;;;;;;;;;;;;;GAkBG;AACH,MAAM,UAAU,WAAW,CAAC,KAAyB;IACjD,OAAO,CAAC,CAAC,MAAM,CAAC;QACZ,GAAG,EAAE,KAAK,EAAE;QACZ,GAAG,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;KAC7B,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
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export { aggregate } from './aggregator.js';
|
|
2
|
+
export type { AggregatorOptions, AggregatedGraph } from './aggregator.js';
|
|
3
|
+
export { createSchemaEndpoint, createSchemaMap } from './routes.js';
|
|
4
|
+
export type { SchemaEndpointOptions, SchemaMapEntry, SchemaMapOptions } from './routes.js';
|
|
5
|
+
export { seoSchema, imageSchema } from './content-helpers.js';
|
|
6
|
+
export { buildAstroSeoProps } from './components/seo-props.js';
|
|
7
|
+
export type { SeoProps, AstroSeoProps } from './components/seo-props.js';
|
|
8
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +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"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// @jdevalk/astro-seo-graph — Astro integration for agent-ready schema.org SEO.
|
|
2
|
+
//
|
|
3
|
+
// The `<Seo>` component lives at `./Seo.astro` and must be imported via
|
|
4
|
+
// the subpath export: `import Seo from '@jdevalk/astro-seo-graph/Seo.astro'`.
|
|
5
|
+
// Its props surface is `SeoProps`, re-exported below.
|
|
6
|
+
export { aggregate } from './aggregator.js';
|
|
7
|
+
export { createSchemaEndpoint, createSchemaMap } from './routes.js';
|
|
8
|
+
export { seoSchema, imageSchema } from './content-helpers.js';
|
|
9
|
+
export { buildAstroSeoProps } from './components/seo-props.js';
|
|
10
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +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"}
|
package/dist/routes.d.ts
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import type { APIRoute } from 'astro';
|
|
2
|
+
import type { GraphEntity } from '@jdevalk/seo-graph-core';
|
|
3
|
+
export interface SchemaEndpointOptions<Entry> {
|
|
4
|
+
/**
|
|
5
|
+
* Async source of content entries. Usually a thin wrapper around
|
|
6
|
+
* Astro's `getCollection`, e.g. `() => getCollection('blog')`, or
|
|
7
|
+
* a filtered variant: `() => (await getCollection('blog')).filter(isPublished)`.
|
|
8
|
+
*/
|
|
9
|
+
entries: () => Promise<readonly Entry[]>;
|
|
10
|
+
/**
|
|
11
|
+
* Map a single entry to an array of schema.org pieces. The pieces
|
|
12
|
+
* are assembled into a single `@graph` via the aggregator and
|
|
13
|
+
* deduplicated by `@id`.
|
|
14
|
+
*/
|
|
15
|
+
mapper: (entry: Entry) => ReadonlyArray<GraphEntity>;
|
|
16
|
+
/**
|
|
17
|
+
* `Cache-Control` header value. Defaults to `max-age=300` (5 min).
|
|
18
|
+
* Pass `null` to omit the header entirely.
|
|
19
|
+
*/
|
|
20
|
+
cacheControl?: string | null;
|
|
21
|
+
/**
|
|
22
|
+
* `Content-Type` header value. Defaults to `application/ld+json`.
|
|
23
|
+
*/
|
|
24
|
+
contentType?: string;
|
|
25
|
+
/**
|
|
26
|
+
* JSON output indentation. Defaults to `2`. Pass `0` for compact
|
|
27
|
+
* output.
|
|
28
|
+
*/
|
|
29
|
+
indent?: number;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Returns an Astro `APIRoute` that serves a corpus-wide schema.org
|
|
33
|
+
* `@graph` for a content collection as JSON-LD. Drop the returned
|
|
34
|
+
* handler into an `.ts` file under `src/pages/`.
|
|
35
|
+
*
|
|
36
|
+
* @example
|
|
37
|
+
* ```ts
|
|
38
|
+
* // src/pages/schema/post.json.ts
|
|
39
|
+
* import { getCollection } from 'astro:content';
|
|
40
|
+
* import { createSchemaEndpoint } from '@jdevalk/astro-seo-graph';
|
|
41
|
+
* import { buildArticle, buildWebPage } from '@jdevalk/seo-graph-core';
|
|
42
|
+
*
|
|
43
|
+
* export const GET = createSchemaEndpoint({
|
|
44
|
+
* entries: () => getCollection('blog'),
|
|
45
|
+
* mapper: (post) => [
|
|
46
|
+
* buildWebPage({ url: `https://example.com/${post.id}/`, ... }, ids),
|
|
47
|
+
* buildArticle({ url: `https://example.com/${post.id}/`, ... }, ids),
|
|
48
|
+
* ],
|
|
49
|
+
* });
|
|
50
|
+
* ```
|
|
51
|
+
*/
|
|
52
|
+
export declare function createSchemaEndpoint<Entry>(options: SchemaEndpointOptions<Entry>): APIRoute;
|
|
53
|
+
export interface SchemaMapEntry {
|
|
54
|
+
/**
|
|
55
|
+
* URL path relative to the site root, e.g. `/schema/post.json`.
|
|
56
|
+
* May start with or without a leading slash.
|
|
57
|
+
*/
|
|
58
|
+
path: string;
|
|
59
|
+
/** When the resource at this path was last modified. */
|
|
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
|
+
}
|
|
66
|
+
export interface SchemaMapOptions {
|
|
67
|
+
/** Canonical site URL. Trailing slash is stripped. */
|
|
68
|
+
siteUrl: string;
|
|
69
|
+
/** One entry per schema endpoint. */
|
|
70
|
+
entries: readonly SchemaMapEntry[];
|
|
71
|
+
/** Defaults to `max-age=300`. Pass `null` to omit. */
|
|
72
|
+
cacheControl?: string | null;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Returns an Astro `APIRoute` that serves a sitemap-style XML listing
|
|
76
|
+
* of the site's schema.org endpoints. Agent crawlers can use this as
|
|
77
|
+
* a discovery point for a site's JSON-LD knowledge graph.
|
|
78
|
+
*
|
|
79
|
+
* @example
|
|
80
|
+
* ```ts
|
|
81
|
+
* // src/pages/schemamap.xml.ts
|
|
82
|
+
* import { createSchemaMap } from '@jdevalk/astro-seo-graph';
|
|
83
|
+
*
|
|
84
|
+
* export const GET = createSchemaMap({
|
|
85
|
+
* siteUrl: 'https://example.com',
|
|
86
|
+
* entries: [
|
|
87
|
+
* { path: '/schema/post.json', lastModified: new Date('2026-04-07') },
|
|
88
|
+
* { path: '/schema/video.json', lastModified: new Date('2026-03-13') },
|
|
89
|
+
* ],
|
|
90
|
+
* });
|
|
91
|
+
* ```
|
|
92
|
+
*/
|
|
93
|
+
export declare function createSchemaMap(options: SchemaMapOptions): APIRoute;
|
|
94
|
+
//# sourceMappingURL=routes.d.ts.map
|
|
@@ -0,0 +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,CAY3F;AAED,MAAM,WAAW,cAAc;IAC3B;;;OAGG;IACH,IAAI,EAAE,MAAM,CAAC;IACb,wDAAwD;IACxD,YAAY,EAAE,IAAI,CAAC;IACnB,6BAA6B;IAC7B,UAAU,CAAC,EAAE,QAAQ,GAAG,QAAQ,GAAG,OAAO,GAAG,QAAQ,GAAG,SAAS,GAAG,QAAQ,GAAG,OAAO,CAAC;IACvF,kCAAkC;IAClC,QAAQ,CAAC,EAAE,MAAM,CAAC;CACrB;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,CA8BnE"}
|
package/dist/routes.js
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { aggregate } from './aggregator.js';
|
|
2
|
+
/**
|
|
3
|
+
* Returns an Astro `APIRoute` that serves a corpus-wide schema.org
|
|
4
|
+
* `@graph` for a content collection as JSON-LD. Drop the returned
|
|
5
|
+
* handler into an `.ts` file under `src/pages/`.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```ts
|
|
9
|
+
* // src/pages/schema/post.json.ts
|
|
10
|
+
* import { getCollection } from 'astro:content';
|
|
11
|
+
* import { createSchemaEndpoint } from '@jdevalk/astro-seo-graph';
|
|
12
|
+
* import { buildArticle, buildWebPage } from '@jdevalk/seo-graph-core';
|
|
13
|
+
*
|
|
14
|
+
* export const GET = createSchemaEndpoint({
|
|
15
|
+
* entries: () => getCollection('blog'),
|
|
16
|
+
* mapper: (post) => [
|
|
17
|
+
* buildWebPage({ url: `https://example.com/${post.id}/`, ... }, ids),
|
|
18
|
+
* buildArticle({ url: `https://example.com/${post.id}/`, ... }, ids),
|
|
19
|
+
* ],
|
|
20
|
+
* });
|
|
21
|
+
* ```
|
|
22
|
+
*/
|
|
23
|
+
export function createSchemaEndpoint(options) {
|
|
24
|
+
const cacheControl = options.cacheControl === undefined ? 'max-age=300' : options.cacheControl;
|
|
25
|
+
const contentType = options.contentType ?? 'application/ld+json';
|
|
26
|
+
const indent = options.indent ?? 2;
|
|
27
|
+
return async () => {
|
|
28
|
+
const entries = await options.entries();
|
|
29
|
+
const graph = aggregate({ entries, mapper: options.mapper });
|
|
30
|
+
const headers = { 'Content-Type': contentType };
|
|
31
|
+
if (cacheControl !== null)
|
|
32
|
+
headers['Cache-Control'] = cacheControl;
|
|
33
|
+
return new Response(JSON.stringify(graph, null, indent), { headers });
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
function escapeXml(value) {
|
|
37
|
+
return value
|
|
38
|
+
.replace(/&/g, '&')
|
|
39
|
+
.replace(/</g, '<')
|
|
40
|
+
.replace(/>/g, '>')
|
|
41
|
+
.replace(/"/g, '"')
|
|
42
|
+
.replace(/'/g, ''');
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Returns an Astro `APIRoute` that serves a sitemap-style XML listing
|
|
46
|
+
* of the site's schema.org endpoints. Agent crawlers can use this as
|
|
47
|
+
* a discovery point for a site's JSON-LD knowledge graph.
|
|
48
|
+
*
|
|
49
|
+
* @example
|
|
50
|
+
* ```ts
|
|
51
|
+
* // src/pages/schemamap.xml.ts
|
|
52
|
+
* import { createSchemaMap } from '@jdevalk/astro-seo-graph';
|
|
53
|
+
*
|
|
54
|
+
* export const GET = createSchemaMap({
|
|
55
|
+
* siteUrl: 'https://example.com',
|
|
56
|
+
* entries: [
|
|
57
|
+
* { path: '/schema/post.json', lastModified: new Date('2026-04-07') },
|
|
58
|
+
* { path: '/schema/video.json', lastModified: new Date('2026-03-13') },
|
|
59
|
+
* ],
|
|
60
|
+
* });
|
|
61
|
+
* ```
|
|
62
|
+
*/
|
|
63
|
+
export function createSchemaMap(options) {
|
|
64
|
+
const cacheControl = options.cacheControl === undefined ? 'max-age=300' : options.cacheControl;
|
|
65
|
+
const site = options.siteUrl.replace(/\/+$/, '');
|
|
66
|
+
return async () => {
|
|
67
|
+
const urls = options.entries
|
|
68
|
+
.map((entry) => {
|
|
69
|
+
const pathPart = entry.path.startsWith('/') ? entry.path : `/${entry.path}`;
|
|
70
|
+
const loc = escapeXml(`${site}${pathPart}`);
|
|
71
|
+
const lastmod = entry.lastModified.toISOString().split('T')[0];
|
|
72
|
+
const changefreq = entry.changeFreq ?? 'daily';
|
|
73
|
+
const priority = entry.priority ?? 0.8;
|
|
74
|
+
return ` <url contentType="structuredData/schema.org">
|
|
75
|
+
<loc>${loc}</loc>
|
|
76
|
+
<lastmod>${lastmod}</lastmod>
|
|
77
|
+
<changefreq>${changefreq}</changefreq>
|
|
78
|
+
<priority>${priority}</priority>
|
|
79
|
+
</url>`;
|
|
80
|
+
})
|
|
81
|
+
.join('\n');
|
|
82
|
+
const xml = `<?xml version="1.0" encoding="UTF-8"?>
|
|
83
|
+
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
|
84
|
+
${urls}
|
|
85
|
+
</urlset>`;
|
|
86
|
+
const headers = { 'Content-Type': 'application/xml' };
|
|
87
|
+
if (cacheControl !== null)
|
|
88
|
+
headers['Cache-Control'] = cacheControl;
|
|
89
|
+
return new Response(xml, { headers });
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
//# sourceMappingURL=routes.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"routes.js","sourceRoot":"","sources":["../src/routes.ts"],"names":[],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@jdevalk/astro-seo-graph",
|
|
3
|
+
"version": "0.1.0-alpha.1",
|
|
4
|
+
"description": "Astro integration for @jdevalk/seo-graph-core. Seo component, route factories, content-collection aggregator, Zod content helpers.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"astro",
|
|
7
|
+
"astro-integration",
|
|
8
|
+
"astro-component",
|
|
9
|
+
"schema.org",
|
|
10
|
+
"json-ld",
|
|
11
|
+
"seo",
|
|
12
|
+
"agent-ready",
|
|
13
|
+
"structured-data"
|
|
14
|
+
],
|
|
15
|
+
"author": "Joost de Valk <joost@joost.blog>",
|
|
16
|
+
"license": "MIT",
|
|
17
|
+
"type": "module",
|
|
18
|
+
"main": "./dist/index.js",
|
|
19
|
+
"types": "./dist/index.d.ts",
|
|
20
|
+
"exports": {
|
|
21
|
+
".": {
|
|
22
|
+
"types": "./dist/index.d.ts",
|
|
23
|
+
"import": "./dist/index.js"
|
|
24
|
+
},
|
|
25
|
+
"./Seo.astro": "./dist/components/Seo.astro"
|
|
26
|
+
},
|
|
27
|
+
"files": [
|
|
28
|
+
"dist",
|
|
29
|
+
"README.md",
|
|
30
|
+
"LICENSE"
|
|
31
|
+
],
|
|
32
|
+
"repository": {
|
|
33
|
+
"type": "git",
|
|
34
|
+
"url": "git+https://github.com/jdevalk/seo-graph.git",
|
|
35
|
+
"directory": "packages/astro-seo-graph"
|
|
36
|
+
},
|
|
37
|
+
"bugs": "https://github.com/jdevalk/seo-graph/issues",
|
|
38
|
+
"homepage": "https://github.com/jdevalk/seo-graph/tree/main/packages/astro-seo-graph#readme",
|
|
39
|
+
"publishConfig": {
|
|
40
|
+
"access": "public"
|
|
41
|
+
},
|
|
42
|
+
"peerDependencies": {
|
|
43
|
+
"astro": "^5.0.0 || ^6.0.0"
|
|
44
|
+
},
|
|
45
|
+
"dependencies": {
|
|
46
|
+
"astro-seo": "^1.1.0",
|
|
47
|
+
"schema-dts": "^2.0.0",
|
|
48
|
+
"zod": "^3.24.0",
|
|
49
|
+
"@jdevalk/seo-graph-core": "0.1.0-alpha.0"
|
|
50
|
+
},
|
|
51
|
+
"devDependencies": {
|
|
52
|
+
"@types/node": "^22.0.0",
|
|
53
|
+
"astro": "^6.0.5",
|
|
54
|
+
"typescript": "^5.6.0",
|
|
55
|
+
"vitest": "^2.0.0"
|
|
56
|
+
},
|
|
57
|
+
"scripts": {
|
|
58
|
+
"build": "tsc -p tsconfig.build.json && mkdir -p dist/components && cp src/components/*.astro dist/components/",
|
|
59
|
+
"typecheck": "tsc -p tsconfig.json",
|
|
60
|
+
"test": "vitest run"
|
|
61
|
+
}
|
|
62
|
+
}
|