@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 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. Currently:
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: [seoGraph()],
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 | Default | Description |
333
- | ------------ | ------- | ------------------------------------------- |
334
- | `validateH1` | `true` | Warn about pages without exactly one `<h1>` |
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
- Only static pages are checked (SSR pages aren't on disk at build time).
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
@@ -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"}
@@ -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"}
@@ -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":"AAOA,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;;;;OAIG;IACH,UAAU,CAAC,EAAE,OAAO,CAAC;CACxB;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,CAoC/F"}
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"}
@@ -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
+ '&amp;': '&',
40
+ '&lt;': '<',
41
+ '&gt;': '>',
42
+ '&quot;': '"',
43
+ '&apos;': "'",
44
+ '&#39;': "'",
45
+ '&nbsp;': ' ',
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 htmlFiles = await collectHtmlFiles(buildDir);
54
- const missing = [];
55
- const multiple = [];
56
- for (const file of htmlFiles) {
57
- const content = await readFile(join(buildDir, file), 'utf8');
58
- const count = countH1s(content);
59
- if (count === 0)
60
- missing.push(file);
61
- else if (count > 1)
62
- multiple.push({ file, count });
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 (missing.length === 0 && multiple.length === 0) {
65
- logger.info(`H1 validation: ${htmlFiles.length} pages checked, all good.`);
66
- return;
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
- for (const file of missing) {
69
- logger.warn(`H1 validation: ${file} has no <h1>.`);
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
- for (const { file, count } of multiple) {
72
- logger.warn(`H1 validation: ${file} has ${count} <h1> elements (expected 1).`);
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
  },
@@ -1 +1 @@
1
- {"version":3,"file":"integration.js","sourceRoot":"","sources":["../src/integration.ts"],"names":[],"mappings":"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"}
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.6.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.5.2"
55
+ "@jdevalk/seo-graph-core": "0.6.1"
56
56
  },
57
57
  "devDependencies": {
58
58
  "@types/node": "^22.0.0",