@saypsst/next 0.2.0 → 0.3.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
@@ -1,12 +1,12 @@
1
1
  # @saypsst/next
2
2
 
3
- Drop-in route modules that serve [psst](https://saypsst.com) pages on your own domain, inside your Next.js App Router app (Next 15 or 16, any host). psst publishes, updates and withdraws pages under a path prefix it owns (default `/resources`) without another deploy on your side.
3
+ Drop-in route modules that serve [PSST](https://saypsst.com) pages on your own domain, inside your Next.js App Router app (Next 15 or 16, any host). PSST publishes, updates and withdraws pages under a path prefix it owns (default `/resources`) without another deploy on your side.
4
4
 
5
5
  - Server components and route handlers only: no client component, no client JavaScript added.
6
6
  - Full text in the HTML for every visitor and crawler, with canonical, Open Graph, JSON-LD and a `psst-version` meta.
7
- - Time-based ISR (300 s). If psst is down, your site keeps serving the last good render.
7
+ - Time-based ISR (300 s). If PSST is down, your site keeps serving the last good render.
8
8
  - No secrets. The only value you configure is your public site id.
9
- - Runtime dependency: `zod` (pinned). `@psst/content`, the psst content contract, is bundled into `dist/`. No install scripts.
9
+ - Runtime dependency: `zod` (pinned). `@psst/content`, the PSST content contract, is bundled into `dist/`. No install scripts.
10
10
 
11
11
  `npx saypsst init` writes all of this for you. This README is the reference for what it writes.
12
12
 
@@ -22,7 +22,7 @@ Paths assume `app/` at the project root and the default prefix `/resources`. Wit
22
22
  import { definePsst } from "@saypsst/next";
23
23
 
24
24
  export default definePsst({
25
- site: "site_xxxxxxxxxxxxxxxx", // public site id from the psst dashboard
25
+ site: "site_xxxxxxxxxxxxxxxx", // public site id from the PSST dashboard
26
26
  domain: "example.com", // production host, no scheme
27
27
  siteName: "Example",
28
28
  prefix: "/resources",
@@ -47,7 +47,7 @@ export const generateMetadata = page.generateMetadata;
47
47
  export default page.default;
48
48
  ```
49
49
 
50
- To get your site's header and footer (needed when your root layout renders none), pass a `Shell`. It receives the psst `<main>` element as `children`; do not wrap it in another `<main>`:
50
+ To get your site's header and footer (needed when your root layout renders none), pass a `Shell`. It receives the PSST `<main>` element as `children`; do not wrap it in another `<main>`:
51
51
 
52
52
  ```tsx
53
53
  import type { PsstShellProps } from "@saypsst/next/page";
@@ -111,13 +111,13 @@ export const config = {
111
111
  };
112
112
  ```
113
113
 
114
- If you already have a proxy, wrap it and keep your own matcher (make sure it also matches the psst prefix):
114
+ If you already have a proxy, wrap it and keep your own matcher (make sure it also matches the PSST prefix):
115
115
 
116
116
  ```ts
117
117
  export const proxy = withPsst(psst, yourProxy);
118
118
  ```
119
119
 
120
- Then reference the psst sitemap from `robots.txt` (`Sitemap: https://example.com/resources/sitemap.xml`) or from `app/robots.ts`.
120
+ Then reference the PSST sitemap from `robots.txt` (`Sitemap: https://example.com/resources/sitemap.xml`) or from `app/robots.ts`.
121
121
 
122
122
  ### Only when the dashboard claimed a root path
123
123
 
@@ -144,7 +144,7 @@ export const revalidate = 300;
144
144
  export const GET = psstLlms(psst);
145
145
  ```
146
146
 
147
- Do not edit these files by hand beyond the `Shell`; psst content is never added locally.
147
+ Do not edit these files by hand beyond the `Shell`; PSST content is never added locally.
148
148
 
149
149
  ## Exports
150
150
 
@@ -154,17 +154,17 @@ Do not edit these files by hand beyond the `Shell`; psst content is never added
154
154
  | | `PsstConfig`, `PsstConfigInput`, `PsstConfigError`, `PSST_REVALIDATE` (300), `DEFAULT_EDGE`, `PSST_NEXT_VERSION` | Types and constants. |
155
155
  | `@saypsst/next/page` | `psstPage(config, { Shell?, path? })` | Returns `{ default, generateMetadata, generateStaticParams }` for the page file. |
156
156
  | | `PsstShellProps`, `PsstPageOptions`, `PsstPageProps`, `PsstPageModule`, `PsstIntegrityError`, `PSST_REVALIDATE` | |
157
- | `@saypsst/next/sitemap` | `psstSitemap(config)` | `GET` handler: XML sitemap of the published psst pages. |
158
- | `@saypsst/next/style` | `psstStyle(config)` | `GET` handler: the psst stylesheet, same-origin, `text/css`. |
159
- | `@saypsst/next/verify` | `psstVerify(config)` | `GET` handler: the domain-proof text psst checks. |
157
+ | `@saypsst/next/sitemap` | `psstSitemap(config)` | `GET` handler: XML sitemap of the published PSST pages. |
158
+ | `@saypsst/next/style` | `psstStyle(config)` | `GET` handler: the PSST stylesheet, same-origin, `text/css`. |
159
+ | `@saypsst/next/verify` | `psstVerify(config)` | `GET` handler: the domain-proof text PSST checks. |
160
160
  | `@saypsst/next/llms` | `psstLlms(config)` | `GET` handler: `/llms.txt`. Throws unless `/llms.txt` is in `rootPaths`. |
161
161
  | `@saypsst/next/proxy` | `withPsst(config, proxy?)`, `PsstProxy` | Wraps your proxy: AI-crawler / AI-referral hits, 410 for withdrawn paths. |
162
162
 
163
163
  ## What each module does
164
164
 
165
- **Page.** `generateMetadata` and the page fetch `GET {edge}/v1/sites/{site}/pages?path=…` (one request per render; 8 s timeout), validate the response and the document against the content contract (images only from your domain or `edge.saypsst.com`), and check the document's sha256 against the version hash psst approved. The body has the same structure as psst's static renderer: header, `main > article.psst-article` (h1, lede, updated date, blocks, sources), footer, plus one JSON-LD script (escaped; the only script). Metadata replaces your layout's defaults: absolute title, description, canonical `https://{domain}{path}`, Open Graph, Twitter, `robots: index, follow`, and `<meta name="psst-version" content="{12-char hash}">`. `generateStaticParams` prerenders the published pages from the manifest at build, and returns `[]` if psst is unreachable.
165
+ **Page.** `generateMetadata` and the page fetch `GET {edge}/v1/sites/{site}/pages?path=…` (one request per render; 8 s timeout), validate the response and the document against the content contract (images only from your domain or `edge.saypsst.com`), and check the document's sha256 against the version hash PSST approved. The body has the same structure as PSST's static renderer: header, `main > article.psst-article` (h1, lede, updated date, blocks, sources), footer, plus one JSON-LD script (escaped; the only script). Metadata replaces your layout's defaults: absolute title, description, canonical `https://{domain}{path}`, Open Graph, Twitter, `robots: index, follow`, and `<meta name="psst-version" content="{12-char hash}">`. `generateStaticParams` prerenders the published pages from the manifest at build, and returns `[]` if PSST is unreachable.
166
166
 
167
- **Sitemap** renders the manifest's published HTML pages (withdrawn paths are already out). **Style, verify, llms** copy the files psst's edge renders (`GET {edge}/s/{site}{path}`), check the media type and size, and serve them same-origin with `nosniff`.
167
+ **Sitemap** renders the manifest's published HTML pages (withdrawn paths are already out). **Style, verify, llms** copy the files PSST's edge renders (`GET {edge}/s/{site}{path}`), check the media type and size, and serve them same-origin with `nosniff`.
168
168
 
169
169
  **Proxy.** On every matched request it classifies the `User-Agent` (a claim, not proof) and the `Referer` host. Known AI crawlers and visitors arriving from ChatGPT, Perplexity, Gemini, Copilot or Claude produce one `POST {edge}/v1/hits` with the path and the classes only: no IP, no cookie, no query string, no UA string. The post is not awaited (`event.waitUntil`, 300 ms abort) and errors are swallowed. Requests with `X-Psst-Doctor: 1`, prefetches, RSC navigations, `/_next/*` and static assets are skipped. Then your proxy runs unchanged and its result is returned as is.
170
170
 
@@ -176,13 +176,13 @@ Verified against the Next 16.4 docs (`incremental-static-regeneration`, `caching
176
176
  | --- | --- |
177
177
  | Time-based ISR, 300 s | Route segment config `export const revalidate = 300` written as a literal in every route file (page and the four route handlers), plus `dynamicParams = true` on the catch-all. Edge fetches set no `cache` option, so they never enter the Data Cache (only status-200 responses would) and the ISR entry is the one cache layer; using no Request-time API keeps the routes static. Pages missing from `generateStaticParams` render on first request and are then cached the same way. |
178
178
  | Keep the last good render on fetch failure | The page and handlers **throw** (`PsstUnavailableError`, `PsstIntegrityError`) on a network error, timeout, non-200/404/410 answer, invalid JSON, contract failure or hash mismatch. Next's response cache keeps serving the previous entry when a background regeneration throws, and retries on the next request. Nothing is caught that Next needs: `notFound()` is called outside any `try`, so `unstable_rethrow` is not needed. |
179
- | Never-rendered page while psst is down | **A page cannot set a 503 in the App Router** (only `notFound()` 404, redirects, and the experimental 401/403). The closest correct mechanism: the thrown error makes Next answer **500 with `Cache-Control: private, no-cache, no-store`** and a `noindex` robots meta, and the failure is never persisted as ISR output, so no cached 404 and no cached error; the next request retries. There is no `Retry-After` on this response. |
180
- | psst down during `next build` | Your build never fails because of psst. `generateStaticParams` returns `[]` (the pages render on first request). A fixed-path page (`/about`) that cannot fetch at build calls `connection()` (`next/server`), so Next renders it at request time for that deployment. Route handlers (`process.env.NEXT_PHASE === "phase-production-build"`) return **503, `Retry-After: 300`, `Cache-Control: no-store`**; Next never stores a build-time route response with status ≥ 400 other than 404 (it sets `revalidate: 0`), so those routes are also rendered at request time for that deployment. In that degraded mode every request asks the edge: it answers normally once psst is back (successful handler responses carry `Cache-Control: public, max-age=0, s-maxage=300, stale-while-revalidate=86400`, so a CDN still caches them), and answers 500 uncached while psst is down. The next deploy restores full ISR. At runtime the handlers never return an error response: they throw, as above, so a good response is never replaced. |
181
- | Not found / withdrawn | Edge 404 or 410 → `notFound()` (404, cached for one ISR period). A page cannot answer 410, so `withPsst()` answers **410** for paths in the manifest's `withdrawn` list. It keeps that list in memory per instance and refreshes it in the background every 300 s, so no request waits on psst; until the first refresh completes, a withdrawn path answers 404. The sitemap drops withdrawn paths. |
179
+ | Never-rendered page while PSST is down | **A page cannot set a 503 in the App Router** (only `notFound()` 404, redirects, and the experimental 401/403). The closest correct mechanism: the thrown error makes Next answer **500 with `Cache-Control: private, no-cache, no-store`** and a `noindex` robots meta, and the failure is never persisted as ISR output, so no cached 404 and no cached error; the next request retries. There is no `Retry-After` on this response. |
180
+ | PSST down during `next build` | Your build never fails because of PSST. `generateStaticParams` returns `[]` (the pages render on first request). A fixed-path page (`/about`) that cannot fetch at build calls `connection()` (`next/server`), so Next renders it at request time for that deployment. Route handlers (`process.env.NEXT_PHASE === "phase-production-build"`) return **503, `Retry-After: 300`, `Cache-Control: no-store`**; Next never stores a build-time route response with status ≥ 400 other than 404 (it sets `revalidate: 0`), so those routes are also rendered at request time for that deployment. In that degraded mode every request asks the edge: it answers normally once PSST is back (successful handler responses carry `Cache-Control: public, max-age=0, s-maxage=300, stale-while-revalidate=86400`, so a CDN still caches them), and answers 500 uncached while PSST is down. The next deploy restores full ISR. At runtime the handlers never return an error response: they throw, as above, so a good response is never replaced. |
181
+ | Not found / withdrawn | Edge 404 or 410 → `notFound()` (404, cached for one ISR period). A page cannot answer 410, so `withPsst()` answers **410** for paths in the manifest's `withdrawn` list. It keeps that list in memory per instance and refreshes it in the background every 300 s, so no request waits on PSST; until the first refresh completes, a withdrawn path answers 404. The sitemap drops withdrawn paths. |
182
182
 
183
- Cache Components (`cacheComponents: true`) removes the `revalidate` and `dynamicParams` segment configs, so these route files do not build there; v1 supports apps without Cache Components.
183
+ Cache Components (`cacheComponents: true`) removes the `revalidate` and `dynamicParams` segment configs, so these route files do not build there. Such an app sets `serveFromEdge: true` in `psst.config.ts` and adds only `proxy.ts`: `withPsst()` rewrites PSST paths to the edge, which renders them with its own header and footer (not your layout), answers 410 for withdrawn paths and counts crawler hits itself. `npx saypsst init` does this when it detects the flag.
184
184
 
185
- Self-hosted with several instances (for example behind CloudFront): the default ISR cache is per instance. psst content then refreshes per instance within 300 s, which is correct but uneven; a shared cache handler fixes that.
185
+ Self-hosted with several instances (for example behind CloudFront): the default ISR cache is per instance. PSST content then refreshes per instance within 300 s, which is correct but uneven; a shared cache handler fixes that.
186
186
 
187
187
  ## Security notes
188
188
 
@@ -1,4 +1,4 @@
1
- import { domainSchema, publicIdSchema, isSafeHref, prefixSchema } from './chunk-WHB66OTA.js';
1
+ import { domainSchema, publicIdSchema, isSafeHref, prefixSchema } from './chunk-X3GT7AOS.js';
2
2
  import { z } from 'zod';
3
3
 
4
4
  var PSST_REVALIDATE = 300;
@@ -29,6 +29,11 @@ var configSchema = z.strictObject({
29
29
  rootPaths: z.array(z.enum(["/about", "/llms.txt"])).max(2).default([]),
30
30
  /** psst edge origin. Override only for local development. */
31
31
  edge: z.string().refine(isEdgeOrigin, "edge must be an https origin (http only on localhost)").default(DEFAULT_EDGE),
32
+ /**
33
+ * Serve psst paths from the edge through `withPsst()` instead of route files, for apps with Cache Components, where
34
+ * the route files do not build. Pages use the edge's own header and footer, not the app's layout.
35
+ */
36
+ serveFromEdge: z.boolean().default(false),
32
37
  /** Header/footer used when the page module gets no `Shell` component. Same shape as the static renderer's shell. */
33
38
  shell: z.strictObject({
34
39
  nav: z.array(
@@ -50,7 +55,7 @@ function definePsst(input) {
50
55
  const parsed = configSchema.safeParse(input);
51
56
  if (!parsed.success) {
52
57
  throw new PsstConfigError(
53
- `invalid psst config: ${parsed.error.issues.map((i) => `${i.path.join(".") || "(root)"}: ${i.message}`).join("; ")}`
58
+ `invalid PSST config: ${parsed.error.issues.map((i) => `${i.path.join(".") || "(root)"}: ${i.message}`).join("; ")}`
54
59
  );
55
60
  }
56
61
  const c = parsed.data;
@@ -1,4 +1,4 @@
1
- import { PsstUnavailableError } from './chunk-5X6OYLUN.js';
1
+ import { PsstUnavailableError } from './chunk-ZELUPPQB.js';
2
2
 
3
3
  // src/route.ts
4
4
  var RETRY_AFTER_SECONDS = 300;
@@ -19,7 +19,7 @@ function psstRoute(handler) {
19
19
  };
20
20
  }
21
21
  function unavailable() {
22
- return new Response("psst is temporarily unavailable\n", {
22
+ return new Response("PSST is temporarily unavailable\n", {
23
23
  status: 503,
24
24
  headers: {
25
25
  "content-type": "text/plain; charset=utf-8",
@@ -1,5 +1,5 @@
1
- import { psstRoute, notFoundResponse, textResponse } from './chunk-CKSLG42W.js';
2
- import { fetchEdgeFile, PsstUnavailableError, readCapped } from './chunk-5X6OYLUN.js';
1
+ import { psstRoute, notFoundResponse, textResponse } from './chunk-ELYUDQHH.js';
2
+ import { fetchEdgeFile, PsstUnavailableError, readCapped } from './chunk-ZELUPPQB.js';
3
3
 
4
4
  // src/edge-file.ts
5
5
  function edgeFileRoute(config, spec) {
@@ -404,7 +404,20 @@ var publicIdSchema = z.string().regex(/^site_[a-z2-7]{16}$/);
404
404
  var sitePathSchema = z.string().max(512).regex(SITE_PATH).refine((p) => !p.startsWith("//") && !p.includes(".."), "invalid path");
405
405
  var siteModeSchema = z.enum(["next", "vercel_static", "proxy"]);
406
406
  var agentClassSchema = z.enum(["training", "search", "user_fetch", "other"]);
407
- var referrerClassSchema = z.enum(["chatgpt", "perplexity", "gemini", "copilot", "claude", "other", "none"]);
407
+ var referrerClassSchema = z.enum([
408
+ "chatgpt",
409
+ "perplexity",
410
+ "gemini",
411
+ "copilot",
412
+ "claude",
413
+ "grok",
414
+ "mistral",
415
+ "deepseek",
416
+ "meta",
417
+ "you",
418
+ "other",
419
+ "none"
420
+ ]);
408
421
  var hitSourceSchema = z.enum(["edge", "proxy"]);
409
422
  var publishStateSchema = z.enum(["published", "verified"]);
410
423
  var hashSchema = z.string().regex(/^[0-9a-f]{64}$/);
@@ -475,7 +488,17 @@ var AI_AGENT_RULES = [
475
488
  { agent: "Meta-ExternalAgent", agentClass: "training", pattern: /meta-externalagent/i },
476
489
  { agent: "Bytespider", agentClass: "training", pattern: /bytespider/i },
477
490
  { agent: "CCBot", agentClass: "training", pattern: /ccbot/i },
478
- { agent: "Amazonbot", agentClass: "training", pattern: /amazonbot/i }
491
+ { agent: "Amazonbot", agentClass: "training", pattern: /amazonbot/i },
492
+ { agent: "MistralAI-User", agentClass: "user_fetch", pattern: /mistralai-user/i },
493
+ { agent: "MistralAI-Index", agentClass: "search", pattern: /mistralai-index/i },
494
+ { agent: "DuckAssistBot", agentClass: "search", pattern: /duckassistbot/i },
495
+ { agent: "Google-Agent", agentClass: "user_fetch", pattern: /google-agent/i },
496
+ { agent: "Meta-ExternalFetcher", agentClass: "user_fetch", pattern: /meta-externalfetcher/i },
497
+ { agent: "meta-webindexer", agentClass: "search", pattern: /meta-webindexer/i },
498
+ { agent: "YouBot", agentClass: "search", pattern: /youbot/i },
499
+ { agent: "Amzn-SearchBot", agentClass: "search", pattern: /amzn-searchbot/i },
500
+ { agent: "Amzn-User", agentClass: "user_fetch", pattern: /amzn-user/i },
501
+ { agent: "Claude-Agent", agentClass: "user_fetch", pattern: /claude-agent/i }
479
502
  ];
480
503
  new Set(AI_AGENT_RULES.map((r) => r.agent));
481
504
  function classifyUserAgent(userAgent) {
@@ -494,22 +517,70 @@ var REFERRER_HOSTS = [
494
517
  { host: "bard.google.com", cls: "gemini" },
495
518
  { host: "copilot.microsoft.com", cls: "copilot" },
496
519
  { host: "copilot.cloud.microsoft", cls: "copilot" },
497
- { host: "claude.ai", cls: "claude" }
520
+ { host: "claude.ai", cls: "claude" },
521
+ { host: "grok.com", cls: "grok" },
522
+ { host: "chat.mistral.ai", cls: "mistral" },
523
+ { host: "chat.deepseek.com", cls: "deepseek" },
524
+ { host: "meta.ai", cls: "meta" },
525
+ { host: "you.com", cls: "you" }
526
+ ];
527
+ var REFERRER_PATHS = [
528
+ { host: "x.com", pathPrefix: "/i/grok", cls: "grok" },
529
+ { host: "twitter.com", pathPrefix: "/i/grok", cls: "grok" }
498
530
  ];
499
531
  function classifyReferrer(referrer) {
500
532
  if (typeof referrer !== "string" || referrer.length === 0 || referrer.length > 2048) return "none";
501
- let host;
533
+ let url;
502
534
  try {
503
- host = new URL(referrer).hostname.toLowerCase();
535
+ url = new URL(referrer);
504
536
  } catch {
505
537
  return "none";
506
538
  }
539
+ const host = url.hostname.toLowerCase();
507
540
  if (host === "") return "none";
508
541
  for (const { host: h, cls } of REFERRER_HOSTS) {
509
542
  if (host === h || host.endsWith(`.${h}`)) return cls;
510
543
  }
544
+ const bare = host.replace(/^www\./, "");
545
+ const path = url.pathname.toLowerCase();
546
+ for (const { host: h, pathPrefix, cls } of REFERRER_PATHS) {
547
+ if (bare === h && (path === pathPrefix || path.startsWith(`${pathPrefix}/`))) return cls;
548
+ }
511
549
  return "other";
512
550
  }
551
+ var AI_UTM_RULES = {
552
+ chatgpt: "chatgpt",
553
+ "chatgpt.com": "chatgpt",
554
+ openai: "chatgpt",
555
+ perplexity: "perplexity",
556
+ "perplexity.ai": "perplexity",
557
+ gemini: "gemini",
558
+ "gemini.google.com": "gemini",
559
+ copilot: "copilot",
560
+ "copilot.microsoft.com": "copilot",
561
+ claude: "claude",
562
+ "claude.ai": "claude",
563
+ grok: "grok",
564
+ mistral: "mistral",
565
+ lechat: "mistral",
566
+ deepseek: "deepseek",
567
+ metaai: "meta",
568
+ youcom: "you"
569
+ };
570
+ function classifyUtmSource(utmSource) {
571
+ if (typeof utmSource !== "string" || utmSource.length === 0 || utmSource.length > 200) return null;
572
+ const v = utmSource.trim().toLowerCase();
573
+ if (Object.hasOwn(AI_UTM_RULES, v)) return AI_UTM_RULES[v] ?? null;
574
+ for (const { host, cls } of REFERRER_HOSTS) {
575
+ if (v === host || v.endsWith(`.${host}`)) return cls;
576
+ }
577
+ return null;
578
+ }
579
+ function classifyArrival(referrer, utmSource) {
580
+ const fromReferrer = classifyReferrer(referrer);
581
+ if (fromReferrer !== "other" && fromReferrer !== "none") return fromReferrer;
582
+ return classifyUtmSource(utmSource) ?? fromReferrer;
583
+ }
513
584
 
514
585
  // src/content.ts
515
586
  var siteShape = manifestResponseSchema.shape.site.shape;
@@ -517,6 +588,6 @@ var domainSchema = siteShape.domain;
517
588
  var prefixSchema = siteShape.prefix;
518
589
 
519
590
  // src/version.ts
520
- var PSST_NEXT_VERSION = "0.2.0";
591
+ var PSST_NEXT_VERSION = "0.3.0";
521
592
 
522
- export { CONTENT_SCHEMA_VERSION, ContentError, PSST_NEXT_VERSION, buildJsonLd, classifyReferrer, classifyUserAgent, domainSchema, factValue, hashDocument, hitRequestSchema, isSafeHref, manifestResponseSchema, pageResponseSchema, parsePageDocument, prefixSchema, publicIdSchema, renderSitemap, serializeJsonForScript, shortHash, sitePathSchema };
593
+ export { CONTENT_SCHEMA_VERSION, ContentError, PSST_NEXT_VERSION, buildJsonLd, classifyArrival, classifyUserAgent, domainSchema, factValue, hashDocument, hitRequestSchema, isSafeHref, manifestResponseSchema, pageResponseSchema, parsePageDocument, prefixSchema, publicIdSchema, renderSitemap, serializeJsonForScript, shortHash, sitePathSchema };
@@ -1,9 +1,9 @@
1
- import { pageResponseSchema, PSST_NEXT_VERSION, manifestResponseSchema } from './chunk-WHB66OTA.js';
1
+ import { pageResponseSchema, PSST_NEXT_VERSION, manifestResponseSchema } from './chunk-X3GT7AOS.js';
2
2
  import { cache } from 'react';
3
3
 
4
4
  var PsstUnavailableError = class extends Error {
5
5
  constructor(message, options) {
6
- super(`psst: ${message}`, options);
6
+ super(`PSST: ${message}`, options);
7
7
  this.name = "PsstUnavailableError";
8
8
  }
9
9
  };
@@ -14,6 +14,7 @@ declare const configSchema: z.ZodObject<{
14
14
  "/llms.txt": "/llms.txt";
15
15
  }>>>;
16
16
  edge: z.ZodDefault<z.ZodString>;
17
+ serveFromEdge: z.ZodDefault<z.ZodBoolean>;
17
18
  shell: z.ZodOptional<z.ZodObject<{
18
19
  nav: z.ZodOptional<z.ZodArray<z.ZodObject<{
19
20
  label: z.ZodString;
@@ -32,6 +33,8 @@ interface PsstConfig {
32
33
  readonly rootPaths: readonly ("/about" | "/llms.txt")[];
33
34
  /** Edge origin without a trailing slash. */
34
35
  readonly edge: string;
36
+ /** Psst paths are rewritten to the edge by `withPsst()` instead of served by route files. */
37
+ readonly serveFromEdge: boolean;
35
38
  readonly shell?: {
36
39
  readonly nav?: readonly {
37
40
  readonly label: string;
package/dist/index.d.ts CHANGED
@@ -1,7 +1,7 @@
1
- export { D as DEFAULT_EDGE, P as PSST_REVALIDATE, a as PsstConfig, b as PsstConfigError, c as PsstConfigInput, d as definePsst } from './config-CoVgm6Ny.js';
1
+ export { D as DEFAULT_EDGE, P as PSST_REVALIDATE, a as PsstConfig, b as PsstConfigError, c as PsstConfigInput, d as definePsst } from './config-LILlPYgW.js';
2
2
  import 'zod';
3
3
 
4
4
  /** Kept equal to package.json `version` (checked by test/version.test.ts). */
5
- declare const PSST_NEXT_VERSION = "0.2.0";
5
+ declare const PSST_NEXT_VERSION = "0.3.0";
6
6
 
7
7
  export { PSST_NEXT_VERSION };
package/dist/index.js CHANGED
@@ -1,2 +1,2 @@
1
- export { DEFAULT_EDGE, PSST_REVALIDATE, PsstConfigError, definePsst } from './chunk-4FFLVIJG.js';
2
- export { PSST_NEXT_VERSION } from './chunk-WHB66OTA.js';
1
+ export { DEFAULT_EDGE, PSST_REVALIDATE, PsstConfigError, definePsst } from './chunk-2NMIB353.js';
2
+ export { PSST_NEXT_VERSION } from './chunk-X3GT7AOS.js';
package/dist/llms.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { a as PsstConfig } from './config-CoVgm6Ny.js';
2
- export { P as PSST_REVALIDATE } from './config-CoVgm6Ny.js';
1
+ import { a as PsstConfig } from './config-LILlPYgW.js';
2
+ export { P as PSST_REVALIDATE } from './config-LILlPYgW.js';
3
3
  import 'zod';
4
4
 
5
5
  /** `GET` for `app/llms.txt/route.ts`, only when the dashboard claimed `/llms.txt` (`rootPaths`). */
package/dist/llms.js CHANGED
@@ -1,14 +1,14 @@
1
- import { edgeFileRoute } from './chunk-XCY5RUQM.js';
2
- import './chunk-CKSLG42W.js';
3
- import { PsstConfigError } from './chunk-4FFLVIJG.js';
4
- export { PSST_REVALIDATE } from './chunk-4FFLVIJG.js';
5
- import './chunk-5X6OYLUN.js';
6
- import './chunk-WHB66OTA.js';
1
+ import { edgeFileRoute } from './chunk-QE3BBW25.js';
2
+ import './chunk-ELYUDQHH.js';
3
+ import { PsstConfigError } from './chunk-2NMIB353.js';
4
+ export { PSST_REVALIDATE } from './chunk-2NMIB353.js';
5
+ import './chunk-ZELUPPQB.js';
6
+ import './chunk-X3GT7AOS.js';
7
7
 
8
8
  // src/llms.ts
9
9
  function psstLlms(config) {
10
10
  if (!config.rootPaths.includes("/llms.txt")) {
11
- throw new PsstConfigError("/llms.txt is not in rootPaths; psst only serves root paths the dashboard claimed");
11
+ throw new PsstConfigError("/llms.txt is not in rootPaths; PSST only serves root paths the dashboard claimed");
12
12
  }
13
13
  return edgeFileRoute(config, { path: "/llms.txt", type: "text/plain", maxBytes: 512 * 1024 });
14
14
  }
package/dist/page.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { Metadata } from 'next';
2
2
  import { ReactNode, ComponentType } from 'react';
3
- import { a as PsstConfig } from './config-CoVgm6Ny.js';
4
- export { P as PSST_REVALIDATE } from './config-CoVgm6Ny.js';
3
+ import { a as PsstConfig } from './config-LILlPYgW.js';
4
+ export { P as PSST_REVALIDATE } from './config-LILlPYgW.js';
5
5
  import 'zod';
6
6
 
7
7
  /** Props your `Shell` receives: the psst `<main>` element. Render your header and footer around it. */
package/dist/page.js CHANGED
@@ -1,8 +1,8 @@
1
- import { isBuildPhase } from './chunk-CKSLG42W.js';
2
- import { PsstConfigError } from './chunk-4FFLVIJG.js';
3
- export { PSST_REVALIDATE } from './chunk-4FFLVIJG.js';
4
- import { fetchManifest, PsstUnavailableError, fetchPage } from './chunk-5X6OYLUN.js';
5
- import { ContentError, parsePageDocument, hashDocument, shortHash, serializeJsonForScript, buildJsonLd, sitePathSchema, factValue } from './chunk-WHB66OTA.js';
1
+ import { isBuildPhase } from './chunk-ELYUDQHH.js';
2
+ import { PsstConfigError } from './chunk-2NMIB353.js';
3
+ export { PSST_REVALIDATE } from './chunk-2NMIB353.js';
4
+ import { fetchManifest, PsstUnavailableError, fetchPage } from './chunk-ZELUPPQB.js';
5
+ import { ContentError, parsePageDocument, hashDocument, shortHash, serializeJsonForScript, buildJsonLd, sitePathSchema, factValue } from './chunk-X3GT7AOS.js';
6
6
  import { notFound } from 'next/navigation';
7
7
  import { connection } from 'next/server';
8
8
  import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
@@ -104,7 +104,7 @@ function DefaultShell({ siteName, nav = [], footer, children }) {
104
104
  }
105
105
  var PsstIntegrityError = class extends Error {
106
106
  constructor(message) {
107
- super(`psst: ${message}`);
107
+ super(`PSST: ${message}`);
108
108
  this.name = "PsstIntegrityError";
109
109
  }
110
110
  };
@@ -140,7 +140,7 @@ async function load(config, fixed, props) {
140
140
  function psstPage(config, options = {}) {
141
141
  const fixed = options.path;
142
142
  if (fixed && !config.rootPaths.includes(fixed)) {
143
- throw new PsstConfigError(`${fixed} is not in rootPaths; psst only serves root paths the dashboard claimed`);
143
+ throw new PsstConfigError(`${fixed} is not in rootPaths; PSST only serves root paths the dashboard claimed`);
144
144
  }
145
145
  const Shell = options.Shell;
146
146
  async function Page(props) {
@@ -178,7 +178,7 @@ function psstPage(config, options = {}) {
178
178
  return manifest.pages.filter((p) => p.kind !== "llms_txt" && p.path.startsWith(base) && p.path.length > base.length).map((p) => ({ slug: p.path.slice(base.length).split("/") }));
179
179
  } catch (err) {
180
180
  if (err instanceof PsstUnavailableError || err instanceof ContentError) {
181
- console.warn(`${err.message}; prerendering no psst pages, they render on first request`);
181
+ console.warn(`${err.message}; prerendering no PSST pages, they render on first request`);
182
182
  return [];
183
183
  }
184
184
  throw err;
package/dist/proxy.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { NextRequest, NextFetchEvent } from 'next/server';
2
- import { a as PsstConfig } from './config-CoVgm6Ny.js';
2
+ import { a as PsstConfig } from './config-LILlPYgW.js';
3
3
  import 'zod';
4
4
 
5
5
  type ProxyResult = Response | null | undefined | void;
@@ -9,6 +9,9 @@ type PsstProxy = (request: NextRequest, event: NextFetchEvent) => ProxyResult |
9
9
  * Wrap the site's proxy. On every request: classify AI crawlers (by User-Agent, a claim not a proof) and AI
10
10
  * referrers, and post a hit without awaiting it (`event.waitUntil`); no IP, cookie, query or UA string is sent.
11
11
  * Withdrawn psst paths answer 410. Everything else goes to `proxy` unchanged, and its result is returned as is.
12
+ *
13
+ * With `serveFromEdge`, psst paths are rewritten to the edge before anything else, with an allowlist of request headers:
14
+ * the edge answers them (410 included) and counts its own hits, so no hit is posted for them here.
12
15
  */
13
16
  declare function withPsst(config: PsstConfig, proxy?: PsstProxy): PsstProxy;
14
17
 
package/dist/proxy.js CHANGED
@@ -1,8 +1,8 @@
1
- import { USER_AGENT, fetchManifest } from './chunk-5X6OYLUN.js';
2
- import { sitePathSchema, classifyUserAgent, classifyReferrer, hitRequestSchema, CONTENT_SCHEMA_VERSION } from './chunk-WHB66OTA.js';
1
+ import { USER_AGENT, fetchManifest } from './chunk-ZELUPPQB.js';
2
+ import { sitePathSchema, classifyUserAgent, classifyArrival, hitRequestSchema, CONTENT_SCHEMA_VERSION } from './chunk-X3GT7AOS.js';
3
+ import { NextResponse } from 'next/server';
3
4
 
4
5
  // src/hits.ts
5
- var AI_REFERRERS = /* @__PURE__ */ new Set(["chatgpt", "perplexity", "gemini", "copilot", "claude"]);
6
6
  var ASSET = /\.(?:js|mjs|css|map|png|jpe?g|gif|webp|avif|svg|ico|woff2?|ttf|otf|mp4|webm)$/i;
7
7
  function hitFor(config, request) {
8
8
  if (request.method !== "GET" && request.method !== "HEAD") return null;
@@ -15,8 +15,8 @@ function hitFor(config, request) {
15
15
  if (path.startsWith("/_next/") || ASSET.test(path)) return null;
16
16
  if (!sitePathSchema.safeParse(path).success) return null;
17
17
  const bot = classifyUserAgent(h.get("user-agent"));
18
- const referrerClass = classifyReferrer(h.get("referer"));
19
- if (bot.agent === null && !AI_REFERRERS.has(referrerClass)) return null;
18
+ const referrerClass = classifyArrival(h.get("referer"), request.nextUrl.searchParams.get("utm_source"));
19
+ if (bot.agent === null && (referrerClass === "other" || referrerClass === "none")) return null;
20
20
  const hit = hitRequestSchema.safeParse({
21
21
  v: CONTENT_SCHEMA_VERSION,
22
22
  publicId: config.site,
@@ -71,12 +71,35 @@ function gone() {
71
71
  headers: { "content-type": "text/plain; charset=utf-8", "cache-control": "public, max-age=0, s-maxage=300" }
72
72
  });
73
73
  }
74
+ var EDGE_REQUEST_HEADERS = ["user-agent", "accept", "x-psst-doctor", "rsc", "next-router-prefetch", "purpose", "sec-purpose"];
75
+ function refererOrigin(value) {
76
+ if (value === null) return null;
77
+ try {
78
+ return new URL(value).origin;
79
+ } catch {
80
+ return null;
81
+ }
82
+ }
83
+ function rewriteToEdge(config, request, path) {
84
+ const headers = new Headers();
85
+ for (const name of EDGE_REQUEST_HEADERS) {
86
+ const value = request.headers.get(name);
87
+ if (value !== null) headers.set(name, value);
88
+ }
89
+ const referer = refererOrigin(request.headers.get("referer"));
90
+ if (referer !== null) headers.set("referer", referer);
91
+ return NextResponse.rewrite(new URL(`${config.edge}/s/${config.site}${path}`), { request: { headers } });
92
+ }
74
93
  function withPsst(config, proxy) {
75
94
  const withdrawn = withdrawnTracker(config);
76
95
  return (request, event) => {
96
+ const path = request.nextUrl.pathname;
97
+ if (config.serveFromEdge) {
98
+ const edgePath = path.replace(/\/+$/, "");
99
+ if (isPsstPath(config, edgePath)) return rewriteToEdge(config, request, edgePath);
100
+ }
77
101
  const hit = hitFor(config, request);
78
102
  if (hit) event.waitUntil(postHit(config, hit));
79
- const path = request.nextUrl.pathname;
80
103
  if (isPsstPath(config, path)) {
81
104
  const refresh = withdrawn.refreshIfStale(Date.now());
82
105
  if (refresh) event.waitUntil(refresh);
package/dist/sitemap.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { a as PsstConfig } from './config-CoVgm6Ny.js';
2
- export { P as PSST_REVALIDATE } from './config-CoVgm6Ny.js';
1
+ import { a as PsstConfig } from './config-LILlPYgW.js';
2
+ export { P as PSST_REVALIDATE } from './config-LILlPYgW.js';
3
3
  import 'zod';
4
4
 
5
5
  /**
package/dist/sitemap.js CHANGED
@@ -1,7 +1,7 @@
1
- import { psstRoute, textResponse } from './chunk-CKSLG42W.js';
2
- export { PSST_REVALIDATE } from './chunk-4FFLVIJG.js';
3
- import { fetchManifest, PsstUnavailableError } from './chunk-5X6OYLUN.js';
4
- import { renderSitemap } from './chunk-WHB66OTA.js';
1
+ import { psstRoute, textResponse } from './chunk-ELYUDQHH.js';
2
+ export { PSST_REVALIDATE } from './chunk-2NMIB353.js';
3
+ import { fetchManifest, PsstUnavailableError } from './chunk-ZELUPPQB.js';
4
+ import { renderSitemap } from './chunk-X3GT7AOS.js';
5
5
 
6
6
  // src/sitemap.ts
7
7
  function psstSitemap(config) {
package/dist/style.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { a as PsstConfig } from './config-CoVgm6Ny.js';
2
- export { P as PSST_REVALIDATE } from './config-CoVgm6Ny.js';
1
+ import { a as PsstConfig } from './config-LILlPYgW.js';
2
+ export { P as PSST_REVALIDATE } from './config-LILlPYgW.js';
3
3
  import 'zod';
4
4
 
5
5
  /** `GET` for `app/(psst){prefix}/_psst/style.css/route.ts`: the psst stylesheet, served same-origin. */
package/dist/style.js CHANGED
@@ -1,8 +1,8 @@
1
- import { edgeFileRoute } from './chunk-XCY5RUQM.js';
2
- import './chunk-CKSLG42W.js';
3
- export { PSST_REVALIDATE } from './chunk-4FFLVIJG.js';
4
- import './chunk-5X6OYLUN.js';
5
- import './chunk-WHB66OTA.js';
1
+ import { edgeFileRoute } from './chunk-QE3BBW25.js';
2
+ import './chunk-ELYUDQHH.js';
3
+ export { PSST_REVALIDATE } from './chunk-2NMIB353.js';
4
+ import './chunk-ZELUPPQB.js';
5
+ import './chunk-X3GT7AOS.js';
6
6
 
7
7
  // src/style.ts
8
8
  function psstStyle(config) {
package/dist/verify.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { a as PsstConfig } from './config-CoVgm6Ny.js';
2
- export { P as PSST_REVALIDATE } from './config-CoVgm6Ny.js';
1
+ import { a as PsstConfig } from './config-LILlPYgW.js';
2
+ export { P as PSST_REVALIDATE } from './config-LILlPYgW.js';
3
3
  import 'zod';
4
4
 
5
5
  /**
package/dist/verify.js CHANGED
@@ -1,8 +1,8 @@
1
- import { edgeFileRoute } from './chunk-XCY5RUQM.js';
2
- import './chunk-CKSLG42W.js';
3
- export { PSST_REVALIDATE } from './chunk-4FFLVIJG.js';
4
- import './chunk-5X6OYLUN.js';
5
- import './chunk-WHB66OTA.js';
1
+ import { edgeFileRoute } from './chunk-QE3BBW25.js';
2
+ import './chunk-ELYUDQHH.js';
3
+ export { PSST_REVALIDATE } from './chunk-2NMIB353.js';
4
+ import './chunk-ZELUPPQB.js';
5
+ import './chunk-X3GT7AOS.js';
6
6
 
7
7
  // src/verify.ts
8
8
  function psstVerify(config) {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@saypsst/next",
3
- "version": "0.2.0",
4
- "description": "Drop-in Next.js App Router route modules that serve psst pages on your own domain.",
3
+ "version": "0.3.0",
4
+ "description": "Drop-in Next.js App Router route modules that serve PSST pages on your own domain.",
5
5
  "license": "UNLICENSED",
6
6
  "type": "module",
7
7
  "sideEffects": false,