@stack-mode/site-manager-kit 1.44.0 → 1.44.1

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.
Files changed (59) hide show
  1. package/README.md +606 -606
  2. package/audit/ts-hooks.mjs +68 -68
  3. package/audit/wiring-audit.d.mts +37 -37
  4. package/audit/wiring-audit.mjs +604 -604
  5. package/bin/site-manager-kit.mjs +10 -10
  6. package/package.json +72 -72
  7. package/src/announcements.ts +13 -13
  8. package/src/attribution-fields.ts +52 -52
  9. package/src/attribution.tsx +47 -47
  10. package/src/blog.ts +122 -122
  11. package/src/chat-route.ts +136 -136
  12. package/src/chat-types.ts +76 -76
  13. package/src/client.ts +116 -116
  14. package/src/config.ts +78 -78
  15. package/src/copy-keys-route.ts +21 -21
  16. package/src/copy-keys.ts +77 -77
  17. package/src/data.ts +189 -189
  18. package/src/downloads.ts +7 -7
  19. package/src/faq.ts +21 -21
  20. package/src/floor-plans.ts +45 -45
  21. package/src/form-guard.tsx +37 -37
  22. package/src/form-types.ts +13 -13
  23. package/src/forms.ts +76 -76
  24. package/src/glossary.ts +19 -19
  25. package/src/health-route.ts +34 -34
  26. package/src/index.ts +36 -36
  27. package/src/indexnow.ts +40 -40
  28. package/src/listings.ts +38 -38
  29. package/src/metadata.ts +55 -55
  30. package/src/not-found-reporter.tsx +16 -16
  31. package/src/not-found.ts +53 -53
  32. package/src/payment.ts +70 -70
  33. package/src/phone-input.tsx +26 -26
  34. package/src/phone.ts +37 -37
  35. package/src/preview-route.ts +48 -48
  36. package/src/preview.ts +94 -94
  37. package/src/residents.ts +23 -23
  38. package/src/revalidate-route.ts +34 -34
  39. package/src/rss-route.ts +26 -26
  40. package/src/services.ts +52 -52
  41. package/src/site-chat-panel.tsx +537 -537
  42. package/src/site-chat.tsx +26 -26
  43. package/src/sitemap.ts +26 -26
  44. package/src/structured-data.tsx +207 -207
  45. package/src/types.ts +580 -580
  46. package/src/version.ts +1 -1
  47. package/src/visual-accents.ts +126 -126
  48. package/src/visual-bridge-client.tsx +1070 -1070
  49. package/src/visual-bridge.tsx +30 -30
  50. package/src/visual-clicks.ts +13 -13
  51. package/src/visual-lists.ts +37 -37
  52. package/src/visual-match.ts +77 -77
  53. package/src/visual-messages.ts +160 -160
  54. package/src/visual-mode.ts +18 -18
  55. package/src/visual-preview.ts +69 -69
  56. package/src/visual-tags.ts +90 -90
  57. package/src/visual.ts +59 -59
  58. package/src/waitlist-form.ts +39 -39
  59. package/src/waitlist.ts +48 -48
@@ -1,10 +1,10 @@
1
- #!/usr/bin/env node
2
- // site-manager-kit's command line. One command so far: `audit` (the wiring audit, audit/wiring-audit.mjs).
3
- const [command, ...rest] = process.argv.slice(2);
4
- if (command === "audit") {
5
- const { main } = await import("../audit/wiring-audit.mjs");
6
- process.exitCode = await main(rest);
7
- } else {
8
- console.log("Usage: site-manager-kit audit [--help]");
9
- process.exitCode = command ? 2 : 0;
10
- }
1
+ #!/usr/bin/env node
2
+ // site-manager-kit's command line. One command so far: `audit` (the wiring audit, audit/wiring-audit.mjs).
3
+ const [command, ...rest] = process.argv.slice(2);
4
+ if (command === "audit") {
5
+ const { main } = await import("../audit/wiring-audit.mjs");
6
+ process.exitCode = await main(rest);
7
+ } else {
8
+ console.log("Usage: site-manager-kit audit [--help]");
9
+ process.exitCode = command ? 2 : 0;
10
+ }
package/package.json CHANGED
@@ -1,72 +1,72 @@
1
- {
2
- "name": "@stack-mode/site-manager-kit",
3
- "version": "1.44.0",
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
- "license": "UNLICENSED",
6
- "repository": {
7
- "type": "git",
8
- "url": "git+https://github.com/stack-mode/site-manager-kit.git"
9
- },
10
- "type": "module",
11
- "main": "./src/index.ts",
12
- "types": "./src/index.ts",
13
- "exports": {
14
- ".": "./src/index.ts",
15
- "./revalidate-route": "./src/revalidate-route.ts",
16
- "./health-route": "./src/health-route.ts",
17
- "./forms": "./src/forms.ts",
18
- "./waitlist": "./src/waitlist.ts",
19
- "./form-guard": "./src/form-guard.tsx",
20
- "./redirects": "./src/redirects.ts",
21
- "./not-found": "./src/not-found.ts",
22
- "./not-found-reporter": "./src/not-found-reporter.tsx",
23
- "./indexnow": "./src/indexnow.ts",
24
- "./config": "./src/config.ts",
25
- "./analytics": "./src/analytics.tsx",
26
- "./preview-route": "./src/preview-route.ts",
27
- "./preview-banner": "./src/preview-banner.tsx",
28
- "./attribution": "./src/attribution.tsx",
29
- "./phone": "./src/phone-input.tsx",
30
- "./rss-route": "./src/rss-route.ts",
31
- "./visual": "./src/visual.ts",
32
- "./visual-tags": "./src/visual-tags.ts",
33
- "./visual-accents": "./src/visual-accents.ts",
34
- "./visual-editing": "./src/visual-bridge.tsx",
35
- "./copy-keys": "./src/copy-keys.ts",
36
- "./copy-keys-route": "./src/copy-keys-route.ts",
37
- "./chat": "./src/site-chat.tsx",
38
- "./chat-route": "./src/chat-route.ts"
39
- },
40
- "bin": {
41
- "site-manager-kit": "./bin/site-manager-kit.mjs"
42
- },
43
- "files": [
44
- "src",
45
- "bin",
46
- "audit",
47
- "README.md"
48
- ],
49
- "scripts": {
50
- "typecheck": "tsc --noEmit",
51
- "test": "vitest run"
52
- },
53
- "peerDependencies": {
54
- "botid": ">=1",
55
- "next": ">=16",
56
- "react": ">=19"
57
- },
58
- "devDependencies": {
59
- "@types/node": "^22.20.5",
60
- "@types/react": "^19.3.0",
61
- "botid": "^1.5.12",
62
- "next": "^16.4.3",
63
- "react": "^19.3.0",
64
- "react-dom": "^19.3.0",
65
- "typescript": "^5.9.3",
66
- "vite": "^8.3.5",
67
- "vitest": "^5.0.5"
68
- },
69
- "publishConfig": {
70
- "access": "public"
71
- }
72
- }
1
+ {
2
+ "name": "@stack-mode/site-manager-kit",
3
+ "version": "1.44.1",
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
+ "license": "UNLICENSED",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/stack-mode/site-manager-kit.git"
9
+ },
10
+ "type": "module",
11
+ "main": "./src/index.ts",
12
+ "types": "./src/index.ts",
13
+ "exports": {
14
+ ".": "./src/index.ts",
15
+ "./revalidate-route": "./src/revalidate-route.ts",
16
+ "./health-route": "./src/health-route.ts",
17
+ "./forms": "./src/forms.ts",
18
+ "./waitlist": "./src/waitlist.ts",
19
+ "./form-guard": "./src/form-guard.tsx",
20
+ "./redirects": "./src/redirects.ts",
21
+ "./not-found": "./src/not-found.ts",
22
+ "./not-found-reporter": "./src/not-found-reporter.tsx",
23
+ "./indexnow": "./src/indexnow.ts",
24
+ "./config": "./src/config.ts",
25
+ "./analytics": "./src/analytics.tsx",
26
+ "./preview-route": "./src/preview-route.ts",
27
+ "./preview-banner": "./src/preview-banner.tsx",
28
+ "./attribution": "./src/attribution.tsx",
29
+ "./phone": "./src/phone-input.tsx",
30
+ "./rss-route": "./src/rss-route.ts",
31
+ "./visual": "./src/visual.ts",
32
+ "./visual-tags": "./src/visual-tags.ts",
33
+ "./visual-accents": "./src/visual-accents.ts",
34
+ "./visual-editing": "./src/visual-bridge.tsx",
35
+ "./copy-keys": "./src/copy-keys.ts",
36
+ "./copy-keys-route": "./src/copy-keys-route.ts",
37
+ "./chat": "./src/site-chat.tsx",
38
+ "./chat-route": "./src/chat-route.ts"
39
+ },
40
+ "bin": {
41
+ "site-manager-kit": "./bin/site-manager-kit.mjs"
42
+ },
43
+ "files": [
44
+ "src",
45
+ "bin",
46
+ "audit",
47
+ "README.md"
48
+ ],
49
+ "scripts": {
50
+ "typecheck": "tsc --noEmit",
51
+ "test": "vitest run"
52
+ },
53
+ "peerDependencies": {
54
+ "botid": ">=1",
55
+ "next": ">=16",
56
+ "react": ">=19"
57
+ },
58
+ "devDependencies": {
59
+ "@types/node": "^22.20.5",
60
+ "@types/react": "^19.3.0",
61
+ "botid": "^1.5.12",
62
+ "next": "^16.4.3",
63
+ "react": "^19.3.0",
64
+ "react-dom": "^19.3.0",
65
+ "typescript": "^5.9.3",
66
+ "vite": "^8.3.5",
67
+ "vitest": "^5.0.5"
68
+ },
69
+ "publishConfig": {
70
+ "access": "public"
71
+ }
72
+ }
@@ -1,13 +1,13 @@
1
- import type { Announcement } from "./types";
2
-
3
- /** Notices live at `now`: started (or no start) and not yet ended. Start inclusive, end exclusive, as in Site Manager. */
4
- export function announcementsLiveAt(list: Announcement[], now = new Date()): Announcement[] {
5
- const t = now.getTime();
6
- return list.filter((a) => (!a.startsAt || Date.parse(a.startsAt) <= t) && (!a.endsAt || Date.parse(a.endsAt) > t));
7
- }
8
-
9
- /** True while a cached response is still current: Site Manager's `meta.nextChangeAt` hasn't passed. */
10
- export function cachedUntilPassed(meta: Record<string, unknown>, now = new Date()): boolean {
11
- const next = typeof meta.nextChangeAt === "string" ? Date.parse(meta.nextChangeAt) : NaN;
12
- return Number.isNaN(next) || next > now.getTime();
13
- }
1
+ import type { Announcement } from "./types";
2
+
3
+ /** Notices live at `now`: started (or no start) and not yet ended. Start inclusive, end exclusive, as in Site Manager. */
4
+ export function announcementsLiveAt(list: Announcement[], now = new Date()): Announcement[] {
5
+ const t = now.getTime();
6
+ return list.filter((a) => (!a.startsAt || Date.parse(a.startsAt) <= t) && (!a.endsAt || Date.parse(a.endsAt) > t));
7
+ }
8
+
9
+ /** True while a cached response is still current: Site Manager's `meta.nextChangeAt` hasn't passed. */
10
+ export function cachedUntilPassed(meta: Record<string, unknown>, now = new Date()): boolean {
11
+ const next = typeof meta.nextChangeAt === "string" ? Date.parse(meta.nextChangeAt) : NaN;
12
+ return Number.isNaN(next) || next > now.getTime();
13
+ }
@@ -1,52 +1,52 @@
1
- // Where a visitor came from, as hidden form fields. Shared by the browser side (attribution.tsx) and the form action
2
- // (forms.ts), so both use the same names. No cookies: values live in the tab's session storage.
3
-
4
- export const ATTRIBUTION_STORAGE_KEY = "bp_attribution";
5
-
6
- /** Hidden field name for each value Site Manager accepts as `attribution`. */
7
- export const ATTRIBUTION_FIELDS = {
8
- source: "bp_utm_source",
9
- medium: "bp_utm_medium",
10
- campaign: "bp_utm_campaign",
11
- content: "bp_utm_content",
12
- term: "bp_utm_term",
13
- landingPath: "bp_landing",
14
- referrer: "bp_referrer",
15
- } as const;
16
-
17
- export type Attribution = Partial<Record<keyof typeof ATTRIBUTION_FIELDS, string>>;
18
-
19
- const MAX = 300;
20
-
21
- /**
22
- * The visit's first touch, from the landing page's address and referrer: utm_ values, the path (no query), and the
23
- * referring site's host when it's another site. Pure, for tests and for the browser.
24
- */
25
- export function firstTouch(href: string, referrer: string): Attribution {
26
- const url = new URL(href);
27
- const a: Attribution = {};
28
- for (const k of ["source", "medium", "campaign", "content", "term"] as const) {
29
- const v = url.searchParams.get(`utm_${k}`)?.trim();
30
- if (v) a[k] = v.slice(0, MAX);
31
- }
32
- a.landingPath = url.pathname.slice(0, MAX);
33
- try {
34
- const ref = referrer ? new URL(referrer) : null;
35
- if (ref && ref.host !== url.host && (ref.protocol === "https:" || ref.protocol === "http:")) a.referrer = ref.host;
36
- } catch {
37
- // An unreadable referrer is simply left out.
38
- }
39
- return a;
40
- }
41
-
42
- /** The attribution a submitted form carries, read from its hidden fields. Undefined when there's none. */
43
- export function attributionFrom(formData: FormData): Attribution | undefined {
44
- const a: Attribution = {};
45
- for (const [k, name] of Object.entries(ATTRIBUTION_FIELDS) as [keyof Attribution, string][]) {
46
- const v = formData.get(name);
47
- if (typeof v === "string" && v.trim()) a[k] = v.trim().slice(0, MAX);
48
- }
49
- return Object.keys(a).length ? a : undefined;
50
- }
51
-
52
- export const ATTRIBUTION_FIELD_NAMES: ReadonlySet<string> = new Set(Object.values(ATTRIBUTION_FIELDS));
1
+ // Where a visitor came from, as hidden form fields. Shared by the browser side (attribution.tsx) and the form action
2
+ // (forms.ts), so both use the same names. No cookies: values live in the tab's session storage.
3
+
4
+ export const ATTRIBUTION_STORAGE_KEY = "bp_attribution";
5
+
6
+ /** Hidden field name for each value Site Manager accepts as `attribution`. */
7
+ export const ATTRIBUTION_FIELDS = {
8
+ source: "bp_utm_source",
9
+ medium: "bp_utm_medium",
10
+ campaign: "bp_utm_campaign",
11
+ content: "bp_utm_content",
12
+ term: "bp_utm_term",
13
+ landingPath: "bp_landing",
14
+ referrer: "bp_referrer",
15
+ } as const;
16
+
17
+ export type Attribution = Partial<Record<keyof typeof ATTRIBUTION_FIELDS, string>>;
18
+
19
+ const MAX = 300;
20
+
21
+ /**
22
+ * The visit's first touch, from the landing page's address and referrer: utm_ values, the path (no query), and the
23
+ * referring site's host when it's another site. Pure, for tests and for the browser.
24
+ */
25
+ export function firstTouch(href: string, referrer: string): Attribution {
26
+ const url = new URL(href);
27
+ const a: Attribution = {};
28
+ for (const k of ["source", "medium", "campaign", "content", "term"] as const) {
29
+ const v = url.searchParams.get(`utm_${k}`)?.trim();
30
+ if (v) a[k] = v.slice(0, MAX);
31
+ }
32
+ a.landingPath = url.pathname.slice(0, MAX);
33
+ try {
34
+ const ref = referrer ? new URL(referrer) : null;
35
+ if (ref && ref.host !== url.host && (ref.protocol === "https:" || ref.protocol === "http:")) a.referrer = ref.host;
36
+ } catch {
37
+ // An unreadable referrer is simply left out.
38
+ }
39
+ return a;
40
+ }
41
+
42
+ /** The attribution a submitted form carries, read from its hidden fields. Undefined when there's none. */
43
+ export function attributionFrom(formData: FormData): Attribution | undefined {
44
+ const a: Attribution = {};
45
+ for (const [k, name] of Object.entries(ATTRIBUTION_FIELDS) as [keyof Attribution, string][]) {
46
+ const v = formData.get(name);
47
+ if (typeof v === "string" && v.trim()) a[k] = v.trim().slice(0, MAX);
48
+ }
49
+ return Object.keys(a).length ? a : undefined;
50
+ }
51
+
52
+ export const ATTRIBUTION_FIELD_NAMES: ReadonlySet<string> = new Set(Object.values(ATTRIBUTION_FIELDS));
@@ -1,47 +1,47 @@
1
- "use client";
2
-
3
- import { useEffect, useState } from "react";
4
- import { ATTRIBUTION_FIELDS, ATTRIBUTION_STORAGE_KEY, firstTouch, type Attribution } from "./attribution-fields";
5
-
6
- /** The visit's first touch: stored once per tab, so later pages keep where the visitor first came from. */
7
- function rememberedTouch(): Attribution | null {
8
- try {
9
- const saved = sessionStorage.getItem(ATTRIBUTION_STORAGE_KEY);
10
- if (saved) return JSON.parse(saved) as Attribution;
11
- const a = firstTouch(window.location.href, document.referrer);
12
- sessionStorage.setItem(ATTRIBUTION_STORAGE_KEY, JSON.stringify(a));
13
- return a;
14
- } catch {
15
- // Storage can be off (some private modes). The current page is then the best we know.
16
- try {
17
- return firstTouch(window.location.href, document.referrer);
18
- } catch {
19
- return null;
20
- }
21
- }
22
- }
23
-
24
- /**
25
- * Put once inside <body> in app/layout.tsx. Remembers where the visitor first came from (utm_ values, landing page,
26
- * referring site) for this browser tab, with no cookie, so forms can send it with a submission. Renders nothing.
27
- */
28
- export function SiteAttribution() {
29
- useEffect(() => {
30
- rememberedTouch();
31
- }, []);
32
- return null;
33
- }
34
-
35
- /** Hidden fields with the remembered first touch. FormGuard includes these, so forms send them with no extra code. */
36
- export function AttributionFields() {
37
- const [a, setA] = useState<Attribution | null>(null);
38
- useEffect(() => {
39
- setA(rememberedTouch());
40
- }, []);
41
- if (!a) return null;
42
- return (
43
- <>
44
- {(Object.entries(ATTRIBUTION_FIELDS) as [keyof Attribution, string][]).map(([k, name]) => (a[k] ? <input key={name} type="hidden" name={name} value={a[k]} /> : null))}
45
- </>
46
- );
47
- }
1
+ "use client";
2
+
3
+ import { useEffect, useState } from "react";
4
+ import { ATTRIBUTION_FIELDS, ATTRIBUTION_STORAGE_KEY, firstTouch, type Attribution } from "./attribution-fields";
5
+
6
+ /** The visit's first touch: stored once per tab, so later pages keep where the visitor first came from. */
7
+ function rememberedTouch(): Attribution | null {
8
+ try {
9
+ const saved = sessionStorage.getItem(ATTRIBUTION_STORAGE_KEY);
10
+ if (saved) return JSON.parse(saved) as Attribution;
11
+ const a = firstTouch(window.location.href, document.referrer);
12
+ sessionStorage.setItem(ATTRIBUTION_STORAGE_KEY, JSON.stringify(a));
13
+ return a;
14
+ } catch {
15
+ // Storage can be off (some private modes). The current page is then the best we know.
16
+ try {
17
+ return firstTouch(window.location.href, document.referrer);
18
+ } catch {
19
+ return null;
20
+ }
21
+ }
22
+ }
23
+
24
+ /**
25
+ * Put once inside <body> in app/layout.tsx. Remembers where the visitor first came from (utm_ values, landing page,
26
+ * referring site) for this browser tab, with no cookie, so forms can send it with a submission. Renders nothing.
27
+ */
28
+ export function SiteAttribution() {
29
+ useEffect(() => {
30
+ rememberedTouch();
31
+ }, []);
32
+ return null;
33
+ }
34
+
35
+ /** Hidden fields with the remembered first touch. FormGuard includes these, so forms send them with no extra code. */
36
+ export function AttributionFields() {
37
+ const [a, setA] = useState<Attribution | null>(null);
38
+ useEffect(() => {
39
+ setA(rememberedTouch());
40
+ }, []);
41
+ if (!a) return null;
42
+ return (
43
+ <>
44
+ {(Object.entries(ATTRIBUTION_FIELDS) as [keyof Attribution, string][]).map(([k, name]) => (a[k] ? <input key={name} type="hidden" name={name} value={a[k]} /> : null))}
45
+ </>
46
+ );
47
+ }