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