@jdevalk/astro-seo-graph 0.6.0 → 0.8.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 +83 -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 +24 -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 +61 -0
- package/dist/integration.d.ts.map +1 -1
- package/dist/integration.js +148 -20
- 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,15 @@ 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
|
+
- Warns about duplicate `<title>` or meta description values across the
|
|
343
|
+
built corpus.
|
|
344
|
+
- Optionally submits built URLs to [IndexNow](https://www.indexnow.org)
|
|
345
|
+
after the build completes.
|
|
319
346
|
|
|
320
347
|
```js
|
|
321
348
|
// astro.config.mjs
|
|
@@ -323,17 +350,67 @@ import { defineConfig } from 'astro/config';
|
|
|
323
350
|
import seoGraph from '@jdevalk/astro-seo-graph/integration';
|
|
324
351
|
|
|
325
352
|
export default defineConfig({
|
|
326
|
-
integrations: [
|
|
353
|
+
integrations: [
|
|
354
|
+
seoGraph({
|
|
355
|
+
indexNow: {
|
|
356
|
+
key: process.env.INDEXNOW_KEY!,
|
|
357
|
+
host: 'example.com',
|
|
358
|
+
siteUrl: 'https://example.com',
|
|
359
|
+
},
|
|
360
|
+
}),
|
|
361
|
+
],
|
|
327
362
|
});
|
|
328
363
|
```
|
|
329
364
|
|
|
330
365
|
Options:
|
|
331
366
|
|
|
332
|
-
| Prop
|
|
333
|
-
|
|
|
334
|
-
| `validateH1`
|
|
367
|
+
| Prop | Default | Description |
|
|
368
|
+
| ------------------------ | ------- | --------------------------------------------------------------- |
|
|
369
|
+
| `validateH1` | `true` | Warn about pages without exactly one `<h1>` |
|
|
370
|
+
| `validateUniqueMetadata` | `true` | Warn about duplicate `<title>` or meta description across pages |
|
|
371
|
+
| `indexNow` | — | Submit built URLs to IndexNow. See below for sub-options. |
|
|
372
|
+
|
|
373
|
+
`indexNow` sub-options: `key` (8–128 hex chars), `host` (bare host, e.g.
|
|
374
|
+
`example.com`), `siteUrl` (absolute origin), `keyLocation?` (defaults to
|
|
375
|
+
`https://<host>/<key>.txt`), `endpoint?` (defaults to `api.indexnow.org`),
|
|
376
|
+
`filter?` (drop URLs for which the callback returns `false`).
|
|
377
|
+
|
|
378
|
+
`index.html` paths are rewritten to their trailing-slash form. Only static
|
|
379
|
+
pages are checked/submitted (SSR pages aren't on disk at build time).
|
|
380
|
+
|
|
381
|
+
## IndexNow key route
|
|
382
|
+
|
|
383
|
+
Serve the IndexNow key-verification file at `/<key>.txt` so participating
|
|
384
|
+
engines can confirm host ownership:
|
|
385
|
+
|
|
386
|
+
```ts
|
|
387
|
+
// src/pages/[your-key-here].txt.ts
|
|
388
|
+
import { createIndexNowKeyRoute } from '@jdevalk/astro-seo-graph';
|
|
389
|
+
|
|
390
|
+
export const GET = createIndexNowKeyRoute({ key: 'your-key-here' });
|
|
391
|
+
```
|
|
392
|
+
|
|
393
|
+
The filename (minus `.txt.ts`) must equal the key. Pair this with the
|
|
394
|
+
`indexNow` integration option above, or call `submitToIndexNow` from your
|
|
395
|
+
own deploy hook.
|
|
396
|
+
|
|
397
|
+
> **Deploy the key file first.** IndexNow verifies host ownership by
|
|
398
|
+
> fetching `https://<host>/<key>.txt` on every submission. Submissions
|
|
399
|
+
> sent before the key file is reachable in production get rejected
|
|
400
|
+
> (HTTP 403) and the key is treated as invalid going forward — you'll
|
|
401
|
+
> have to rotate it. Ship the route, deploy, confirm the `.txt` loads
|
|
402
|
+
> over HTTPS, *then* enable `indexNow` in the integration.
|
|
403
|
+
|
|
404
|
+
## Validating your output
|
|
405
|
+
|
|
406
|
+
The build-time integration checks only catch a narrow set of issues.
|
|
407
|
+
After deploying, verify the rendered JSON-LD against:
|
|
408
|
+
|
|
409
|
+
1. [Google Rich Results Test](https://search.google.com/test/rich-results)
|
|
410
|
+
2. [Schema.org Validator](https://validator.schema.org/)
|
|
335
411
|
|
|
336
|
-
|
|
412
|
+
See [AGENTS.md](https://github.com/jdevalk/seo-graph/blob/main/AGENTS.md#validating-your-output)
|
|
413
|
+
for details on what to look for.
|
|
337
414
|
|
|
338
415
|
## License
|
|
339
416
|
|
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,24 @@
|
|
|
1
|
+
import type { APIRoute } from 'astro';
|
|
2
|
+
export interface IndexNowKeyRouteOptions {
|
|
3
|
+
/** IndexNow key (8–128 hex characters). */
|
|
4
|
+
key: string;
|
|
5
|
+
/** Defaults to `public, max-age=86400`. Pass `null` to omit. */
|
|
6
|
+
cacheControl?: string | null;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Returns an Astro `APIRoute` that serves the IndexNow key verification
|
|
10
|
+
* file. Place this at `src/pages/[key].txt.ts` or `src/pages/<key>.txt.ts`
|
|
11
|
+
* so it resolves to `/<key>.txt` on the deployed site.
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* ```ts
|
|
15
|
+
* // src/pages/your-key-here.txt.ts
|
|
16
|
+
* import { createIndexNowKeyRoute } from '@jdevalk/astro-seo-graph';
|
|
17
|
+
*
|
|
18
|
+
* export const GET = createIndexNowKeyRoute({ key: 'your-key-here' });
|
|
19
|
+
* ```
|
|
20
|
+
*/
|
|
21
|
+
export declare function createIndexNowKeyRoute(options: IndexNowKeyRouteOptions): APIRoute;
|
|
22
|
+
export { submitToIndexNow, validateIndexNowKey } from '@jdevalk/seo-graph-core';
|
|
23
|
+
export type { IndexNowSubmitResult } from '@jdevalk/seo-graph-core';
|
|
24
|
+
//# 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;AAGtC,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,MAAM,yBAAyB,CAAC;AAChF,YAAY,EAAE,oBAAoB,EAAE,MAAM,yBAAyB,CAAC"}
|
package/dist/indexnow.js
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { getIndexNowKeyFileContent } 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 } from '@jdevalk/seo-graph-core';
|
|
28
|
+
//# sourceMappingURL=indexnow.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"indexnow.js","sourceRoot":"","sources":["../src/indexnow.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,yBAAyB,EAAE,MAAM,yBAAyB,CAAC;AASpE;;;;;;;;;;;;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,MAAM,yBAAyB,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,13 +46,47 @@ export interface SeoGraphIntegrationOptions {
|
|
|
19
46
|
* not present on disk at build time).
|
|
20
47
|
*/
|
|
21
48
|
validateH1?: boolean;
|
|
49
|
+
/**
|
|
50
|
+
* Warn when two or more built pages share the same `<title>` or
|
|
51
|
+
* meta description. Duplicate metadata is an SEO smell that can only
|
|
52
|
+
* be detected across the whole corpus. Defaults to `true`.
|
|
53
|
+
*
|
|
54
|
+
* Pages without a title or description are reported separately by
|
|
55
|
+
* the H1 validator's siblings — this check only compares values that
|
|
56
|
+
* are present on multiple pages.
|
|
57
|
+
*/
|
|
58
|
+
validateUniqueMetadata?: boolean;
|
|
59
|
+
/**
|
|
60
|
+
* Submit built URLs to IndexNow after the build completes. Omit to
|
|
61
|
+
* disable. Only URLs on `host` are submitted; URLs with trailing
|
|
62
|
+
* `index.html` are rewritten to their directory form.
|
|
63
|
+
*/
|
|
64
|
+
indexNow?: IndexNowIntegrationOptions;
|
|
22
65
|
}
|
|
66
|
+
/**
|
|
67
|
+
* Turn a built HTML file path (relative to the outDir, e.g.
|
|
68
|
+
* `blog/post/index.html`) into an absolute URL on `siteUrl`. Rewrites
|
|
69
|
+
* `index.html` to a trailing slash and strips other `.html` extensions
|
|
70
|
+
* — matching Astro's default `trailingSlash: 'ignore'` output layout.
|
|
71
|
+
*/
|
|
72
|
+
export declare function htmlFileToUrl(relativePath: string, siteUrl: string): string;
|
|
23
73
|
/**
|
|
24
74
|
* Count `<h1>` elements in an HTML string. Matches the opening tag only;
|
|
25
75
|
* tolerant of attributes and whitespace. Doesn't parse — good enough for
|
|
26
76
|
* a lint warning.
|
|
27
77
|
*/
|
|
28
78
|
export declare function countH1s(html: string): number;
|
|
79
|
+
/**
|
|
80
|
+
* Extract the first `<title>` element's text content. Returns `null` when
|
|
81
|
+
* no title tag is found. Whitespace-collapsed and entity-decoded so
|
|
82
|
+
* duplicate-detection compares rendered text, not raw HTML.
|
|
83
|
+
*/
|
|
84
|
+
export declare function extractTitle(html: string): string | null;
|
|
85
|
+
/**
|
|
86
|
+
* Extract the `content` attribute of the first `<meta name="description">`
|
|
87
|
+
* tag. Returns `null` when absent. Entity-decoded for duplicate detection.
|
|
88
|
+
*/
|
|
89
|
+
export declare function extractMetaDescription(html: string): string | null;
|
|
29
90
|
/**
|
|
30
91
|
* Astro integration for `@jdevalk/astro-seo-graph`.
|
|
31
92
|
*
|
|
@@ -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;;;;;;;;OAQG;IACH,sBAAsB,CAAC,EAAE,OAAO,CAAC;IACjC;;;;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;;;;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,CA0H/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
|
|
@@ -10,6 +35,44 @@ export function countH1s(html) {
|
|
|
10
35
|
const matches = html.match(/<h1[\s>]/gi);
|
|
11
36
|
return matches ? matches.length : 0;
|
|
12
37
|
}
|
|
38
|
+
const HTML_ENTITIES = {
|
|
39
|
+
'&': '&',
|
|
40
|
+
'<': '<',
|
|
41
|
+
'>': '>',
|
|
42
|
+
'"': '"',
|
|
43
|
+
''': "'",
|
|
44
|
+
''': "'",
|
|
45
|
+
' ': ' ',
|
|
46
|
+
};
|
|
47
|
+
function decodeHtmlEntities(value) {
|
|
48
|
+
return value.replace(/&(?:amp|lt|gt|quot|apos|#39|nbsp);/g, (m) => HTML_ENTITIES[m] ?? m);
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Extract the first `<title>` element's text content. Returns `null` when
|
|
52
|
+
* no title tag is found. Whitespace-collapsed and entity-decoded so
|
|
53
|
+
* duplicate-detection compares rendered text, not raw HTML.
|
|
54
|
+
*/
|
|
55
|
+
export function extractTitle(html) {
|
|
56
|
+
const match = html.match(/<title[^>]*>([\s\S]*?)<\/title>/i);
|
|
57
|
+
if (!match)
|
|
58
|
+
return null;
|
|
59
|
+
const text = decodeHtmlEntities(match[1]).replace(/\s+/g, ' ').trim();
|
|
60
|
+
return text.length > 0 ? text : null;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Extract the `content` attribute of the first `<meta name="description">`
|
|
64
|
+
* tag. Returns `null` when absent. Entity-decoded for duplicate detection.
|
|
65
|
+
*/
|
|
66
|
+
export function extractMetaDescription(html) {
|
|
67
|
+
const re = /<meta\s+[^>]*name\s*=\s*["']description["'][^>]*content\s*=\s*["']([^"']*)["'][^>]*>|<meta\s+[^>]*content\s*=\s*["']([^"']*)["'][^>]*name\s*=\s*["']description["'][^>]*>/i;
|
|
68
|
+
const match = html.match(re);
|
|
69
|
+
if (!match)
|
|
70
|
+
return null;
|
|
71
|
+
const raw = (match[1] ?? match[2] ?? '').trim();
|
|
72
|
+
if (!raw)
|
|
73
|
+
return null;
|
|
74
|
+
return decodeHtmlEntities(raw);
|
|
75
|
+
}
|
|
13
76
|
async function collectHtmlFiles(dir, base = dir) {
|
|
14
77
|
const entries = await readdir(dir, { withFileTypes: true });
|
|
15
78
|
const files = [];
|
|
@@ -42,34 +105,99 @@ async function collectHtmlFiles(dir, base = dir) {
|
|
|
42
105
|
* ```
|
|
43
106
|
*/
|
|
44
107
|
export default function seoGraph(options = {}) {
|
|
45
|
-
const { validateH1 = true } = options;
|
|
108
|
+
const { validateH1 = true, validateUniqueMetadata = true, indexNow } = options;
|
|
46
109
|
return {
|
|
47
110
|
name: '@jdevalk/astro-seo-graph',
|
|
48
111
|
hooks: {
|
|
49
112
|
'astro:build:done': async ({ dir, logger }) => {
|
|
50
|
-
if (!validateH1)
|
|
51
|
-
return;
|
|
52
113
|
const buildDir = fileURLToPath(dir);
|
|
53
|
-
const
|
|
54
|
-
const
|
|
55
|
-
const
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
114
|
+
const needsContentScan = validateH1 || validateUniqueMetadata;
|
|
115
|
+
const htmlFiles = needsContentScan || indexNow ? await collectHtmlFiles(buildDir) : [];
|
|
116
|
+
const h1Missing = [];
|
|
117
|
+
const h1Multiple = [];
|
|
118
|
+
const titlesByValue = new Map();
|
|
119
|
+
const descriptionsByValue = new Map();
|
|
120
|
+
if (needsContentScan) {
|
|
121
|
+
for (const file of htmlFiles) {
|
|
122
|
+
const content = await readFile(join(buildDir, file), 'utf8');
|
|
123
|
+
if (validateH1) {
|
|
124
|
+
const count = countH1s(content);
|
|
125
|
+
if (count === 0)
|
|
126
|
+
h1Missing.push(file);
|
|
127
|
+
else if (count > 1)
|
|
128
|
+
h1Multiple.push({ file, count });
|
|
129
|
+
}
|
|
130
|
+
if (validateUniqueMetadata) {
|
|
131
|
+
const title = extractTitle(content);
|
|
132
|
+
if (title) {
|
|
133
|
+
const list = titlesByValue.get(title) ?? [];
|
|
134
|
+
list.push(file);
|
|
135
|
+
titlesByValue.set(title, list);
|
|
136
|
+
}
|
|
137
|
+
const description = extractMetaDescription(content);
|
|
138
|
+
if (description) {
|
|
139
|
+
const list = descriptionsByValue.get(description) ?? [];
|
|
140
|
+
list.push(file);
|
|
141
|
+
descriptionsByValue.set(description, list);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
63
145
|
}
|
|
64
|
-
if (
|
|
65
|
-
|
|
66
|
-
|
|
146
|
+
if (validateH1) {
|
|
147
|
+
if (h1Missing.length === 0 && h1Multiple.length === 0) {
|
|
148
|
+
logger.info(`H1 validation: ${htmlFiles.length} pages checked, all good.`);
|
|
149
|
+
}
|
|
150
|
+
else {
|
|
151
|
+
for (const file of h1Missing) {
|
|
152
|
+
logger.warn(`H1 validation: ${file} has no <h1>.`);
|
|
153
|
+
}
|
|
154
|
+
for (const { file, count } of h1Multiple) {
|
|
155
|
+
logger.warn(`H1 validation: ${file} has ${count} <h1> elements (expected 1).`);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
67
158
|
}
|
|
68
|
-
|
|
69
|
-
|
|
159
|
+
if (validateUniqueMetadata) {
|
|
160
|
+
const dupTitles = [...titlesByValue.entries()].filter(([, files]) => files.length > 1);
|
|
161
|
+
const dupDescriptions = [...descriptionsByValue.entries()].filter(([, files]) => files.length > 1);
|
|
162
|
+
if (dupTitles.length === 0 && dupDescriptions.length === 0) {
|
|
163
|
+
logger.info(`Metadata uniqueness: ${htmlFiles.length} pages checked, all good.`);
|
|
164
|
+
}
|
|
165
|
+
else {
|
|
166
|
+
for (const [title, files] of dupTitles) {
|
|
167
|
+
logger.warn(`Metadata uniqueness: title ${JSON.stringify(title)} appears on ${files.length} pages: ${files.join(', ')}`);
|
|
168
|
+
}
|
|
169
|
+
for (const [description, files] of dupDescriptions) {
|
|
170
|
+
const preview = description.length > 80
|
|
171
|
+
? description.slice(0, 77) + '…'
|
|
172
|
+
: description;
|
|
173
|
+
logger.warn(`Metadata uniqueness: description ${JSON.stringify(preview)} appears on ${files.length} pages: ${files.join(', ')}`);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
70
176
|
}
|
|
71
|
-
|
|
72
|
-
|
|
177
|
+
if (indexNow) {
|
|
178
|
+
const urls = htmlFiles
|
|
179
|
+
.map((f) => htmlFileToUrl(f, indexNow.siteUrl))
|
|
180
|
+
.filter((u) => (indexNow.filter ? indexNow.filter(u) : true));
|
|
181
|
+
if (urls.length === 0) {
|
|
182
|
+
logger.info('IndexNow: no URLs to submit.');
|
|
183
|
+
}
|
|
184
|
+
else {
|
|
185
|
+
const results = await submitToIndexNow({
|
|
186
|
+
host: indexNow.host,
|
|
187
|
+
key: indexNow.key,
|
|
188
|
+
keyLocation: indexNow.keyLocation,
|
|
189
|
+
endpoint: indexNow.endpoint,
|
|
190
|
+
urls,
|
|
191
|
+
});
|
|
192
|
+
for (const r of results) {
|
|
193
|
+
if (r.ok) {
|
|
194
|
+
logger.info(`IndexNow: submitted ${r.submitted} URLs (status ${r.status}).`);
|
|
195
|
+
}
|
|
196
|
+
else {
|
|
197
|
+
logger.warn(`IndexNow: submission failed (status ${r.status}): ${r.message}`);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
73
201
|
}
|
|
74
202
|
},
|
|
75
203
|
},
|
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.8.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.1"
|
|
56
56
|
},
|
|
57
57
|
"devDependencies": {
|
|
58
58
|
"@types/node": "^22.0.0",
|