@stack-mode/site-manager-kit 1.44.1 → 1.46.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 +13 -1
- package/audit/wiring-audit.d.mts +2 -1
- package/audit/wiring-audit.mjs +10 -8
- package/package.json +2 -1
- package/src/llms-route.ts +45 -0
- package/src/robots.ts +5 -1
- package/src/types.ts +2 -1
- package/src/version.ts +1 -1
package/README.md
CHANGED
|
@@ -194,6 +194,18 @@ export default function Robots() {
|
|
|
194
194
|
}
|
|
195
195
|
```
|
|
196
196
|
|
|
197
|
+
robots.txt also keeps out the AI crawlers the site blocks in Site Manager (Search Settings > Indexing): AI search tools,
|
|
198
|
+
AI training crawlers, or both.
|
|
199
|
+
|
|
200
|
+
`app/llms.txt/route.ts` (optional), the site's pages with their titles and descriptions for AI tools that read
|
|
201
|
+
`/llms.txt`. It answers 404 while llms.txt is off for the site (Search Settings > Tools):
|
|
202
|
+
|
|
203
|
+
```ts
|
|
204
|
+
import { llmsRoute } from "site-manager-kit/llms-route";
|
|
205
|
+
|
|
206
|
+
export const GET = llmsRoute();
|
|
207
|
+
```
|
|
208
|
+
|
|
197
209
|
`src/proxy.ts` (or `proxy.ts` at the root when there's no `src` folder):
|
|
198
210
|
|
|
199
211
|
```ts
|
|
@@ -497,7 +509,7 @@ It reads `BLUEPRINT_URL` and `BLUEPRINT_API_KEY` from `.env.local` (or `--env <f
|
|
|
497
509
|
- **Wired**: the site reads it from Site Manager.
|
|
498
510
|
- **Not wired**: Site Manager has it, but editing it changes nothing on the website (a key the site never asks for, a section that's on but never read, a Business Info field the code ignores). Fails the audit.
|
|
499
511
|
- **Missing in Site Manager**: the site uses it, but Site Manager has no such field (a key not synced yet, a form key with no form). Fails the audit.
|
|
500
|
-
- **
|
|
512
|
+
- **Built in** (status `hard_coded`): the code keeps its own copy of a Site Manager value, with file and line. Items the site also reads from Site Manager carry `fallback: true` and show as **Has a fallback**. Reported, never failing, since a fallback can be on purpose.
|
|
501
513
|
- **Unknown**: the audit couldn't tell (the copy keys file didn't load, a section it doesn't know yet).
|
|
502
514
|
|
|
503
515
|
Exit codes: 0 wired, 1 something Not wired or missing, 2 it couldn't run. Folders it shouldn't scan (an old admin, scripts) go in `package.json`:
|
package/audit/wiring-audit.d.mts
CHANGED
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
export type AuditStatus = "wired" | "not_wired" | "missing" | "hard_coded" | "unknown" | "info";
|
|
4
4
|
export type Where = { file: string; line: number };
|
|
5
|
-
|
|
5
|
+
/** `fallback`: a Built in (hard_coded) item the site reads from Site Manager, keeping its own copy only as a backup. */
|
|
6
|
+
export type AuditItem = { area: string; status: AuditStatus; name: string; detail: string; fix: string | null; where: Where[]; more: number; fallback?: boolean };
|
|
6
7
|
export type SiteFile = { rel: string; text: string; lines: string[]; usesKit: boolean };
|
|
7
8
|
export type SiteFiles = { files: SiteFile[]; publicFiles: string[] };
|
|
8
9
|
export type ApiAnswer = { status: number; data: any; meta: any; code: string | null };
|
package/audit/wiring-audit.mjs
CHANGED
|
@@ -14,7 +14,7 @@ export const STATUS = {
|
|
|
14
14
|
wired: "Wired",
|
|
15
15
|
not_wired: "Not wired",
|
|
16
16
|
missing: "Missing in Site Manager",
|
|
17
|
-
hard_coded: "
|
|
17
|
+
hard_coded: "Built in",
|
|
18
18
|
unknown: "Unknown",
|
|
19
19
|
info: "Note",
|
|
20
20
|
};
|
|
@@ -279,7 +279,9 @@ export async function uploadResult(base, key, result, kitVersion, fetchImpl = fe
|
|
|
279
279
|
*/
|
|
280
280
|
export async function audit({ site: siteFiles, api, declaredResult, keysFile, hasCopyKeysRoute }) {
|
|
281
281
|
const items = [];
|
|
282
|
-
|
|
282
|
+
// `extra.fallback`: a Built in item the site does read from Site Manager, keeping its own copy only as a backup.
|
|
283
|
+
const add = (area, status, name, detail, fix = null, where = [], extra = {}) =>
|
|
284
|
+
items.push({ area, status, name, detail, fix, where: where.slice(0, MAX_WHERE), more: Math.max(0, where.length - MAX_WHERE), ...extra });
|
|
283
285
|
const { files, publicFiles } = siteFiles;
|
|
284
286
|
|
|
285
287
|
const siteRes = await api("/site");
|
|
@@ -347,7 +349,7 @@ export async function audit({ site: siteFiles, api, declaredResult, keysFile, ha
|
|
|
347
349
|
const read = readsField(fact.read);
|
|
348
350
|
const where = fact.pattern ? findAll(files, fact.pattern) : [];
|
|
349
351
|
if (!read) add("Business Info", "not_wired", fact.name, `Set in Site Manager but the site never reads it${where.length ? "; the site shows its own copy instead" : ""}.`, "Read it from getSite().business (or the site's settings helper) and delete the built-in copy.", where);
|
|
350
|
-
else if (where.length) add("Business Info", "hard_coded", fact.name, "Read from Site Manager, but the code also keeps its own copy (a fallback or a second place that shows it). If the value is cleared or changed in Site Manager, these places can show the old one.", "Show the Site Manager value everywhere and drop the built-in copy, or keep it only as a fallback that's never shown when Site Manager has a value.", where);
|
|
352
|
+
else if (where.length) add("Business Info", "hard_coded", fact.name, "Read from Site Manager, but the code also keeps its own copy (a fallback or a second place that shows it). If the value is cleared or changed in Site Manager, these places can show the old one.", "Show the Site Manager value everywhere and drop the built-in copy, or keep it only as a fallback that's never shown when Site Manager has a value.", where, { fallback: true });
|
|
351
353
|
else add("Business Info", "wired", fact.name, "Read from Site Manager.");
|
|
352
354
|
}
|
|
353
355
|
|
|
@@ -419,7 +421,7 @@ export async function audit({ site: siteFiles, api, declaredResult, keysFile, ha
|
|
|
419
421
|
const reads = readsModule(files, { names: l.reader.filter((n) => !n.startsWith("/")), paths: l.reader.filter((n) => n.startsWith("/")) });
|
|
420
422
|
for (const name of l.names.filter((n) => n && n.trim().length >= 3)) {
|
|
421
423
|
const where = findAll(files, literalPattern(name));
|
|
422
|
-
if (where.length) add(l.area, "hard_coded", name, `${reads ? "Read from Site Manager, but the" : "The"} code also has its own copy of this name${reads ? "" : " and never reads the list"}, so a rename, a new entry or a new order in Site Manager may not show.`, "Read the list (names and order) from Site Manager and drop the built-in list.", where);
|
|
424
|
+
if (where.length) add(l.area, "hard_coded", name, `${reads ? "Read from Site Manager, but the" : "The"} code also has its own copy of this name${reads ? "" : " and never reads the list"}, so a rename, a new entry or a new order in Site Manager may not show.`, "Read the list (names and order) from Site Manager and drop the built-in list.", where, reads ? { fallback: true } : {});
|
|
423
425
|
else add(l.area, reads ? "wired" : "not_wired", name, reads ? "Read from Site Manager." : "In Site Manager, but the site never reads this list.", reads ? null : "Read the list from Site Manager.");
|
|
424
426
|
}
|
|
425
427
|
}
|
|
@@ -482,9 +484,9 @@ export function renderReport(result, meta) {
|
|
|
482
484
|
out.push(`# Wiring audit: ${result.site.name}`, "");
|
|
483
485
|
out.push(`Run ${meta.when} against ${meta.host} (site ${meta.siteId}), site kit ${meta.kitVersion}. Read only.`, "");
|
|
484
486
|
out.push(failed ? "**Result: not wired.** Fix the Not wired and Missing in Site Manager items below." : "**Result: wired.** Nothing is Not wired or missing in Site Manager.", "");
|
|
485
|
-
out.push("| Wired | Not wired | Missing in Site Manager |
|
|
487
|
+
out.push("| Wired | Not wired | Missing in Site Manager | Built in | Unknown |", "| --- | --- | --- | --- | --- |");
|
|
486
488
|
out.push(`| ${counts.wired} | ${counts.not_wired} | ${counts.missing} | ${counts.hard_coded} | ${counts.unknown} |`, "");
|
|
487
|
-
out.push("Not wired: Site Manager has it, but editing it changes nothing on the website. Missing in Site Manager: the website uses it, but Site Manager has no such field.
|
|
489
|
+
out.push("Not wired: Site Manager has it, but editing it changes nothing on the website. Missing in Site Manager: the website uses it, but Site Manager has no such field. Built in: the code keeps its own copy of a Site Manager value (Has a fallback: it reads Site Manager and keeps the copy as a backup). Unknown: the audit couldn't tell.", "");
|
|
488
490
|
const order = ["not_wired", "missing", "hard_coded", "unknown", "info", "wired"];
|
|
489
491
|
const areas = [...new Set(result.items.map((i) => i.area))];
|
|
490
492
|
for (const area of areas) {
|
|
@@ -499,7 +501,7 @@ export function renderReport(result, meta) {
|
|
|
499
501
|
rows.sort((a, b) => order.indexOf(a.status) - order.indexOf(b.status));
|
|
500
502
|
for (const i of rows) {
|
|
501
503
|
const where = i.where.map((w) => `${w.file}:${w.line}`).join("<br>") + (i.more ? `<br>and ${i.more} more` : "");
|
|
502
|
-
out.push(`| ${STATUS[i.status]} | ${cell(i.name)} | ${cell(i.detail)} | ${cell(i.fix ?? "")} | ${where} |`);
|
|
504
|
+
out.push(`| ${i.fallback ? "Has a fallback" : STATUS[i.status]} | ${cell(i.name)} | ${cell(i.detail)} | ${cell(i.fix ?? "")} | ${where} |`);
|
|
503
505
|
}
|
|
504
506
|
if (wiredHidden) out.push("", `Plus ${wiredHidden} wired (run with --all to list them).`);
|
|
505
507
|
out.push("");
|
|
@@ -592,7 +594,7 @@ export async function main(argv) {
|
|
|
592
594
|
writeFileSync(out, renderReport(result, meta));
|
|
593
595
|
writeFileSync(out.replace(/\.md$/, "") + ".json", JSON.stringify({ ...meta, ...result }, null, 2));
|
|
594
596
|
const { counts, failed } = summarize(result.items);
|
|
595
|
-
console.log(`Wiring audit for ${result.site.name}: ${counts.wired} wired, ${counts.not_wired} not wired, ${counts.missing} missing in Site Manager, ${counts.hard_coded}
|
|
597
|
+
console.log(`Wiring audit for ${result.site.name}: ${counts.wired} wired, ${counts.not_wired} not wired, ${counts.missing} missing in Site Manager, ${counts.hard_coded} built in, ${counts.unknown} unknown.`);
|
|
596
598
|
console.log(`Report: ${path.relative(process.cwd(), out) || out}`);
|
|
597
599
|
if (args.upload) {
|
|
598
600
|
const up = await uploadResult(env.BLUEPRINT_URL, env.BLUEPRINT_AUDIT_KEY, result, kitVersion);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stack-mode/site-manager-kit",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.46.0",
|
|
4
4
|
"description": "Reads a site's content from Site Manager and builds its metadata, sitemap, robots, redirects, forms and refreshes. For Next.js App Router sites on Vercel.",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"repository": {
|
|
@@ -28,6 +28,7 @@
|
|
|
28
28
|
"./attribution": "./src/attribution.tsx",
|
|
29
29
|
"./phone": "./src/phone-input.tsx",
|
|
30
30
|
"./rss-route": "./src/rss-route.ts",
|
|
31
|
+
"./llms-route": "./src/llms-route.ts",
|
|
31
32
|
"./visual": "./src/visual.ts",
|
|
32
33
|
"./visual-tags": "./src/visual-tags.ts",
|
|
33
34
|
"./visual-accents": "./src/visual-accents.ts",
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { BlueprintError, bp, tagsFor } from "./client";
|
|
2
|
+
|
|
3
|
+
export type LlmsData = {
|
|
4
|
+
name: string;
|
|
5
|
+
summary: string | null;
|
|
6
|
+
sections: { title: string; pages: { url: string; title: string; description: string | null }[] }[];
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
const line = (s: string) => s.replace(/\s+/g, " ").trim();
|
|
10
|
+
const linkText = (s: string) => line(s).replace(/[[\]]/g, "");
|
|
11
|
+
|
|
12
|
+
/** Pure: the llms.txt text (llmstxt.org): the business, its summary, then each section's pages as links. */
|
|
13
|
+
export function buildLlmsTxt(d: LlmsData): string {
|
|
14
|
+
const out = [`# ${line(d.name)}`, ""];
|
|
15
|
+
if (d.summary) out.push(`> ${line(d.summary)}`, "");
|
|
16
|
+
for (const s of d.sections) {
|
|
17
|
+
if (!s.pages.length) continue;
|
|
18
|
+
out.push(`## ${line(s.title)}`, "");
|
|
19
|
+
for (const p of s.pages) out.push(`- [${linkText(p.title)}](${p.url})${p.description ? `: ${line(p.description)}` : ""}`);
|
|
20
|
+
out.push("");
|
|
21
|
+
}
|
|
22
|
+
return out.join("\n");
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* /llms.txt, from the site's pages in Site Manager. Put in app/llms.txt/route.ts:
|
|
27
|
+
*
|
|
28
|
+
* import { llmsRoute } from "site-manager-kit/llms-route";
|
|
29
|
+
* export const GET = llmsRoute();
|
|
30
|
+
*
|
|
31
|
+
* Answers 404 when llms.txt is off for the site in Site Manager, or the Search section is.
|
|
32
|
+
*/
|
|
33
|
+
export function llmsRoute() {
|
|
34
|
+
return async function GET(): Promise<Response> {
|
|
35
|
+
let data: LlmsData | null;
|
|
36
|
+
try {
|
|
37
|
+
data = await bp<LlmsData>("/llms", { tags: tagsFor("seo") });
|
|
38
|
+
} catch (e) {
|
|
39
|
+
if (e instanceof BlueprintError && (e.code === "module_off" || e.code === "feature_off")) return new Response("Not found", { status: 404 });
|
|
40
|
+
throw e;
|
|
41
|
+
}
|
|
42
|
+
if (!data) return new Response("Not found", { status: 404 });
|
|
43
|
+
return new Response(buildLlmsTxt(data), { headers: { "content-type": "text/plain; charset=utf-8", "cache-control": "public, max-age=0, s-maxage=3600, stale-while-revalidate=86400" } });
|
|
44
|
+
};
|
|
45
|
+
}
|
package/src/robots.ts
CHANGED
|
@@ -10,7 +10,11 @@ import type { Robots } from "./types";
|
|
|
10
10
|
export function buildRobots(r: Robots | null, websiteUrl: string | null): MetadataRoute.Robots {
|
|
11
11
|
if (!r || !r.indexable) return { rules: [{ userAgent: "*", disallow: "/" }] };
|
|
12
12
|
return {
|
|
13
|
-
rules: [
|
|
13
|
+
rules: [
|
|
14
|
+
{ userAgent: "*", allow: "/", ...(r.disallow.length ? { disallow: r.disallow } : {}) },
|
|
15
|
+
// A crawler named in its own group ignores the "*" group, so the AI crawlers the site keeps out are listed apart.
|
|
16
|
+
...(r.blockedBots?.length ? [{ userAgent: r.blockedBots, disallow: "/" }] : []),
|
|
17
|
+
],
|
|
14
18
|
...(websiteUrl ? { sitemap: `${websiteUrl.replace(/\/+$/, "")}/sitemap.xml` } : {}),
|
|
15
19
|
};
|
|
16
20
|
}
|
package/src/types.ts
CHANGED
|
@@ -312,7 +312,8 @@ export type PageMeta = {
|
|
|
312
312
|
maxImagePreview?: "large" | "standard" | "none";
|
|
313
313
|
};
|
|
314
314
|
export type SitemapEntry = { url: string; lastModified: string };
|
|
315
|
-
|
|
315
|
+
/** blockedBots: AI crawlers the site keeps out, by user agent (Site Manager 2026-11, optional for older servers). */
|
|
316
|
+
export type Robots = { indexable: boolean; disallow: string[]; blockedBots?: string[] };
|
|
316
317
|
export type Redirect = { from: string; to: string; status: 301 | 302 | 308 };
|
|
317
318
|
|
|
318
319
|
// Real estate (Site Manager's Listings and Team sections).
|
package/src/version.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const KIT_VERSION = "1.
|
|
1
|
+
export const KIT_VERSION = "1.46.0";
|