searvia-next 0.1.0 → 0.2.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
@@ -29,7 +29,7 @@ SEARVIA_WEBHOOK_URL=https://www.example.com/api/searvia # optional, instant ref
29
29
  export { middleware } from 'searvia-next/middleware';
30
30
  ```
31
31
 
32
- Already have a middleware? Wrap your response: `return withSearviaPath(request, response)`.
32
+ Already have a middleware? Wrap your response: `return withSearviaPath(request, response)`, and to apply approved redirects first add `const redirect = await searviaRedirect(request); if (redirect) return redirect;`.
33
33
 
34
34
  **2. Root layout** (`app/layout.tsx`):
35
35
 
package/dist/feed.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- export declare const VERSION = "0.1.0";
1
+ export declare const VERSION = "0.2.0";
2
2
  export type Fix = {
3
3
  t: string;
4
4
  v: Record<string, any>;
package/dist/feed.js CHANGED
@@ -1,5 +1,5 @@
1
1
  // Searvia fix feed: signed (HMAC-SHA256) list of approved fixes keyed by path. Works in the Node.js and Edge runtimes.
2
- export const VERSION = '0.1.0';
2
+ export const VERSION = '0.2.0';
3
3
  export function config() {
4
4
  const [channel, secret] = (process.env.SEARVIA_SITE_KEY ?? '').split('.', 2);
5
5
  if (!channel || !secret || !/^[a-z0-9]{24}$/.test(channel)) {
@@ -1,9 +1,14 @@
1
1
  import { NextResponse, type NextRequest } from 'next/server';
2
2
  /** Request header that carries the current address to Server Components (App Router layouts do not know the path). */
3
3
  export declare const PATH_HEADER = "x-searvia-path";
4
+ /**
5
+ * A redirect approved in Searvia (a broken or merged page) for this request, or null. Call it from your own middleware:
6
+ * `const redirect = await searviaRedirect(request); if (redirect) return redirect;`
7
+ */
8
+ export declare function searviaRedirect(request: NextRequest): Promise<NextResponse | null>;
4
9
  /**
5
10
  * Middleware that passes the path to `searviaMetadata()` and `<SearviaHead />`. Use it as your middleware, or call it from
6
11
  * your own: `return withSearviaPath(request, NextResponse.next(...))`.
7
12
  */
8
13
  export declare function withSearviaPath(request: NextRequest, response?: NextResponse): NextResponse;
9
- export declare function middleware(request: NextRequest): NextResponse;
14
+ export declare function middleware(request: NextRequest): Promise<NextResponse>;
@@ -1,6 +1,42 @@
1
1
  import { NextResponse } from 'next/server';
2
+ import { config, same, sign, VERSION } from './feed.js';
2
3
  /** Request header that carries the current address to Server Components (App Router layouts do not know the path). */
3
4
  export const PATH_HEADER = 'x-searvia-path';
5
+ const TTL = 300_000;
6
+ let cached = null;
7
+ /** The fix list for redirects, kept in memory for five minutes (middleware fetches are not cached by Next.js). */
8
+ async function feed() {
9
+ const settings = config();
10
+ if (!settings)
11
+ return null;
12
+ if (cached && Date.now() - cached.at < TTL)
13
+ return cached.feed;
14
+ try {
15
+ const response = await fetch(`${settings.base}/fixes/${settings.channel}/feed`, { headers: { 'User-Agent': `SearviaNext/${VERSION} (middleware)` } });
16
+ const body = response.ok ? await response.text() : '';
17
+ const valid = body !== '' && same(await sign(body, settings.secret), response.headers.get('x-searvia-signature') ?? '');
18
+ cached = { feed: valid ? JSON.parse(body) : (cached?.feed ?? null), at: Date.now() };
19
+ }
20
+ catch {
21
+ cached = { feed: cached?.feed ?? null, at: Date.now() };
22
+ }
23
+ return cached.feed;
24
+ }
25
+ /**
26
+ * A redirect approved in Searvia (a broken or merged page) for this request, or null. Call it from your own middleware:
27
+ * `const redirect = await searviaRedirect(request); if (redirect) return redirect;`
28
+ */
29
+ export async function searviaRedirect(request) {
30
+ if (request.method !== 'GET')
31
+ return null;
32
+ const { pathname, search } = request.nextUrl;
33
+ const list = (await feed())?.fixes;
34
+ const fix = (list?.[pathname + search] ?? list?.[pathname] ?? []).find((item) => item.t === 'redirect');
35
+ if (!fix || typeof fix.v.href !== 'string')
36
+ return null;
37
+ const target = new URL(fix.v.href, request.url);
38
+ return target.href === request.nextUrl.href ? null : NextResponse.redirect(target, [301, 302, 308].includes(fix.v.status) ? fix.v.status : 301);
39
+ }
4
40
  /**
5
41
  * Middleware that passes the path to `searviaMetadata()` and `<SearviaHead />`. Use it as your middleware, or call it from
6
42
  * your own: `return withSearviaPath(request, NextResponse.next(...))`.
@@ -12,6 +48,6 @@ export function withSearviaPath(request, response) {
12
48
  response?.headers.forEach((value, key) => next.headers.set(key, value));
13
49
  return next;
14
50
  }
15
- export function middleware(request) {
16
- return withSearviaPath(request);
51
+ export async function middleware(request) {
52
+ return (await searviaRedirect(request)) ?? withSearviaPath(request);
17
53
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "searvia-next",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Apply the SEO fixes you approve in Searvia (titles, descriptions, canonical, robots, hreflang, structured data, alt text, internal links) to a Next.js App Router site.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",