saypsst 0.0.0-stage → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,66 @@
1
+ ---
2
+ name: psst
3
+ description: PSST publishes pages that make this brand show up in AI answers (ChatGPT, Claude, Gemini, Perplexity, Google AI Overviews) on this site's own domain, under a path prefix PSST owns (default /resources), plus optionally /about and /llms.txt. Use it for finishing setup after `npx saypsst init`; any change near psst.config.ts, app/(psst)/, the PSST rewrites in vercel.json, or the proxy's withPsst(); checking that PSST pages are live and visible to AI crawlers (`npx saypsst doctor`); reading what PSST has published (`npx saypsst status`); and any PSST message id (P01…P44), exit code or build problem.
4
+ ---
5
+
6
+ # PSST
7
+
8
+ PSST helps a brand show up in AI answers. Its agents find the most useful page the site is missing (an `/about`, an `/llms.txt`, a guide under the prefix), draft it from the site's own facts, and, once an admin approves it in the PSST dashboard, publish it **on this site's domain** with no deploy. Every page is plain HTML (no JavaScript), the same for people and crawlers, with a canonical URL, JSON-LD and a `psst-version` meta that proves which approved version is live.
9
+
10
+ The person installed PSST once, with one pull request. From then on PSST writes content, and the site serves it. You keep the plumbing working; you never write PSST content.
11
+
12
+ **First:**
13
+ - **Read the site's own file before any change:**
14
+
15
+ | The site | Read |
16
+ |---|---|
17
+ | `psst.config.ts` (or `.js`) imports from `@saypsst/next` | `nextjs.md` |
18
+ | `vercel.json` rewrites a path to `https://edge.saypsst.com/s/site_…` | `vercel-static.md` |
19
+ | Neither: PSST is routed by a CDN, a Worker or a reverse proxy | `proxy.md` |
20
+
21
+ - **When a command, doctor, a build log or a warning names a problem** (`P01` to `P44`, or an exit code), read `troubleshooting.md`.
22
+
23
+ ## Where things are
24
+
25
+ - **The site id** (`site_` + 16 characters) is public, not a secret. It is in `psst.config.ts` or in the `vercel.json` rewrite destination. PSST needs no key, token or secret anywhere: if something asks for one, it is not PSST.
26
+ - **The prefix** (`/resources` by default) belongs to PSST. The site must not serve anything else there. Root paths (`/about`, `/llms.txt`) are PSST's only when the dashboard claimed them and the site did not already serve them.
27
+ - **The content lives in PSST**, never in the repository. Pages change when an admin approves a new version in the dashboard, and appear within about 10 minutes (300 s ISR plus 300 s CDN cache).
28
+ - **`npx saypsst <command>`:**
29
+ - `init --site <id>`: sets up or repairs the plumbing. Idempotent: it marks with ✓ what is in place, does only what is missing, never overwrites an edit, and prints the exact edit for a file too unusual to change.
30
+ - `doctor [--wait] [--url <origin>]`: fetches the production site (or `--url`) as a browser and as GPTBot, OAI-SearchBot, ClaudeBot, PerplexityBot and Googlebot, and checks every PSST path. Its probes carry `X-Psst-Doctor: 1` and are never counted as crawler hits.
31
+ - `status`: what PSST has published, with versions.
32
+ - Every command takes `--json`; the shapes are in `node_modules/saypsst/schemas/` (or the CLI package's `schemas/`).
33
+ - **This skill's other files:** the site's own (above), and `troubleshooting.md`. `init` copies this folder into `.claude/skills/psst/` and `.agents/skills/psst/`, and replaces it on the next `init` when PSST updates it: do not edit it.
34
+
35
+ ## Setup after `npx saypsst init`
36
+
37
+ 1. **Read the end of init's output** (or run `npx saypsst init` again: it changes nothing that is in place).
38
+
39
+ | init ended with | Do this |
40
+ |---|---|
41
+ | All ✓, and `Next:` | Go to step 2. |
42
+ | `!` and a printed edit (P20 proxy, P21 robots, P22/P26 vercel.json, P23 config, P24 route file) | Make the edit exactly as printed, then run `npx saypsst init` again: the line turns ✓. |
43
+ | `Ask the person: "Add a two-line note to AGENTS.md…"` (P28) | Ask it, and say it is recommended: agents read AGENTS.md in every session. If yes, run `npx saypsst init --agents-md`. |
44
+ | Exit 2, P07 | Run init again with `--site-name "Brand"`: the brand as the site names itself (its `<title>`, logo alt, or layout metadata). Ask the person if it is unclear. |
45
+ | Exit 1 or 4 | Read `troubleshooting.md` at the message id. Nothing was half-written: init checks before it writes. |
46
+ | `Run pnpm install …` in `Next:` | Run it (init skipped it because of `--no-install` or `--dry-run`). |
47
+
48
+ 2. **Check the change like any other:** the repository's own typecheck, lint and build. For Next.js, `next build` must pass (the route files are plain re-exports; PSST being unreachable never fails a build).
49
+ 3. **Show the person `git diff`** in two sentences: PSST's routes under the prefix, the config with the public site id, the proxy wrapper, the robots line, this skill. Nothing changes for visitors until a page is published, and only under the prefix.
50
+ 4. **After the deploy**, run `npx saypsst doctor --wait` (give the Bash call a 360000 ms timeout). Exit 0 means the site serves PSST; exit 3 means it is still waiting on the deploy or caches: run it again. Before any page is published, doctor checks the domain proof, the stylesheet and the sitemap, and says "nothing is published yet" (P41): that is expected.
51
+ 5. **Leave the person oriented:** the first page appears after they approve it in the dashboard (Publishing), then PSST verifies it in production by itself.
52
+
53
+ ## Rules
54
+
55
+ - **Never edit PSST's route files by hand** (`app/(psst)/**`, a claimed `app/about/page.tsx` or `app/llms.txt/route.ts`), except to pass a `Shell` to `psstPage` (see `nextjs.md`). `export const revalidate = 300` must stay a literal in each file: Next.js reads it statically.
56
+ - **Never add PSST content locally**: no MDX, no pages, no copy under the prefix. Content changes go through the dashboard, where an admin approves each version.
57
+ - **Never put anything else under the prefix**, and never block it in the proxy, middleware, auth or a CDN rule.
58
+ - **Never change robots.txt policy for PSST** without the person. Doctor reports training, search and user-fetch crawlers separately; only blocked search or user-fetch agents fail it.
59
+ - **Never cache PSST paths longer than the origin says**, and never cache a 404 or 5xx there (a CDN rule that does breaks updates and outages).
60
+ - **Verify with doctor, not by reading code.** Done means `npx saypsst doctor` exits 0 against production.
61
+
62
+ ## Exit codes
63
+
64
+ - **0** done · **1** failed, with a message id · **2** the command or its arguments are wrong · **3** waiting on a deploy, caches or PSST: run the same command again · **4** PSST or the site is unreachable from here (a sandbox without network): allow network access for the command, or ask the person to run it.
65
+
66
+ With `--json`, a failure reads `{"contract":"1.0","command":"init","site":…,"exit":1,"error":{"id":"P02","code":"prefix_served","message":"…"}}`.
@@ -0,0 +1,84 @@
1
+ # PSST on Next.js (App Router)
2
+
3
+ This site serves PSST through `@saypsst/next`, route modules inside its own Next.js app (15 or 16, App Router, any host). `SKILL.md` holds what every site shares; this file holds the code. The package's own reference is `node_modules/@saypsst/next/README.md`.
4
+
5
+ ## What `init` set up
6
+
7
+ Paths assume `app/` and the prefix `/resources`. With `src/app/`, `psst.config.ts` and `proxy.ts` sit in `src/`. With another prefix, the folders under `(psst)` follow it.
8
+
9
+ | File | What it does |
10
+ |---|---|
11
+ | `package.json` | `@saypsst/next` at an exact version (its only dependency is `zod`; no install scripts) |
12
+ | `psst.config.ts` | `definePsst({ site, domain, siteName, prefix, rootPaths })`: the public site id, the production host, the brand name. `definePsst` validates it and fails the build on a typo. |
13
+ | `app/(psst)/resources/[...slug]/page.tsx` | Every PSST page under the prefix. Time-based ISR (`revalidate = 300`), `dynamicParams = true`. |
14
+ | `app/(psst)/resources/sitemap.xml/route.ts` | The PSST sitemap |
15
+ | `app/(psst)/resources/%5Fpsst/style.css/route.ts` | The PSST stylesheet, same-origin (no inline style, so strict CSPs pass) |
16
+ | `app/(psst)/resources/%5Fpsst/verify/route.ts` | The domain proof PSST checks before it publishes |
17
+ | `app/about/page.tsx`, `app/llms.txt/route.ts` | Only when the dashboard claimed the path and the app did not serve it |
18
+ | `proxy.ts` (Next 16) or `middleware.ts` (Next 15) | `withPsst(psst)`: counts AI-crawler fetches and AI referrals (classes only: no IP, no cookie, no query, no UA string), answers 410 for withdrawn pages. Never awaited, never throws. |
19
+ | `public/robots.txt` | One added line: `Sitemap: https://{domain}/resources/sitemap.xml` |
20
+
21
+ Two Next.js rules shape these files:
22
+ - Route segment config (`revalidate`, `dynamicParams`) must be a **literal in the route file**: a re-export from a package is ignored. Never move it into a helper.
23
+ - A folder starting with `_` is private in the App Router, so the URL segment `/_psst` lives in a folder named **`%5Fpsst`**. Never rename it.
24
+
25
+ ## The site's header and footer
26
+
27
+ PSST pages render inside the root layout. When that layout has no header or footer (common: they live in a `(marketing)` group layout), pass a `Shell` built from the site's own components. It is the one hand edit PSST allows in its route files, and `init` keeps it:
28
+
29
+ ```tsx
30
+ // app/(psst)/resources/[...slug]/page.tsx
31
+ import { psstPage, type PsstShellProps } from "@saypsst/next/page";
32
+ import psst from "../../../../psst.config";
33
+ import { SiteHeader, SiteFooter } from "@/components/site";
34
+
35
+ function Shell({ children }: PsstShellProps) {
36
+ return (
37
+ <>
38
+ <SiteHeader />
39
+ {children}
40
+ <SiteFooter />
41
+ </>
42
+ );
43
+ }
44
+
45
+ const page = psstPage(psst, { Shell });
46
+
47
+ export const revalidate = 300;
48
+ export const dynamicParams = true;
49
+ export const generateStaticParams = page.generateStaticParams;
50
+ export const generateMetadata = page.generateMetadata;
51
+ export default page.default;
52
+ ```
53
+
54
+ `children` is PSST's own `<main>`: do not wrap it in another `<main>`. The Shell must be a server component (no `"use client"` at its top; client components inside it are fine). Without a Shell, `psst.config.ts` can take `shell: { nav: [{ label, href }], footer }` for a plain built-in header and footer.
55
+
56
+ ## An existing proxy or middleware
57
+
58
+ `init` never rewrites the site's `proxy.ts` / `middleware.ts`. It prints the edit (P20): import `withPsst` and the config, rename the site's handler (`siteProxy`), and export `withPsst(psst, siteProxy)` under the original name. The site's handler runs unchanged and its response is returned as is. Then:
59
+ - keep `config.matcher` a **literal** (Next.js reads it statically) and make sure it also matches `/resources/:path*`;
60
+ - run `npx saypsst init` again: the line turns ✓ once `withPsst(` is there.
61
+
62
+ A proxy that rewrites, redirects or authenticates every path must let the prefix through untouched.
63
+
64
+ ## Caching and failures
65
+
66
+ - Pages and handlers revalidate every 300 s. When PSST is unreachable, the last good render keeps being served; a page never rendered answers 500 uncached (never a cached 404), and the next request retries.
67
+ - `next build` never fails because of PSST: unreachable at build, the pages render on first request.
68
+ - **Cache Components** (`cacheComponents: true`, P08): the segment config the route files need is removed there, so init writes no route files. `serveFromEdge: true` in `psst.config` makes `withPsst()` rewrite PSST paths to the edge, which renders them with its own header and footer, not the app's layout. To use the app's layout, turn the flag off and run init again.
69
+ - **`trailingSlash: true`**: Next redirects PSST's paths before any PSST code runs, so `doctor` and the domain proof report P39. Pages still answer after the redirect. With `serveFromEdge`, adding `skipTrailingSlashRedirect: true` to `next.config` clears it, but that changes the redirects of the whole site: ask the person first.
70
+ - **Self-hosted with several instances** (for example behind CloudFront): each instance keeps its own ISR cache, so updates land per instance within 300 s. The CDN in front must respect the origin's `Cache-Control` on the prefix.
71
+
72
+ ## Build or type errors
73
+
74
+ | Error | Fix |
75
+ |---|---|
76
+ | `Cannot find module '@saypsst/next/page'` | Install: run the package manager (`pnpm install`, `npm install`). |
77
+ | `invalid PSST config: …` (PsstConfigError) | A value in `psst.config.ts` is wrong; the message names it. `npx saypsst init` prints the expected keys (P23). |
78
+ | `Cannot find module '../../../../psst.config'` | The config moved. Put it back next to `app/` (or in `src/` with `src/app/`), or run `npx saypsst init`, which prints each route file's expected content (P24). |
79
+ | Type error in the Shell | Type its props as `PsstShellProps` from `@saypsst/next/page`. |
80
+ | A lint rule flags a PSST route file | Exclude `app/(psst)/**` from that rule rather than editing the file. |
81
+
82
+ ## Remove PSST
83
+
84
+ Delete `app/(psst)/`, the claimed root-path files, `psst.config.ts`, the `withPsst` wrapper (or the proxy file init created), the robots line, `@saypsst/next` from `package.json`, and `.claude/skills/psst/`. Ask the person to withdraw the pages in the dashboard first, so crawlers get 410s rather than 404s.
@@ -0,0 +1,78 @@
1
+ # PSST behind a proxy, a CDN or a Worker
2
+
3
+ For any site that is neither a Next.js App Router app nor a static site on Vercel: a Pages Router app, Nuxt, Astro SSR, WordPress, Shopify, or a Next.js app behind CloudFront whose infra owner prefers routing. PSST's edge renders full pages; the site's host or CDN routes PSST's paths to it. `init --mode proxy` writes nothing but the skill; this file has the snippets. `SKILL.md` holds what every site shares.
4
+
5
+ Throughout: `SITE` is the site id (`site_…`), `PREFIX` the prefix (`/resources`), `EDGE` is `https://edge.saypsst.com`. The edge path is always `EDGE/s/SITE` followed by the path on the site, unchanged.
6
+
7
+ ## What to route
8
+
9
+ | On the site | To |
10
+ |---|---|
11
+ | `PREFIX` and everything under it | `EDGE/s/SITE/PREFIX/…` (same path) |
12
+ | `/about`, `/llms.txt`, only if the dashboard claimed them | `EDGE/s/SITE/about`, `EDGE/s/SITE/llms.txt` |
13
+
14
+ Forward the method, the `User-Agent` and the `Referer` (PSST classifies AI crawlers and referrals from them; it stores no IP, cookie or UA string). Do not forward cookies or `Authorization`. Keep the response status and the `Cache-Control`, `Content-Type`, `Content-Security-Policy` and `X-Psst-Version` headers. Never cache a 404 or 5xx from the edge, and never cache longer than its `s-maxage`.
15
+
16
+ ## Cloudflare Worker (route `example.com/resources*`)
17
+
18
+ ```js
19
+ const EDGE = "https://edge.saypsst.com";
20
+ const SITE = "site_xxxxxxxxxxxxxxxx";
21
+
22
+ export default {
23
+ async fetch(request) {
24
+ const url = new URL(request.url);
25
+ const upstream = new URL(`/s/${SITE}${url.pathname}`, EDGE);
26
+ const headers = new Headers();
27
+ for (const h of ["user-agent", "referer", "accept", "x-psst-doctor"]) {
28
+ const v = request.headers.get(h);
29
+ if (v) headers.set(h, v);
30
+ }
31
+ return fetch(upstream, { method: request.method, headers, redirect: "manual" });
32
+ },
33
+ };
34
+ ```
35
+
36
+ Add one route per claimed root path (`example.com/about`, `example.com/llms.txt`) to the same Worker. Cache rules on the zone must "Respect origin" for these paths.
37
+
38
+ ## nginx
39
+
40
+ ```nginx
41
+ location ^~ /resources {
42
+ proxy_pass https://edge.saypsst.com/s/site_xxxxxxxxxxxxxxxx$request_uri;
43
+ proxy_set_header Host edge.saypsst.com;
44
+ proxy_set_header Cookie "";
45
+ proxy_set_header Authorization "";
46
+ proxy_ssl_server_name on;
47
+ proxy_intercept_errors off;
48
+ }
49
+ ```
50
+
51
+ A `proxy_pass` with a variable needs a `resolver` (for example `resolver 1.1.1.1;` in the `server` block). `$request_uri` keeps the query string, which the edge ignores. For a claimed root path, add `location = /about { … }` with the same body.
52
+
53
+ ## CloudFront
54
+
55
+ 1. Origin: `edge.saypsst.com`, HTTPS only, origin path `/s/site_xxxxxxxxxxxxxxxx`.
56
+ 2. Behavior: path pattern `/resources*` (above the default behavior), that origin, cache policy honouring origin headers (`CachingOptimized` with min TTL 0, or a policy with "Use origin cache headers"), origin request policy forwarding `User-Agent` and `Referer`, no cookies, no query strings.
57
+ 3. One behavior per claimed root path (`/about`, `/llms.txt`), same origin.
58
+ 4. Error pages: none for this behavior (a custom error response would turn PSST's 410 and 503 into the site's page).
59
+
60
+ ## Next.js Pages Router (P01)
61
+
62
+ A Pages Router app can route in `next.config.js` without any CDN:
63
+
64
+ ```js
65
+ async rewrites() {
66
+ return {
67
+ beforeFiles: [
68
+ { source: "/resources/:path*", destination: "https://edge.saypsst.com/s/site_xxxxxxxxxxxxxxxx/resources/:path*" },
69
+ ],
70
+ };
71
+ }
72
+ ```
73
+
74
+ `beforeFiles` keeps a `pages/[...slug].tsx` catch-all from shadowing it. Add the claimed root paths the same way.
75
+
76
+ ## Check it
77
+
78
+ `npx saypsst doctor --site site_xxxxxxxxxxxxxxxx --wait` once the route is live (doctor needs `--site` here, as nothing in the repository names it). Every check must pass: verify, stylesheet, sitemap, and each published page as a browser and as AI crawlers.
@@ -0,0 +1,62 @@
1
+ # Troubleshooting
2
+
3
+ **Start with `npx saypsst doctor`** (or `npx saypsst init` again: it repeats what is missing). Every message ends with an id such as `(P02)`; find it below. Running `init` again is always safe: it checks before it writes and never overwrites an edit.
4
+
5
+ **Exit codes:** 0 done · 1 failed · 2 usage · 3 waiting, run the same command again · 4 unreachable from here (allow network access for the command, or ask the person to run it).
6
+
7
+ ## init stops (nothing was written)
8
+
9
+ - **P01** `no_app_router`: a Next.js app without `app/` or `src/app/` (Pages Router only). PSST's Next.js routes need the App Router. Route PSST at the host instead: `proxy.md`, "Next.js Pages Router" (`beforeFiles` rewrites), then `npx saypsst init --mode proxy`.
10
+ - **P02** `prefix_served`: the live site already answers 200 under the prefix. Another prefix is needed: the person changes it in the dashboard (Settings › Connections), then run init again. Never delete the site's pages to make room.
11
+ - **P03** `prefix_route_exists`: the repository already has routes or files under the prefix (`app/resources/`, `public/resources/`, a rewrite or redirect). Same fix as P02.
12
+ - **P04** `edge_unreachable` (exit 4): PSST's edge cannot be reached from here, as in a sandbox without network. Allow network access for the command, or ask the person to run it.
13
+ - **P05** `site_unknown`: no site with that id. Copy the id again from the dashboard (Settings › Connections); it looks like `site_` + 16 letters and digits 2–7.
14
+ - **P06** `edge_error`: PSST answered something unexpected. If it says "contract", update the CLI (`npx saypsst@latest init`); otherwise retry later.
15
+ - **P07** `site_name_required` (exit 2): pass `--site-name "Brand"`, the brand as the site names itself. Normally not needed: init takes the brand name from the dashboard, then from `psst.config.ts`. Only an edge without it asks.
16
+ - **P08** `cache_components`: not a failure. `cacheComponents: true` in `next.config` removes the route segment config PSST's route files need, so init sets `serveFromEdge: true` and writes only `proxy.ts`; the edge serves PSST pages with its own header and footer. If init prints a route file to delete, delete it: it comes from an install made before the flag was on.
17
+ - **P09** `config_other_site`: `psst.config.ts` names another site. init never rewrites it. If the repository really moves to the new site, change `site` there by hand (ask the person), then run init.
18
+ - **P10** `prefix_mismatch` (exit 2): `--prefix` must equal the dashboard's prefix. Change it in the dashboard first, or drop `--prefix`.
19
+ - **P11** `domain_mismatch` (exit 2): `--domain` must equal the dashboard's domain. Fix the domain in the dashboard, or drop `--domain`.
20
+ - **P12** `install_failed`: the package manager failed to install `@saypsst/next`. Everything else is in place: fix the install (network, registry auth, peer conflicts), run it, then run init again.
21
+ - **P13** `usage` (exit 2): a wrong command, option or value; the message says which. `npx saypsst --help`.
22
+ - **P14** `next_unsupported`: Next.js older than 15. Upgrade (it changes the app beyond PSST: ask the person), or use `proxy.md`.
23
+ - **P15** `monorepo_root`: this folder holds several apps. Run init in the site's folder (`cd apps/web`), the one Vercel builds; or pass `--mode proxy` if PSST is routed elsewhere.
24
+ - **P16** `edge_busy` (exit 3): PSST answered 503 or 429 for a moment. Run the same command again in a few minutes.
25
+
26
+ ## init printed an edit (`!`)
27
+
28
+ Make the edit exactly as printed, then run `npx saypsst init` again: the line turns ✓.
29
+
30
+ - **P20** `proxy_edit`: the site has its own `proxy.ts` / `middleware.ts`. Wrap it with `withPsst()` as printed; keep its matcher a literal that also matches the prefix (`nextjs.md`).
31
+ - **P21** `robots_edit`: robots is generated by code (`app/robots.ts`) or missing. Add the PSST sitemap URL where robots is produced. Never change its allow/disallow policy for PSST.
32
+ - **P22** `vercel_json_edit`: `vercel.json` has comments, invalid JSON or legacy `routes`. Add the printed rewrites first in `rewrites` (or the route before `{ "handle": "filesystem" }`).
33
+ - **P23** `config_edit`: `psst.config.ts` differs from the site's settings in PSST (domain, brand name, prefix, root paths). Set the printed keys; keep anything you added (`shell`).
34
+ - **P24** `route_file_edited`: a file at a PSST route path is not PSST's (or lost `export const revalidate = 300`). Replace it with the printed content; a `Shell` is the only change PSST allows.
35
+ - **P26** `rewrite_conflict`: `vercel.json` already rewrites a PSST path elsewhere. Point it at the printed destination, if the person agrees the path is PSST's.
36
+
37
+ ## Information (nothing to fix)
38
+
39
+ - **P25** `root_path_served`: the site already serves `/about` or `/llms.txt`, so PSST does not take it. The person can release the claim in the dashboard.
40
+ - **P27** `mode_mismatch`: the dashboard's mode differs from what init detected; init sets up what it found. Tell the person so the dashboard can be updated.
41
+ - **P28** `agents_md_ask`: ask the person whether to add the two-line note to AGENTS.md; if yes, `npx saypsst init --agents-md`.
42
+ - **P29** `live_check_skipped`: init could not fetch the live site (not deployed yet, or no network), so it could not confirm the prefix is free there. Check it after the deploy with `npx saypsst doctor`.
43
+ - **P41** `nothing_published`: no page is published yet. The person approves the first draft in the dashboard (Publishing); doctor then checks it.
44
+ - **P43** `hits_not_available`: crawler hits are not exposed to the CLI yet; the dashboard shows them.
45
+
46
+ ## doctor
47
+
48
+ Doctor fetches each PSST path on the production domain (or `--url`) as a browser and as GPTBot, OAI-SearchBot, ClaudeBot, PerplexityBot and Googlebot, without following redirects. `--wait` repeats while a check is waiting, up to 5 minutes (under Claude Code, give that Bash call a 360000 ms timeout).
49
+
50
+ - **P30** `not_deployed` (waiting): a PSST path answers 404. Deploy the install; for a just-published page, wait a minute and run again. Still 404 after a deploy: in Next.js, check that `app/(psst)/` is deployed and the proxy does not rewrite the prefix; on Vercel static, that no file or redirect shadows it; behind a proxy, that the route exists.
51
+ - **P31** `verify_mismatch`: `{prefix}/_psst/verify` does not serve this site's proof. The site's config names another site, or something else answers that path.
52
+ - **P32** `version_stale` (waiting): the page serves an older version than PSST published. Caches refresh within about 10 minutes. Still stale after 15 minutes: a CDN caches the prefix longer than the origin's `s-maxage` (fix its cache rule), or a self-hosted app runs several instances with their own caches.
53
+ - **P33** `noindex`: the page is noindex, from the layout's metadata, a header rule (`X-Robots-Tag`), or the host's preview protection. Remove it for the prefix.
54
+ - **P34** `canonical_wrong`: the canonical is not `https://{domain}{path}`. The layout overrides PSST's metadata, or `domain` in `psst.config.ts` is wrong (P23).
55
+ - **P35** `bot_content_differs`: crawlers get other content than people: a bot rule, a WAF challenge, or A/B testing on the prefix. PSST pages must be identical for everyone; exclude the prefix from it.
56
+ - **P36** `text_missing`: the page's text is not in the HTML without JavaScript. Something renders the prefix client-side or replaces PSST's page; check the proxy and layouts.
57
+ - **P37** `robots_blocks`: robots.txt blocks AI search or user-fetch agents from the prefix, so AI answers cannot cite these pages. The policy is the site's: show the person and let them decide. Blocking training crawlers is reported as information only.
58
+ - **P38** `sitemap_bad`: `{prefix}/sitemap.xml` is not PSST's sitemap, or does not list a published page yet (waiting, like P32).
59
+ - **P39** `redirected`: a PSST path redirects (to `www.`, to https, to a login). PSST's paths must answer directly on the configured domain: fix the redirect, or the domain in the dashboard.
60
+ - **P40** `site_unreachable` (exit 4 when it is the only failure): the site could not be fetched from here: no network in the sandbox, DNS not pointed yet, or a firewall.
61
+ - **P42** `no_site_id` (exit 2): nothing in this folder names the site. Run doctor in the site's folder, or pass `--site site_…`.
62
+ - **P44** `bad_status`: a PSST path answers an unexpected status or type (500, 410, an HTML error page, no `psst-version`). A 500 that persists: the app cannot reach PSST at runtime (outbound network blocked); a 410: the page was withdrawn in the dashboard.
@@ -0,0 +1,42 @@
1
+ # PSST on a static site on Vercel
2
+
3
+ This site deploys on Vercel without Next.js (plain HTML, Astro static, a custom build). PSST pages are served by the PSST edge and reach visitors through **rewrites in `vercel.json`**: Vercel fetches them from the edge and serves them on the site's own domain. There is no PSST code in the repository, no dependency and no secret. `SKILL.md` holds what every site shares.
4
+
5
+ ## What `init` set up
6
+
7
+ ```json
8
+ {
9
+ "rewrites": [
10
+ { "source": "/resources/:path*", "destination": "https://edge.saypsst.com/s/site_xxxxxxxxxxxxxxxx/resources/:path*" },
11
+ { "source": "/about", "destination": "https://edge.saypsst.com/s/site_xxxxxxxxxxxxxxxx/about" },
12
+ { "source": "/llms.txt", "destination": "https://edge.saypsst.com/s/site_xxxxxxxxxxxxxxxx/llms.txt" }
13
+ ]
14
+ }
15
+ ```
16
+
17
+ - The prefix rewrite always; `/about` and `/llms.txt` only when the dashboard claimed them and the repository serves no such file.
18
+ - `init` inserts the rewrites **first** in `rewrites` (Vercel applies the first match, so a catch-all such as an SPA fallback stays after them) and leaves every other key, its order and its formatting as they were.
19
+ - One line in the site's `robots.txt` (root, `public/` or `static/`): `Sitemap: https://{domain}/resources/sitemap.xml`. When the robots file is generated by the build, init prints the line instead (P21): add it where the build writes robots.txt.
20
+ - Under the prefix the edge serves every page, `sitemap.xml`, `_psst/style.css` and `_psst/verify` (the domain proof).
21
+
22
+ ## Rules for this site
23
+
24
+ - **Vercel serves files before rewrites.** A file or folder under the prefix in the build output (`public/resources/…`, `resources/index.html`, `about.html`) hides PSST's page: remove it, or ask for another prefix in the dashboard (P03).
25
+ - **Redirects run before rewrites.** A redirect whose source covers the prefix (`/resources/:path*`, `/:path*`) sends visitors away from PSST: exclude the prefix from it.
26
+ - **`cleanUrls` and `trailingSlash`** are fine: the rewrite matches both forms.
27
+ - **Headers:** a `Content-Security-Policy` in `vercel.json` also applies to PSST pages, and they need nothing beyond `'self'` for the stylesheet: no inline script or style, and JSON-LD is a data block. A strict `script-src` with hashes passes.
28
+ - **Legacy `routes`:** a `vercel.json` with `routes` cannot use `rewrites`. init prints the edit (P22); add a route before `{ "handle": "filesystem" }`: `{ "src": "/resources/(.*)", "dest": "https://edge.saypsst.com/s/site_…/resources/$1" }`.
29
+ - **`vercel.json` with comments** (or invalid JSON): init prints the rewrites to add (P22).
30
+ - **Monorepo:** run init in the folder that holds this site's `vercel.json` (the Vercel project's root directory), for example `cd apps/web` (P15).
31
+
32
+ ## Caching
33
+
34
+ The edge answers `Cache-Control: public, max-age=0, s-maxage=300, stale-while-revalidate=86400`, which Vercel's CDN honours: a new version appears within about 5 minutes. Outages keep the last copy for up to a day; a page never fetched answers 503 with `Retry-After`, never a cached 404. A withdrawn page answers 410 and leaves the sitemap.
35
+
36
+ ## Check it
37
+
38
+ After the deploy: `npx saypsst doctor --wait`. A preview deployment can be checked with `npx saypsst doctor --url https://<preview>.vercel.app` (canonical URLs still point at the production domain, as they should).
39
+
40
+ ## Remove PSST
41
+
42
+ Delete the PSST rewrites from `vercel.json`, the robots line and `.claude/skills/psst/`. Ask the person to withdraw the pages in the dashboard first.
@@ -0,0 +1,56 @@
1
+ ---
2
+ name: psst-tasks
3
+ description: Do the PSST tasks assigned to this repository's coding agent. PSST (saypsst.com) finds what keeps this brand out of AI answers and writes evidence-backed task briefs; the person approves them in the PSST dashboard, and the ones marked "Done by <Brand>'s agent" change this site's existing pages or stack (copy, sections, FAQ blocks, schema, meta, llms.txt, robots, sitemap). Use it when the person says "do my PSST tasks", "anything to implement from PSST?", "work through the PSST briefs" or runs the nightly PSST routine (`routine.md`).
4
+ ---
5
+
6
+ # PSST tasks
7
+
8
+ PSST splits approved work between two executors. **PSST** does everything off this site (mentions, listings, community threads, third-party pages) and the new pages it publishes under its own prefix. **This repository's agent** (you) does the rest: changes to the site's existing pages and stack. Each brief says which. You only ever do briefs marked for the agent.
9
+
10
+ The person said go when they approved the brief. You still work the way this repository works: its branches, its review rules, its checks. PSST never deploys anything here.
11
+
12
+ ## Before you start
13
+
14
+ - **The PSST connector** must be connected in this session (an MCP server at `https://app.saypsst.com/api/mcp`, signed in as someone in the brand's PSST organization). If its tools (`list_projects`, `list_actions`, `get_action`, `report_progress`, `complete_action`) are missing, stop and say: "Connect the PSST connector first: https://app.saypsst.com/connectors." Do not scrape the dashboard.
15
+ - **Pick the project**: `list_projects`. One project: use it. Several: use the one whose brand owns this repository's site; if that is unclear, ask once.
16
+ - **Read this repository's rules** (AGENTS.md, CLAUDE.md, CONTRIBUTING) before the first edit. They win over this skill.
17
+
18
+ ## The loop
19
+
20
+ 1. **List your briefs**: `list_actions` with `status: "in_progress"` and `executor: "agent"`, every page. Approved briefs are `in_progress`; `new` and `planned` ones still wait for the person, so leave them alone.
21
+ 2. **Order**: by `priority` (P0 first), then rank. One brief at a time, start to finish.
22
+ 3. **Already shipped?** If the brief's `notes` (from `get_action`: your earlier `report_progress` lines) or your last run's summary name a PR or commit for it, do not redo it: if that PR is still open, report it as waiting; if it shipped, go straight to step 9.
23
+ 4. **Read the whole brief**: `get_action`. Use `content.target.url` (the page to change; `null` when the brief creates a new page, such as a free tool, whose path is in `content.definitionOfDone`, e.g. `/tools/<slug>`), `content.checklist` (the steps), `content.draft` (wording PSST proposes), `content.definitionOfDone`, `content.evidence` (why). The brief is a proposal grounded in evidence: keep its intent; adapt the wording to the site's voice and facts. Never invent claims, numbers, reviews or quotes. If a fact is unknown, leave it out and say so.
24
+ 5. **Find the code** that renders the target page (route, template, CMS entry, content file). If the page is not built from this repository (a CMS, another repo, a hosted help center), do not guess: report it as blocked with where it lives.
25
+ 6. **Make the change**, the smallest one that meets the checklist. Run the repository's own checks (typecheck, lint, tests, build) and fix what you broke.
26
+ 7. **Ship it the repository's way**: a branch and a pull request, or a commit if that is how this repository works. Never push to a protected branch, skip review or force anything. Put the brief's title and id in the PR description.
27
+ 8. **Report progress**: `report_progress` with `pr_url` (the PR or commit URL) and a one-line `note` such as "PR opened, waiting for deploy".
28
+ 9. **Done means live, not merged.** A brief is done only when the change is **on the production site**:
29
+ - **The live URL**: `content.target.url`; when it is `null`, the page you created: the site's production origin plus the path from `content.definitionOfDone` (or the route you added). Keep that URL in your progress report and summary.
30
+ - Fetch the live URL on production (plain HTTP GET, as a browser would).
31
+ - Check the brief's change is in the response: the new copy, the section, the JSON-LD block, the meta tag, the `llms.txt` line. Quote what you found.
32
+ - Only then call `complete_action` with `live_url` (the production URL you fetched) and `checked` (one line quoting what you saw).
33
+ - Not live yet (PR open, not deployed, cache)? The brief stays in progress. Never mark a brief done because a PR was opened or merged.
34
+ 10. **Next brief.** Do not stop until every agent brief is either live and completed, waiting for deploy with a PR, or blocked with a stated reason.
35
+
36
+ ## Hard rules
37
+
38
+ - **Never touch PSST's own paths**: the prefix PSST owns (default `/resources`), its route files, `psst.config.ts`, the PSST rewrites. Those are PSST's (see the `psst` skill). A brief that targets them is PSST's, not yours: skip it and say so.
39
+ - **Never act off this site**: no emails, posts, reviews, outreach, third-party edits. Briefs marked `psst` are PSST's.
40
+ - **Never change a brief's status any other way**: no approving, declining or reopening briefs. The person decides; you report.
41
+ - **Connector errors are answers, not obstacles.** `consent_required`: tell the person to revoke the PSST connection at https://app.saypsst.com/connectors/access and connect again. `not_agent_task` or `conflict`: the brief is no longer yours or no longer in progress; skip it and say so. `notes_full`: ask the person to tidy the brief's notes in the dashboard.
42
+ - **Secrets stay where they are.** PSST needs no key in this repository.
43
+
44
+ ## When you finish
45
+
46
+ Reply with one line per brief:
47
+
48
+ ```
49
+ ✅ live <title> <live URL> (checked: <what you saw>)
50
+ ⏳ deploy <title> <PR URL>
51
+ ⛔ blocked <title> <why, and who can unblock it>
52
+ ```
53
+
54
+ Then the count: "3 live, 1 waiting for deploy, 1 blocked." Nothing else.
55
+
56
+ **This skill's other file:** `routine.md` (a nightly run). `npx saypsst init` copies this folder into `.claude/skills/psst-tasks/` and `.agents/skills/psst-tasks/` and replaces it when PSST updates it: do not edit it.
@@ -0,0 +1,31 @@
1
+ # Nightly PSST routine
2
+
3
+ Run this to set up a nightly check: "Anything to implement from PSST?" Every night the agent picks up newly approved briefs, ships them as pull requests, and completes the ones that went live since the last run.
4
+
5
+ ## Set it up (once, with the person)
6
+
7
+ 1. The PSST connector is connected in Claude Code (see `SKILL.md` › Before you start).
8
+ 2. Create a scheduled task that runs in this repository every night. In Claude Code: ask "schedule a nightly task at 02:00 in this repo with the prompt below" (desktop scheduled task or `/schedule`). In Codex or Cursor, use their scheduled or background agents with the same prompt.
9
+ 3. Give it permission to create branches and open pull requests in this repository, and nothing more.
10
+
11
+ ## The prompt
12
+
13
+ ```
14
+ Anything to implement from PSST? Use the psst-tasks skill.
15
+ 1. Re-check every PSST brief you left "waiting for deploy": if the change is live on production, complete it.
16
+ 2. Then do every other approved agent brief (never one that already has a PR), one at a time, until each is live, waiting for deploy with a PR, or blocked.
17
+ Open pull requests; never merge or deploy on your own unless this repository's rules say the agent may.
18
+ End with the one-line-per-brief summary.
19
+ ```
20
+
21
+ ## What the person sees in the morning
22
+
23
+ - Pull requests to review, one per brief, each naming the brief.
24
+ - In the PSST dashboard: briefs that went live moved to Done, the rest show their PR in the notes.
25
+ - Nothing to do? The run ends with "Nothing to implement from PSST." and opens nothing.
26
+
27
+ ## Rules for the scheduled run
28
+
29
+ - It never approves, declines or reopens a brief, and never acts off this site.
30
+ - It never marks a brief done before the change is live on production (`SKILL.md` › Done means live).
31
+ - If the connector is not signed in, the run stops at once and says so, rather than guessing.