@jdevalk/astro-seo-graph 0.5.0 → 0.5.2

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
@@ -5,8 +5,8 @@
5
5
 
6
6
  Astro integration for [`@jdevalk/seo-graph-core`](../seo-graph-core). Ships a
7
7
  `<Seo>` component, route factories for agent-ready schema endpoints, a
8
- content-collection aggregator, breadcrumb helpers, and Zod helpers for content
9
- schemas.
8
+ content-collection aggregator, breadcrumb helpers, a fuzzy 404 redirect
9
+ component, and Zod helpers for content schemas.
10
10
 
11
11
  For detailed usage — including all builder signatures, site-type recipes, and
12
12
  schema.org best practices — see [AGENTS.md](https://github.com/jdevalk/seo-graph/blob/main/AGENTS.md).
@@ -23,6 +23,7 @@ schema.org best practices — see [AGENTS.md](https://github.com/jdevalk/seo-gra
23
23
  | **`buildAstroSeoProps`** | Pure-TS logic that powers `<Seo>` — exported for users who want to feed a different head component. |
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
+ | **`<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. |
26
27
 
27
28
  ## Installation
28
29
 
@@ -110,6 +111,57 @@ Segments without a `names` entry are title-cased from their slug
110
111
  (e.g. `https://example.com/docs`) are supported — pass the base path as part
111
112
  of `siteUrl`.
112
113
 
114
+ ## Fuzzy 404 redirect
115
+
116
+ When a visitor hits a 404, `<FuzzyRedirect>` fetches your sitemap, compares
117
+ the mistyped URL against all known paths, and either suggests the closest
118
+ match or auto-redirects. Drop it into your `404.astro` page:
119
+
120
+ ```astro
121
+ ---
122
+ // src/pages/404.astro
123
+ import FuzzyRedirect from '@jdevalk/astro-seo-graph/FuzzyRedirect.astro';
124
+ ---
125
+
126
+ <html lang="en">
127
+ <head>
128
+ <meta charset="utf-8" />
129
+ <title>Page not found</title>
130
+ </head>
131
+ <body>
132
+ <h1>Page not found</h1>
133
+ <p>Sorry, the page you're looking for doesn't exist.</p>
134
+ <p style="font-size: 1.25em; font-weight: bold;">
135
+ <FuzzyRedirect />
136
+ </p>
137
+ <p><a href="/">Go to the homepage</a></p>
138
+ </body>
139
+ </html>
140
+ ```
141
+
142
+ When a close match is found, the component renders a message like
143
+ **Did you mean [/seo-graph/](/seo-graph/)?** inside the element where
144
+ you place it. Style the surrounding element to make it prominent.
145
+
146
+ ### How it works
147
+
148
+ 1. Fetches `/sitemap-index.xml` (follows sitemap index → child sitemaps)
149
+ 2. Extracts all paths and computes
150
+ [Levenshtein similarity](https://en.wikipedia.org/wiki/Levenshtein_distance)
151
+ against the current URL
152
+ 3. **0.6–0.85 similarity**: shows "Did you mean /correct-path/?"
153
+ 4. **Above 0.85**: auto-redirects with `window.location.replace`
154
+ 5. **Below 0.6 or exact match**: does nothing
155
+
156
+ ### Props
157
+
158
+ | Prop | Default | Description |
159
+ | ----------------------- | ---------------------- | -------------------------------------------------- |
160
+ | `threshold` | `0.6` | Minimum similarity for a suggestion to appear |
161
+ | `autoRedirectThreshold` | `0.85` | Similarity above which the user is auto-redirected |
162
+ | `sitemapUrl` | `'/sitemap-index.xml'` | URL of the sitemap index or sitemap file |
163
+ | `suggestionText` | `'Did you mean'` | Text shown before the suggested link |
164
+
113
165
  ## hreflang alternates
114
166
 
115
167
  For multilingual sites, pass an `alternates` prop with one entry per locale.
@@ -2,13 +2,13 @@
2
2
  interface Props {
3
3
  /**
4
4
  * Minimum similarity score (0–1) for a suggestion to appear.
5
- * Defaults to 0.7.
5
+ * Defaults to 0.6.
6
6
  */
7
7
  threshold?: number;
8
8
  /**
9
9
  * Similarity score (0–1) above which the user is automatically
10
10
  * redirected instead of shown a suggestion. Set to 1 to disable
11
- * auto-redirect. Defaults to 0.95.
11
+ * auto-redirect. Defaults to 0.85.
12
12
  */
13
13
  autoRedirectThreshold?: number;
14
14
  /**
@@ -23,8 +23,8 @@ interface Props {
23
23
  }
24
24
 
25
25
  const {
26
- threshold = 0.7,
27
- autoRedirectThreshold = 0.95,
26
+ threshold = 0.6,
27
+ autoRedirectThreshold = 0.85,
28
28
  sitemapUrl = '/sitemap-index.xml',
29
29
  suggestionText = 'Did you mean',
30
30
  } = Astro.props;
@@ -96,12 +96,24 @@ const {
96
96
  const container = document.getElementById('fuzzy-redirect');
97
97
  if (!container) return;
98
98
 
99
- const threshold = parseFloat(container.dataset.threshold ?? '0.7');
100
- const autoRedirectThreshold = parseFloat(container.dataset.autoRedirectThreshold ?? '0.95');
99
+ const threshold = parseFloat(container.dataset.threshold ?? '0.6');
100
+ const autoRedirectThreshold = parseFloat(container.dataset.autoRedirectThreshold ?? '0.85');
101
101
  const sitemapUrl = container.dataset.sitemapUrl ?? '/sitemap-index.xml';
102
102
  const suggestionText = container.dataset.suggestionText ?? 'Did you mean';
103
103
 
104
104
  const currentPath = window.location.pathname;
105
+
106
+ // Out-of-bounds pagination: /something/page/N/ → /something/
107
+ const paginationMatch = currentPath.match(/^(.+?)\/page\/\d+\/?$/);
108
+ if (paginationMatch) {
109
+ const basePath = paginationMatch[1]!.replace(/\/$/, '') + '/';
110
+ console.log(
111
+ `[FuzzyRedirect] Pagination redirect: "${currentPath}" → "${basePath}"`,
112
+ );
113
+ window.location.replace(basePath);
114
+ return;
115
+ }
116
+
105
117
  const urls = await fetchSitemapUrls(sitemapUrl);
106
118
 
107
119
  // Extract paths from full URLs.
@@ -124,6 +136,10 @@ const {
124
136
  }
125
137
  }
126
138
 
139
+ console.log(
140
+ `[FuzzyRedirect] Best match for "${currentPath}": "${bestPath}" (similarity: ${bestScore.toFixed(3)})`,
141
+ );
142
+
127
143
  // Exact match means the 404 is correct — the path exists in the
128
144
  // sitemap but returned a 404 (possibly a stale sitemap entry).
129
145
  if (bestScore >= 1 || bestScore < threshold) return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jdevalk/astro-seo-graph",
3
- "version": "0.5.0",
3
+ "version": "0.5.2",
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",