@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 +18 -18
- package/dist/{chunk-4FFLVIJG.js → chunk-2NMIB353.js} +7 -2
- package/dist/{chunk-CKSLG42W.js → chunk-ELYUDQHH.js} +2 -2
- package/dist/{chunk-XCY5RUQM.js → chunk-QE3BBW25.js} +2 -2
- package/dist/{chunk-WHB66OTA.js → chunk-X3GT7AOS.js} +78 -7
- package/dist/{chunk-5X6OYLUN.js → chunk-ZELUPPQB.js} +2 -2
- package/dist/{config-CoVgm6Ny.d.ts → config-LILlPYgW.d.ts} +3 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -2
- package/dist/llms.d.ts +2 -2
- package/dist/llms.js +7 -7
- package/dist/page.d.ts +2 -2
- package/dist/page.js +8 -8
- package/dist/proxy.d.ts +4 -1
- package/dist/proxy.js +29 -6
- package/dist/sitemap.d.ts +2 -2
- package/dist/sitemap.js +4 -4
- package/dist/style.d.ts +2 -2
- package/dist/style.js +5 -5
- package/dist/verify.d.ts +2 -2
- package/dist/verify.js +5 -5
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
# @saypsst/next
|
|
2
2
|
|
|
3
|
-
Drop-in route modules that serve [
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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`;
|
|
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
|
|
158
|
-
| `@saypsst/next/style` | `psstStyle(config)` | `GET` handler: the
|
|
159
|
-
| `@saypsst/next/verify` | `psstVerify(config)` | `GET` handler: the domain-proof text
|
|
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
|
|
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
|
|
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
|
|
180
|
-
|
|
|
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
|
|
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
|
|
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.
|
|
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-
|
|
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
|
|
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-
|
|
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("
|
|
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-
|
|
2
|
-
import { fetchEdgeFile, PsstUnavailableError, readCapped } from './chunk-
|
|
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([
|
|
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
|
|
533
|
+
let url;
|
|
502
534
|
try {
|
|
503
|
-
|
|
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.
|
|
591
|
+
var PSST_NEXT_VERSION = "0.3.0";
|
|
521
592
|
|
|
522
|
-
export { CONTENT_SCHEMA_VERSION, ContentError, PSST_NEXT_VERSION, buildJsonLd,
|
|
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-
|
|
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(`
|
|
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-
|
|
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.
|
|
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-
|
|
2
|
-
export { PSST_NEXT_VERSION } from './chunk-
|
|
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-
|
|
2
|
-
export { P as PSST_REVALIDATE } from './config-
|
|
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-
|
|
2
|
-
import './chunk-
|
|
3
|
-
import { PsstConfigError } from './chunk-
|
|
4
|
-
export { PSST_REVALIDATE } from './chunk-
|
|
5
|
-
import './chunk-
|
|
6
|
-
import './chunk-
|
|
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;
|
|
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-
|
|
4
|
-
export { P as PSST_REVALIDATE } from './config-
|
|
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-
|
|
2
|
-
import { PsstConfigError } from './chunk-
|
|
3
|
-
export { PSST_REVALIDATE } from './chunk-
|
|
4
|
-
import { fetchManifest, PsstUnavailableError, fetchPage } from './chunk-
|
|
5
|
-
import { ContentError, parsePageDocument, hashDocument, shortHash, serializeJsonForScript, buildJsonLd, sitePathSchema, factValue } from './chunk-
|
|
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(`
|
|
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;
|
|
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
|
|
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-
|
|
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-
|
|
2
|
-
import { sitePathSchema, classifyUserAgent,
|
|
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 =
|
|
19
|
-
if (bot.agent === 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
package/dist/sitemap.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { psstRoute, textResponse } from './chunk-
|
|
2
|
-
export { PSST_REVALIDATE } from './chunk-
|
|
3
|
-
import { fetchManifest, PsstUnavailableError } from './chunk-
|
|
4
|
-
import { renderSitemap } from './chunk-
|
|
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-
|
|
2
|
-
export { P as PSST_REVALIDATE } from './config-
|
|
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-
|
|
2
|
-
import './chunk-
|
|
3
|
-
export { PSST_REVALIDATE } from './chunk-
|
|
4
|
-
import './chunk-
|
|
5
|
-
import './chunk-
|
|
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
package/dist/verify.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { edgeFileRoute } from './chunk-
|
|
2
|
-
import './chunk-
|
|
3
|
-
export { PSST_REVALIDATE } from './chunk-
|
|
4
|
-
import './chunk-
|
|
5
|
-
import './chunk-
|
|
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.
|
|
4
|
-
"description": "Drop-in Next.js App Router route modules that serve
|
|
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,
|