@saypsst/next 0.0.0-stage → 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 +190 -2
- package/dist/chunk-2NMIB353.js +65 -0
- package/dist/chunk-ELYUDQHH.js +42 -0
- package/dist/chunk-QE3BBW25.js +18 -0
- package/dist/chunk-X3GT7AOS.js +593 -0
- package/dist/chunk-ZELUPPQB.js +95 -0
- package/dist/config-LILlPYgW.d.ts +55 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +2 -0
- package/dist/llms.d.ts +8 -0
- package/dist/llms.js +16 -0
- package/dist/page.d.ts +41 -0
- package/dist/page.js +190 -0
- package/dist/proxy.d.ts +18 -0
- package/dist/proxy.js +112 -0
- package/dist/sitemap.d.ts +11 -0
- package/dist/sitemap.js +19 -0
- package/dist/style.d.ts +8 -0
- package/dist/style.js +12 -0
- package/dist/verify.d.ts +12 -0
- package/dist/verify.js +17 -0
- package/package.json +73 -4
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
|
|
3
|
+
/** Revalidation period of every psst route, in seconds. Route files must repeat it as a literal. */
|
|
4
|
+
declare const PSST_REVALIDATE = 300;
|
|
5
|
+
/** Default psst edge. */
|
|
6
|
+
declare const DEFAULT_EDGE = "https://edge.saypsst.com";
|
|
7
|
+
declare const configSchema: z.ZodObject<{
|
|
8
|
+
site: z.ZodString;
|
|
9
|
+
domain: z.ZodString;
|
|
10
|
+
siteName: z.ZodString;
|
|
11
|
+
prefix: z.ZodDefault<z.ZodString>;
|
|
12
|
+
rootPaths: z.ZodDefault<z.ZodArray<z.ZodEnum<{
|
|
13
|
+
"/about": "/about";
|
|
14
|
+
"/llms.txt": "/llms.txt";
|
|
15
|
+
}>>>;
|
|
16
|
+
edge: z.ZodDefault<z.ZodString>;
|
|
17
|
+
serveFromEdge: z.ZodDefault<z.ZodBoolean>;
|
|
18
|
+
shell: z.ZodOptional<z.ZodObject<{
|
|
19
|
+
nav: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
20
|
+
label: z.ZodString;
|
|
21
|
+
href: z.ZodString;
|
|
22
|
+
}, z.core.$strict>>>;
|
|
23
|
+
footer: z.ZodOptional<z.ZodString>;
|
|
24
|
+
}, z.core.$strict>>;
|
|
25
|
+
}, z.core.$strict>;
|
|
26
|
+
type PsstConfigInput = z.input<typeof configSchema>;
|
|
27
|
+
/** Validated psst configuration. Create it with `definePsst()` in `psst.config.ts`. */
|
|
28
|
+
interface PsstConfig {
|
|
29
|
+
readonly site: string;
|
|
30
|
+
readonly domain: string;
|
|
31
|
+
readonly siteName: string;
|
|
32
|
+
readonly prefix: string;
|
|
33
|
+
readonly rootPaths: readonly ("/about" | "/llms.txt")[];
|
|
34
|
+
/** Edge origin without a trailing slash. */
|
|
35
|
+
readonly edge: string;
|
|
36
|
+
/** Psst paths are rewritten to the edge by `withPsst()` instead of served by route files. */
|
|
37
|
+
readonly serveFromEdge: boolean;
|
|
38
|
+
readonly shell?: {
|
|
39
|
+
readonly nav?: readonly {
|
|
40
|
+
readonly label: string;
|
|
41
|
+
readonly href: string;
|
|
42
|
+
}[];
|
|
43
|
+
readonly footer?: string;
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
declare class PsstConfigError extends Error {
|
|
47
|
+
constructor(message: string);
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Validate the site's psst configuration. Throws `PsstConfigError` on invalid input, so a bad config fails the
|
|
51
|
+
* build instead of serving broken pages.
|
|
52
|
+
*/
|
|
53
|
+
declare function definePsst(input: PsstConfigInput): PsstConfig;
|
|
54
|
+
|
|
55
|
+
export { DEFAULT_EDGE as D, PSST_REVALIDATE as P, type PsstConfig as a, PsstConfigError as b, type PsstConfigInput as c, definePsst as d };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +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-LILlPYgW.js';
|
|
2
|
+
import 'zod';
|
|
3
|
+
|
|
4
|
+
/** Kept equal to package.json `version` (checked by test/version.test.ts). */
|
|
5
|
+
declare const PSST_NEXT_VERSION = "0.3.0";
|
|
6
|
+
|
|
7
|
+
export { PSST_NEXT_VERSION };
|
package/dist/index.js
ADDED
package/dist/llms.d.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { a as PsstConfig } from './config-LILlPYgW.js';
|
|
2
|
+
export { P as PSST_REVALIDATE } from './config-LILlPYgW.js';
|
|
3
|
+
import 'zod';
|
|
4
|
+
|
|
5
|
+
/** `GET` for `app/llms.txt/route.ts`, only when the dashboard claimed `/llms.txt` (`rootPaths`). */
|
|
6
|
+
declare function psstLlms(config: PsstConfig): () => Promise<Response>;
|
|
7
|
+
|
|
8
|
+
export { psstLlms };
|
package/dist/llms.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
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
|
+
|
|
8
|
+
// src/llms.ts
|
|
9
|
+
function psstLlms(config) {
|
|
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");
|
|
12
|
+
}
|
|
13
|
+
return edgeFileRoute(config, { path: "/llms.txt", type: "text/plain", maxBytes: 512 * 1024 });
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export { psstLlms };
|
package/dist/page.d.ts
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { Metadata } from 'next';
|
|
2
|
+
import { ReactNode, ComponentType } from 'react';
|
|
3
|
+
import { a as PsstConfig } from './config-LILlPYgW.js';
|
|
4
|
+
export { P as PSST_REVALIDATE } from './config-LILlPYgW.js';
|
|
5
|
+
import 'zod';
|
|
6
|
+
|
|
7
|
+
/** Props your `Shell` receives: the psst `<main>` element. Render your header and footer around it. */
|
|
8
|
+
interface PsstShellProps {
|
|
9
|
+
children: ReactNode;
|
|
10
|
+
}
|
|
11
|
+
interface PsstPageOptions {
|
|
12
|
+
/** Your site's header/footer wrapper. Without it, psst renders a minimal header (brand + `shell.nav`) and footer. */
|
|
13
|
+
Shell?: ComponentType<PsstShellProps>;
|
|
14
|
+
/** Serve one fixed root path (`/about`) instead of `{prefix}/[...slug]`. Must be in `rootPaths`. */
|
|
15
|
+
path?: "/about";
|
|
16
|
+
}
|
|
17
|
+
/** Params of the catch-all route `{prefix}/[...slug]`. A fixed-path page gets none. */
|
|
18
|
+
interface PsstPageProps {
|
|
19
|
+
params: Promise<{
|
|
20
|
+
slug?: string[];
|
|
21
|
+
}>;
|
|
22
|
+
}
|
|
23
|
+
interface PsstPageModule {
|
|
24
|
+
default: (props: PsstPageProps) => Promise<ReactNode>;
|
|
25
|
+
generateMetadata: (props: PsstPageProps) => Promise<Metadata>;
|
|
26
|
+
generateStaticParams: () => Promise<{
|
|
27
|
+
slug: string[];
|
|
28
|
+
}[]>;
|
|
29
|
+
}
|
|
30
|
+
/** The served document failed the content contract or its hash. Thrown so ISR keeps the last good render. */
|
|
31
|
+
declare class PsstIntegrityError extends Error {
|
|
32
|
+
constructor(message: string);
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Build the page module for `app/(psst){prefix}/[...slug]/page.tsx` (or a fixed root path such as `/about`).
|
|
36
|
+
* The route file must also export the literals `revalidate = 300` and `dynamicParams = true`: Next reads route
|
|
37
|
+
* segment config statically, so it cannot come from a package.
|
|
38
|
+
*/
|
|
39
|
+
declare function psstPage(config: PsstConfig, options?: PsstPageOptions): PsstPageModule;
|
|
40
|
+
|
|
41
|
+
export { PsstIntegrityError, type PsstPageModule, type PsstPageOptions, type PsstPageProps, type PsstShellProps, psstPage };
|
package/dist/page.js
ADDED
|
@@ -0,0 +1,190 @@
|
|
|
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
|
+
import { notFound } from 'next/navigation';
|
|
7
|
+
import { connection } from 'next/server';
|
|
8
|
+
import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
|
|
9
|
+
|
|
10
|
+
function linkProps(href) {
|
|
11
|
+
return href.startsWith("/") ? { href } : { href, rel: "noopener" };
|
|
12
|
+
}
|
|
13
|
+
function InlineContent({ items }) {
|
|
14
|
+
return items.map((i, n) => {
|
|
15
|
+
switch (i.type) {
|
|
16
|
+
case "text":
|
|
17
|
+
return i.text;
|
|
18
|
+
case "strong":
|
|
19
|
+
return /* @__PURE__ */ jsx("strong", { children: i.text }, n);
|
|
20
|
+
case "em":
|
|
21
|
+
return /* @__PURE__ */ jsx("em", { children: i.text }, n);
|
|
22
|
+
case "link":
|
|
23
|
+
return /* @__PURE__ */ jsx("a", { ...linkProps(i.href), children: i.text }, n);
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
function Table({ b, className }) {
|
|
28
|
+
return /* @__PURE__ */ jsxs("table", { className, children: [
|
|
29
|
+
/* @__PURE__ */ jsx("caption", { children: b.caption }),
|
|
30
|
+
/* @__PURE__ */ jsx("thead", { children: /* @__PURE__ */ jsx("tr", { children: b.columns.map((c, n) => /* @__PURE__ */ jsx("th", { scope: "col", children: c }, n)) }) }),
|
|
31
|
+
/* @__PURE__ */ jsx("tbody", { children: b.rows.map((r, n) => {
|
|
32
|
+
const [first = "", ...rest] = r;
|
|
33
|
+
return /* @__PURE__ */ jsxs("tr", { children: [
|
|
34
|
+
/* @__PURE__ */ jsx("th", { scope: "row", children: first }),
|
|
35
|
+
rest.map((c, m) => /* @__PURE__ */ jsx("td", { children: c }, m))
|
|
36
|
+
] }, n);
|
|
37
|
+
}) })
|
|
38
|
+
] });
|
|
39
|
+
}
|
|
40
|
+
function BlockView({ b }) {
|
|
41
|
+
switch (b.type) {
|
|
42
|
+
case "heading":
|
|
43
|
+
return b.level === 2 ? /* @__PURE__ */ jsx("h2", { children: b.text }) : /* @__PURE__ */ jsx("h3", { children: b.text });
|
|
44
|
+
case "paragraph":
|
|
45
|
+
return /* @__PURE__ */ jsx("p", { children: /* @__PURE__ */ jsx(InlineContent, { items: b.inline }) });
|
|
46
|
+
case "list": {
|
|
47
|
+
const items = b.items.map((i, n) => /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsx(InlineContent, { items: i }) }, n));
|
|
48
|
+
return b.ordered ? /* @__PURE__ */ jsx("ol", { children: items }) : /* @__PURE__ */ jsx("ul", { children: items });
|
|
49
|
+
}
|
|
50
|
+
case "table":
|
|
51
|
+
return /* @__PURE__ */ jsx(Table, { b, className: "psst-table" });
|
|
52
|
+
case "faq":
|
|
53
|
+
return /* @__PURE__ */ jsx("section", { className: "psst-faq", "aria-label": "Frequently asked questions", children: /* @__PURE__ */ jsx("dl", { children: b.items.flatMap((i, n) => [
|
|
54
|
+
/* @__PURE__ */ jsx("dt", { children: i.q }, `q${n}`),
|
|
55
|
+
/* @__PURE__ */ jsx("dd", { children: /* @__PURE__ */ jsx(InlineContent, { items: i.a }) }, `a${n}`)
|
|
56
|
+
]) }) });
|
|
57
|
+
case "comparison":
|
|
58
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
59
|
+
/* @__PURE__ */ jsx(Table, { b, className: "psst-table psst-comparison" }),
|
|
60
|
+
b.notes.length > 0 ? /* @__PURE__ */ jsx("ul", { className: "psst-notes", children: b.notes.map((note, n) => /* @__PURE__ */ jsx("li", { children: note }, n)) }) : null
|
|
61
|
+
] });
|
|
62
|
+
case "facts":
|
|
63
|
+
return /* @__PURE__ */ jsx("dl", { className: "psst-facts", children: b.items.flatMap((i, n) => [/* @__PURE__ */ jsx("dt", { children: i.label }, `l${n}`), /* @__PURE__ */ jsx("dd", { children: factValue(i) }, `v${n}`)]) });
|
|
64
|
+
case "cta":
|
|
65
|
+
return /* @__PURE__ */ jsx("p", { className: "psst-cta", children: /* @__PURE__ */ jsx("a", { className: "psst-button", ...linkProps(b.href), children: b.label }) });
|
|
66
|
+
case "image":
|
|
67
|
+
return /* @__PURE__ */ jsx("figure", { children: /* @__PURE__ */ jsx("img", { src: b.src, alt: b.alt, width: b.width, height: b.height, loading: "lazy", decoding: "async" }) });
|
|
68
|
+
case "citation":
|
|
69
|
+
return /* @__PURE__ */ jsxs("p", { className: "psst-citation", children: [
|
|
70
|
+
/* @__PURE__ */ jsx("cite", { children: /* @__PURE__ */ jsx("a", { href: b.url, rel: "noopener", children: b.title }) }),
|
|
71
|
+
", accessed ",
|
|
72
|
+
/* @__PURE__ */ jsx("time", { dateTime: b.accessed, children: b.accessed })
|
|
73
|
+
] });
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
function PsstMain({ doc, ctx }) {
|
|
77
|
+
const jsonLd = serializeJsonForScript(buildJsonLd(doc, ctx));
|
|
78
|
+
return /* @__PURE__ */ jsxs("main", { children: [
|
|
79
|
+
/* @__PURE__ */ jsx("script", { type: "application/ld+json", dangerouslySetInnerHTML: { __html: jsonLd } }),
|
|
80
|
+
/* @__PURE__ */ jsxs("article", { className: "psst-article", children: [
|
|
81
|
+
/* @__PURE__ */ jsx("h1", { children: doc.h1 }),
|
|
82
|
+
/* @__PURE__ */ jsx("p", { className: "psst-lede", children: doc.lede }),
|
|
83
|
+
/* @__PURE__ */ jsxs("p", { className: "psst-updated", children: [
|
|
84
|
+
"Updated ",
|
|
85
|
+
/* @__PURE__ */ jsx("time", { dateTime: doc.updatedAt, children: doc.updatedAt.slice(0, 10) })
|
|
86
|
+
] }),
|
|
87
|
+
doc.blocks.map((b, n) => /* @__PURE__ */ jsx(BlockView, { b }, n)),
|
|
88
|
+
doc.sources.length > 0 ? /* @__PURE__ */ jsxs("section", { className: "psst-sources", children: [
|
|
89
|
+
/* @__PURE__ */ jsx("h2", { children: "Sources" }),
|
|
90
|
+
/* @__PURE__ */ jsx("ol", { children: doc.sources.map((s, n) => /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsx("a", { href: s.url, rel: "noopener", children: s.title }) }, n)) })
|
|
91
|
+
] }) : null
|
|
92
|
+
] })
|
|
93
|
+
] });
|
|
94
|
+
}
|
|
95
|
+
function DefaultShell({ siteName, nav = [], footer, children }) {
|
|
96
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
97
|
+
/* @__PURE__ */ jsxs("header", { className: "psst-header", children: [
|
|
98
|
+
/* @__PURE__ */ jsx("a", { className: "psst-brand", href: "/", children: siteName }),
|
|
99
|
+
nav.length > 0 ? /* @__PURE__ */ jsx("nav", { "aria-label": "Primary", children: nav.map((l, n) => /* @__PURE__ */ jsx("a", { ...linkProps(l.href), children: l.label }, n)) }) : null
|
|
100
|
+
] }),
|
|
101
|
+
children,
|
|
102
|
+
footer ? /* @__PURE__ */ jsx("footer", { className: "psst-footer", children: /* @__PURE__ */ jsx("p", { children: footer }) }) : null
|
|
103
|
+
] });
|
|
104
|
+
}
|
|
105
|
+
var PsstIntegrityError = class extends Error {
|
|
106
|
+
constructor(message) {
|
|
107
|
+
super(`PSST: ${message}`);
|
|
108
|
+
this.name = "PsstIntegrityError";
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
async function resolvePath(config, fixed, props) {
|
|
112
|
+
if (fixed) return fixed;
|
|
113
|
+
const { slug } = await props.params;
|
|
114
|
+
if (!slug || slug.length === 0) notFound();
|
|
115
|
+
const path = `${config.prefix}/${slug.join("/")}`;
|
|
116
|
+
if (!sitePathSchema.safeParse(path).success) notFound();
|
|
117
|
+
return path;
|
|
118
|
+
}
|
|
119
|
+
async function fetchAtRender(config, path) {
|
|
120
|
+
try {
|
|
121
|
+
return await fetchPage(config, path);
|
|
122
|
+
} catch (err) {
|
|
123
|
+
if (err instanceof PsstUnavailableError && isBuildPhase()) {
|
|
124
|
+
console.warn(`${err.message}; ${path} will render at request time for this build`);
|
|
125
|
+
await connection();
|
|
126
|
+
}
|
|
127
|
+
throw err;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
async function load(config, fixed, props) {
|
|
131
|
+
const path = await resolvePath(config, fixed, props);
|
|
132
|
+
const result = await fetchAtRender(config, path);
|
|
133
|
+
if (result.status !== "ok") notFound();
|
|
134
|
+
const parsed = parsePageDocument(result.page.document, { domain: config.domain });
|
|
135
|
+
if (!parsed.success) throw new PsstIntegrityError(`document for ${path} failed the content contract`);
|
|
136
|
+
const hash = hashDocument(parsed.data);
|
|
137
|
+
if (hash !== result.page.hash) throw new PsstIntegrityError(`document hash mismatch for ${path}`);
|
|
138
|
+
return { path, doc: parsed.data, version: shortHash(hash) };
|
|
139
|
+
}
|
|
140
|
+
function psstPage(config, options = {}) {
|
|
141
|
+
const fixed = options.path;
|
|
142
|
+
if (fixed && !config.rootPaths.includes(fixed)) {
|
|
143
|
+
throw new PsstConfigError(`${fixed} is not in rootPaths; PSST only serves root paths the dashboard claimed`);
|
|
144
|
+
}
|
|
145
|
+
const Shell = options.Shell;
|
|
146
|
+
async function Page(props) {
|
|
147
|
+
const { path, doc } = await load(config, fixed, props);
|
|
148
|
+
const main = /* @__PURE__ */ jsx(PsstMain, { doc, ctx: { siteName: config.siteName, domain: config.domain, canonicalPath: path } });
|
|
149
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
150
|
+
/* @__PURE__ */ jsx("link", { rel: "stylesheet", href: `${config.prefix}/_psst/style.css`, precedence: "psst" }),
|
|
151
|
+
Shell ? /* @__PURE__ */ jsx(Shell, { children: main }) : /* @__PURE__ */ jsx(DefaultShell, { siteName: config.siteName, nav: config.shell?.nav, footer: config.shell?.footer, children: main })
|
|
152
|
+
] });
|
|
153
|
+
}
|
|
154
|
+
async function generateMetadata(props) {
|
|
155
|
+
const { path, doc, version } = await load(config, fixed, props);
|
|
156
|
+
const canonical = `https://${config.domain}${path}`;
|
|
157
|
+
return {
|
|
158
|
+
title: { absolute: doc.title },
|
|
159
|
+
description: doc.description,
|
|
160
|
+
alternates: { canonical },
|
|
161
|
+
robots: { index: true, follow: true },
|
|
162
|
+
openGraph: {
|
|
163
|
+
type: doc.kind === "about" ? "website" : "article",
|
|
164
|
+
title: doc.title,
|
|
165
|
+
description: doc.description,
|
|
166
|
+
url: canonical,
|
|
167
|
+
siteName: config.siteName
|
|
168
|
+
},
|
|
169
|
+
twitter: { card: "summary", title: doc.title, description: doc.description },
|
|
170
|
+
other: { "psst-version": version }
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
async function generateStaticParams() {
|
|
174
|
+
if (fixed) return [];
|
|
175
|
+
try {
|
|
176
|
+
const manifest = await fetchManifest(config);
|
|
177
|
+
const base = `${config.prefix}/`;
|
|
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
|
+
} catch (err) {
|
|
180
|
+
if (err instanceof PsstUnavailableError || err instanceof ContentError) {
|
|
181
|
+
console.warn(`${err.message}; prerendering no PSST pages, they render on first request`);
|
|
182
|
+
return [];
|
|
183
|
+
}
|
|
184
|
+
throw err;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
return { default: Page, generateMetadata, generateStaticParams };
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export { PsstIntegrityError, psstPage };
|
package/dist/proxy.d.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { NextRequest, NextFetchEvent } from 'next/server';
|
|
2
|
+
import { a as PsstConfig } from './config-LILlPYgW.js';
|
|
3
|
+
import 'zod';
|
|
4
|
+
|
|
5
|
+
type ProxyResult = Response | null | undefined | void;
|
|
6
|
+
/** The shape of a Next proxy (`proxy.ts`, Next 16) or middleware (`middleware.ts`, Next 15) function. */
|
|
7
|
+
type PsstProxy = (request: NextRequest, event: NextFetchEvent) => ProxyResult | Promise<ProxyResult>;
|
|
8
|
+
/**
|
|
9
|
+
* Wrap the site's proxy. On every request: classify AI crawlers (by User-Agent, a claim not a proof) and AI
|
|
10
|
+
* referrers, and post a hit without awaiting it (`event.waitUntil`); no IP, cookie, query or UA string is sent.
|
|
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.
|
|
15
|
+
*/
|
|
16
|
+
declare function withPsst(config: PsstConfig, proxy?: PsstProxy): PsstProxy;
|
|
17
|
+
|
|
18
|
+
export { type PsstProxy, withPsst };
|
package/dist/proxy.js
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
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';
|
|
4
|
+
|
|
5
|
+
// src/hits.ts
|
|
6
|
+
var ASSET = /\.(?:js|mjs|css|map|png|jpe?g|gif|webp|avif|svg|ico|woff2?|ttf|otf|mp4|webm)$/i;
|
|
7
|
+
function hitFor(config, request) {
|
|
8
|
+
if (request.method !== "GET" && request.method !== "HEAD") return null;
|
|
9
|
+
const h = request.headers;
|
|
10
|
+
if (h.get("x-psst-doctor") === "1") return null;
|
|
11
|
+
if (h.has("rsc") || h.has("next-router-prefetch") || h.get("purpose") === "prefetch" || h.get("sec-purpose")?.includes("prefetch")) {
|
|
12
|
+
return null;
|
|
13
|
+
}
|
|
14
|
+
const path = request.nextUrl.pathname;
|
|
15
|
+
if (path.startsWith("/_next/") || ASSET.test(path)) return null;
|
|
16
|
+
if (!sitePathSchema.safeParse(path).success) return null;
|
|
17
|
+
const bot = classifyUserAgent(h.get("user-agent"));
|
|
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
|
+
const hit = hitRequestSchema.safeParse({
|
|
21
|
+
v: CONTENT_SCHEMA_VERSION,
|
|
22
|
+
publicId: config.site,
|
|
23
|
+
path,
|
|
24
|
+
agent: bot.agent,
|
|
25
|
+
agentClass: bot.agentClass,
|
|
26
|
+
referrerClass,
|
|
27
|
+
source: "proxy"
|
|
28
|
+
});
|
|
29
|
+
return hit.success ? hit.data : null;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// src/proxy.ts
|
|
33
|
+
var HIT_TIMEOUT_MS = 300;
|
|
34
|
+
var MANIFEST_TTL_MS = 3e5;
|
|
35
|
+
var MANIFEST_TIMEOUT_MS = 2e3;
|
|
36
|
+
async function postHit(config, hit) {
|
|
37
|
+
try {
|
|
38
|
+
await fetch(`${config.edge}/v1/hits`, {
|
|
39
|
+
method: "POST",
|
|
40
|
+
headers: { "content-type": "application/json", "user-agent": USER_AGENT },
|
|
41
|
+
body: JSON.stringify(hit),
|
|
42
|
+
signal: AbortSignal.timeout(HIT_TIMEOUT_MS)
|
|
43
|
+
});
|
|
44
|
+
} catch {
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
function withdrawnTracker(config) {
|
|
48
|
+
const state = { paths: /* @__PURE__ */ new Set(), fetchedAt: 0, refreshing: null };
|
|
49
|
+
return {
|
|
50
|
+
get: () => state.paths,
|
|
51
|
+
refreshIfStale(now) {
|
|
52
|
+
if (state.refreshing || now - state.fetchedAt < MANIFEST_TTL_MS) return null;
|
|
53
|
+
state.refreshing = fetchManifest(config, MANIFEST_TIMEOUT_MS).then((m) => {
|
|
54
|
+
state.paths = new Set(m.withdrawn);
|
|
55
|
+
state.fetchedAt = Date.now();
|
|
56
|
+
}).catch(() => {
|
|
57
|
+
state.fetchedAt = Date.now();
|
|
58
|
+
}).finally(() => {
|
|
59
|
+
state.refreshing = null;
|
|
60
|
+
});
|
|
61
|
+
return state.refreshing;
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
function isPsstPath(config, path) {
|
|
66
|
+
return path.startsWith(`${config.prefix}/`) || config.rootPaths.includes(path);
|
|
67
|
+
}
|
|
68
|
+
function gone() {
|
|
69
|
+
return new Response("Gone\n", {
|
|
70
|
+
status: 410,
|
|
71
|
+
headers: { "content-type": "text/plain; charset=utf-8", "cache-control": "public, max-age=0, s-maxage=300" }
|
|
72
|
+
});
|
|
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
|
+
}
|
|
93
|
+
function withPsst(config, proxy) {
|
|
94
|
+
const withdrawn = withdrawnTracker(config);
|
|
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
|
+
}
|
|
101
|
+
const hit = hitFor(config, request);
|
|
102
|
+
if (hit) event.waitUntil(postHit(config, hit));
|
|
103
|
+
if (isPsstPath(config, path)) {
|
|
104
|
+
const refresh = withdrawn.refreshIfStale(Date.now());
|
|
105
|
+
if (refresh) event.waitUntil(refresh);
|
|
106
|
+
if (withdrawn.get().has(path)) return gone();
|
|
107
|
+
}
|
|
108
|
+
return proxy ? proxy(request, event) : void 0;
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export { withPsst };
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { a as PsstConfig } from './config-LILlPYgW.js';
|
|
2
|
+
export { P as PSST_REVALIDATE } from './config-LILlPYgW.js';
|
|
3
|
+
import 'zod';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* `GET` for `app/(psst){prefix}/sitemap.xml/route.ts`: the published HTML pages psst owns (the manifest already
|
|
7
|
+
* leaves withdrawn paths out), rendered with `renderSitemap` from `@psst/content`.
|
|
8
|
+
*/
|
|
9
|
+
declare function psstSitemap(config: PsstConfig): () => Promise<Response>;
|
|
10
|
+
|
|
11
|
+
export { psstSitemap };
|
package/dist/sitemap.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
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
|
+
|
|
6
|
+
// src/sitemap.ts
|
|
7
|
+
function psstSitemap(config) {
|
|
8
|
+
return psstRoute(async () => {
|
|
9
|
+
const manifest = await fetchManifest(config);
|
|
10
|
+
if (manifest.site.domain !== config.domain) {
|
|
11
|
+
throw new PsstUnavailableError(`manifest domain ${manifest.site.domain} does not match psst.config domain ${config.domain}`);
|
|
12
|
+
}
|
|
13
|
+
const pages = manifest.pages.filter((p) => p.kind !== "llms_txt").map((p) => ({ path: p.path, title: p.title, updatedAt: p.updatedAt }));
|
|
14
|
+
const xml = renderSitemap({ siteName: config.siteName, domain: config.domain }, pages);
|
|
15
|
+
return textResponse(xml, "application/xml; charset=utf-8");
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export { psstSitemap };
|
package/dist/style.d.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { a as PsstConfig } from './config-LILlPYgW.js';
|
|
2
|
+
export { P as PSST_REVALIDATE } from './config-LILlPYgW.js';
|
|
3
|
+
import 'zod';
|
|
4
|
+
|
|
5
|
+
/** `GET` for `app/(psst){prefix}/_psst/style.css/route.ts`: the psst stylesheet, served same-origin. */
|
|
6
|
+
declare function psstStyle(config: PsstConfig): () => Promise<Response>;
|
|
7
|
+
|
|
8
|
+
export { psstStyle };
|
package/dist/style.js
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
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
|
+
|
|
7
|
+
// src/style.ts
|
|
8
|
+
function psstStyle(config) {
|
|
9
|
+
return edgeFileRoute(config, { path: `${config.prefix}/_psst/style.css`, type: "text/css", maxBytes: 256 * 1024 });
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export { psstStyle };
|
package/dist/verify.d.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { a as PsstConfig } from './config-LILlPYgW.js';
|
|
2
|
+
export { P as PSST_REVALIDATE } from './config-LILlPYgW.js';
|
|
3
|
+
import 'zod';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* `GET` for `app/(psst){prefix}/_psst/verify/route.ts`: the domain-proof text
|
|
7
|
+
* (`psst-site-verification: {publicId}.{proofNonce}`), copied from the edge so psst can check the site's own domain
|
|
8
|
+
* serves it.
|
|
9
|
+
*/
|
|
10
|
+
declare function psstVerify(config: PsstConfig): () => Promise<Response>;
|
|
11
|
+
|
|
12
|
+
export { psstVerify };
|
package/dist/verify.js
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
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
|
+
|
|
7
|
+
// src/verify.ts
|
|
8
|
+
function psstVerify(config) {
|
|
9
|
+
return edgeFileRoute(config, {
|
|
10
|
+
path: `${config.prefix}/_psst/verify`,
|
|
11
|
+
type: "text/plain",
|
|
12
|
+
maxBytes: 1024,
|
|
13
|
+
accept: (body) => body.includes(`${config.site}.`)
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export { psstVerify };
|
package/package.json
CHANGED
|
@@ -1,6 +1,75 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@saypsst/next",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.3.0",
|
|
4
|
+
"description": "Drop-in Next.js App Router route modules that serve PSST pages on your own domain.",
|
|
5
|
+
"license": "UNLICENSED",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"sideEffects": false,
|
|
8
|
+
"files": [
|
|
9
|
+
"dist",
|
|
10
|
+
"README.md"
|
|
11
|
+
],
|
|
12
|
+
"exports": {
|
|
13
|
+
".": {
|
|
14
|
+
"types": "./dist/index.d.ts",
|
|
15
|
+
"default": "./dist/index.js"
|
|
16
|
+
},
|
|
17
|
+
"./page": {
|
|
18
|
+
"types": "./dist/page.d.ts",
|
|
19
|
+
"default": "./dist/page.js"
|
|
20
|
+
},
|
|
21
|
+
"./llms": {
|
|
22
|
+
"types": "./dist/llms.d.ts",
|
|
23
|
+
"default": "./dist/llms.js"
|
|
24
|
+
},
|
|
25
|
+
"./sitemap": {
|
|
26
|
+
"types": "./dist/sitemap.d.ts",
|
|
27
|
+
"default": "./dist/sitemap.js"
|
|
28
|
+
},
|
|
29
|
+
"./verify": {
|
|
30
|
+
"types": "./dist/verify.d.ts",
|
|
31
|
+
"default": "./dist/verify.js"
|
|
32
|
+
},
|
|
33
|
+
"./style": {
|
|
34
|
+
"types": "./dist/style.d.ts",
|
|
35
|
+
"default": "./dist/style.js"
|
|
36
|
+
},
|
|
37
|
+
"./proxy": {
|
|
38
|
+
"types": "./dist/proxy.d.ts",
|
|
39
|
+
"default": "./dist/proxy.js"
|
|
40
|
+
},
|
|
41
|
+
"./package.json": "./package.json"
|
|
42
|
+
},
|
|
43
|
+
"scripts": {
|
|
44
|
+
"typecheck": "tsc --noEmit",
|
|
45
|
+
"lint": "eslint src test",
|
|
46
|
+
"test": "vitest run",
|
|
47
|
+
"build": "tsup && node scripts/check-dist.mjs"
|
|
48
|
+
},
|
|
49
|
+
"publishConfig": {
|
|
50
|
+
"access": "public",
|
|
51
|
+
"provenance": true
|
|
52
|
+
},
|
|
53
|
+
"engines": {
|
|
54
|
+
"node": ">=20"
|
|
55
|
+
},
|
|
56
|
+
"peerDependencies": {
|
|
57
|
+
"next": ">=15 <17",
|
|
58
|
+
"react": ">=19"
|
|
59
|
+
},
|
|
60
|
+
"dependencies": {
|
|
61
|
+
"zod": "4.6.7"
|
|
62
|
+
},
|
|
63
|
+
"devDependencies": {
|
|
64
|
+
"@psst/content": "workspace:*",
|
|
65
|
+
"@types/node": "20.19.43",
|
|
66
|
+
"@types/react": "19.3.0",
|
|
67
|
+
"@types/react-dom": "19.3.0",
|
|
68
|
+
"eslint": "^9",
|
|
69
|
+
"next": "16.4.3",
|
|
70
|
+
"react": "19.3.0",
|
|
71
|
+
"react-dom": "19.3.0",
|
|
72
|
+
"tsup": "8.5.1",
|
|
73
|
+
"typescript-eslint": "^8"
|
|
74
|
+
}
|
|
75
|
+
}
|