bsmnt 0.5.0 → 0.6.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.
Files changed (123) hide show
  1. package/README.md +1 -0
  2. package/dist/application/add-hooks/index.d.ts.map +1 -1
  3. package/dist/application/add-hooks/index.js +1 -0
  4. package/dist/application/add-hooks/index.js.map +1 -1
  5. package/dist/application/add-integration/index.d.ts +13 -6
  6. package/dist/application/add-integration/index.d.ts.map +1 -1
  7. package/dist/application/add-integration/index.js +145 -62
  8. package/dist/application/add-integration/index.js.map +1 -1
  9. package/dist/application/create-project/build-context.d.ts +3 -1
  10. package/dist/application/create-project/build-context.d.ts.map +1 -1
  11. package/dist/application/create-project/build-context.js +2 -1
  12. package/dist/application/create-project/build-context.js.map +1 -1
  13. package/dist/application/create-project/index.d.ts +3 -1
  14. package/dist/application/create-project/index.d.ts.map +1 -1
  15. package/dist/application/create-project/index.js +4 -2
  16. package/dist/application/create-project/index.js.map +1 -1
  17. package/dist/application/install-skills/index.js +1 -1
  18. package/dist/application/install-skills/index.js.map +1 -1
  19. package/dist/core/create/execute-plan.d.ts +4 -0
  20. package/dist/core/create/execute-plan.d.ts.map +1 -1
  21. package/dist/core/create/execute-plan.js +8 -0
  22. package/dist/core/create/execute-plan.js.map +1 -1
  23. package/dist/core/create/types.d.ts +2 -0
  24. package/dist/core/create/types.d.ts.map +1 -1
  25. package/dist/domain/skills.d.ts +26 -0
  26. package/dist/domain/skills.d.ts.map +1 -1
  27. package/dist/domain/skills.js +13 -0
  28. package/dist/domain/skills.js.map +1 -1
  29. package/dist/index.js +26 -8
  30. package/dist/index.js.map +1 -1
  31. package/dist/infrastructure/create/executor.d.ts.map +1 -1
  32. package/dist/infrastructure/create/executor.js +5 -0
  33. package/dist/infrastructure/create/executor.js.map +1 -1
  34. package/dist/infrastructure/create/lifecycle-registry.d.ts.map +1 -1
  35. package/dist/infrastructure/create/lifecycle-registry.js +2 -0
  36. package/dist/infrastructure/create/lifecycle-registry.js.map +1 -1
  37. package/dist/infrastructure/create/setup-agent.d.ts +2 -0
  38. package/dist/infrastructure/create/setup-agent.d.ts.map +1 -1
  39. package/dist/infrastructure/create/setup-agent.js +9 -8
  40. package/dist/infrastructure/create/setup-agent.js.map +1 -1
  41. package/dist/infrastructure/create/setup-dotenvx.d.ts +41 -0
  42. package/dist/infrastructure/create/setup-dotenvx.d.ts.map +1 -0
  43. package/dist/infrastructure/create/setup-dotenvx.js +502 -0
  44. package/dist/infrastructure/create/setup-dotenvx.js.map +1 -0
  45. package/dist/infrastructure/create/setup-sanity.d.ts +13 -1
  46. package/dist/infrastructure/create/setup-sanity.d.ts.map +1 -1
  47. package/dist/infrastructure/create/setup-sanity.js +71 -31
  48. package/dist/infrastructure/create/setup-sanity.js.map +1 -1
  49. package/dist/modules/features/cms/sanity/config.d.ts.map +1 -1
  50. package/dist/modules/features/cms/sanity/config.js +2 -1
  51. package/dist/modules/features/cms/sanity/config.js.map +1 -1
  52. package/dist/modules/features/cms/sanity/mergers/layout.d.ts.map +1 -1
  53. package/dist/modules/features/cms/sanity/mergers/layout.js +10 -0
  54. package/dist/modules/features/cms/sanity/mergers/layout.js.map +1 -1
  55. package/dist/modules/features/env/dotenvx/config.d.ts +13 -0
  56. package/dist/modules/features/env/dotenvx/config.d.ts.map +1 -0
  57. package/dist/modules/features/env/dotenvx/config.js +47 -0
  58. package/dist/modules/features/env/dotenvx/config.js.map +1 -0
  59. package/dist/paths.d.ts +8 -0
  60. package/dist/paths.d.ts.map +1 -1
  61. package/dist/paths.js +8 -0
  62. package/dist/paths.js.map +1 -1
  63. package/package.json +5 -1
  64. package/src/agent-skills/json-ld/SKILL.md +85 -0
  65. package/src/agent-skills/manage-env/SKILL.md +67 -0
  66. package/src/modules/features/cms/sanity/files/app/blog/[slug]/page.tsx +48 -21
  67. package/src/modules/features/cms/sanity/files/components/sanity/structured-data.tsx +83 -0
  68. package/src/modules/features/cms/sanity/files/lib/integrations/sanity/queries.ts +14 -0
  69. package/src/modules/features/cms/sanity/files/lib/integrations/sanity/sanity.config.ts +19 -3
  70. package/src/modules/features/cms/sanity/files/lib/integrations/sanity/sanity.types.ts +1 -0
  71. package/src/modules/features/cms/sanity/files/lib/integrations/sanity/schemas/index.ts +36 -1
  72. package/src/modules/features/cms/sanity/files/lib/integrations/sanity/schemas/metadata.ts +12 -1
  73. package/src/modules/features/cms/sanity/files/lib/integrations/sanity/schemas/structured-data-blocks.ts +416 -0
  74. package/src/modules/features/cms/sanity/files/lib/integrations/sanity/schemas/structured-data.ts +100 -0
  75. package/src/modules/features/cms/sanity/files/lib/integrations/sanity/structure.ts +37 -2
  76. package/src/modules/features/cms/sanity/files/lib/integrations/sanity/utils/page-json-ld.ts +208 -0
  77. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/components/same-as-input.tsx +66 -0
  78. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/fetchers/layout.ts +20 -1
  79. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/icons.ts +17 -0
  80. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/queries.ts +22 -0
  81. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/index.ts +28 -0
  82. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/layout/company-data.ts +8 -0
  83. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/layout/structured-data.ts +124 -0
  84. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/shared/metadata.ts +12 -1
  85. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/shared/structured-data-blocks.ts +436 -0
  86. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/singletons.ts +13 -1
  87. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/utils/page-json-ld.ts +208 -0
  88. package/src/modules/features/cms/sanity-pagebuilder/files/lib/utils/json-ld.tsx +324 -15
  89. package/src/modules/features/env/dotenvx/files/decrypt-env.mjs +45 -0
  90. package/src/modules/features/env/dotenvx/files/gitleaks.yml +25 -0
  91. package/src/modules/features/env/dotenvx/files/instrumentation.ts +14 -0
  92. package/src/modules/features/env/dotenvx/files/push-keys.mjs +129 -0
  93. package/src/modules/features/env/dotenvx/files/save-key.mjs +108 -0
  94. package/src/modules/features/env/dotenvx/files/setup-env.mjs +67 -0
  95. package/src/modules/features/env/dotenvx/files/vercelignore +4 -0
  96. package/src/modules/features/env/dotenvx/files/write-env.mjs +47 -0
  97. package/src/templates/next-default/.env.example +14 -6
  98. package/src/templates/next-default/.github/workflows/ci.yml +22 -1
  99. package/src/templates/next-default/.vscode/extensions.json +3 -0
  100. package/src/templates/next-default/README.md +31 -0
  101. package/src/templates/next-default/lib/utils/json-ld.tsx +327 -21
  102. package/src/templates/next-default/next.config.ts +6 -0
  103. package/src/templates/next-experiments/.env.example +14 -6
  104. package/src/templates/next-experiments/.github/workflows/ci.yml +22 -1
  105. package/src/templates/next-experiments/.vscode/extensions.json +3 -0
  106. package/src/templates/next-experiments/README.md +31 -0
  107. package/src/templates/next-experiments/lib/utils/json-ld.tsx +327 -21
  108. package/src/templates/next-experiments/next.config.ts +6 -0
  109. package/src/templates/next-pagebuilder/.env.example +19 -1
  110. package/src/templates/next-pagebuilder/.github/workflows/ci.yml +21 -1
  111. package/src/templates/next-pagebuilder/.vscode/extensions.json +3 -0
  112. package/src/templates/next-pagebuilder/README.md +31 -0
  113. package/src/templates/next-pagebuilder/components/layout/json-ld/index.tsx +32 -19
  114. package/src/templates/next-pagebuilder/components/page-document/index.tsx +38 -0
  115. package/src/templates/next-pagebuilder/lib/integrations/sanity/sanity.types.ts +36 -2
  116. package/src/templates/next-pagebuilder/next.config.ts +6 -0
  117. package/src/templates/next-webgl/.env.example +14 -6
  118. package/src/templates/next-webgl/.github/workflows/ci.yml +22 -1
  119. package/src/templates/next-webgl/.vscode/extensions.json +3 -0
  120. package/src/templates/next-webgl/README.md +31 -0
  121. package/src/templates/next-webgl/lib/utils/json-ld.tsx +327 -21
  122. package/src/templates/next-webgl/next.config.ts +6 -0
  123. package/src/modules/features/cms/sanity/files/lib/utils/json-ld.tsx +0 -244
@@ -0,0 +1,67 @@
1
+ // Restore .env.keys (dotenvx private decryption keys) from 1Password via `op`.
2
+ // Store either the full .env.keys contents (recommended — covers every
3
+ // environment) or just the DOTENV_PRIVATE_KEY value in a 1Password item field;
4
+ // the reference lives in the committed .env (DOTENV_PRIVATE_KEY_OP_REF):
5
+ //
6
+ // DOTENV_PRIVATE_KEY_OP_REF="op://Development/<item>/<field>" bun run env:setup
7
+ //
8
+ // `--auto` (run from the preinstall hook) is for onboarding: it never fails
9
+ // `bun install` — it skips when .env.keys already exists, in CI, or when the
10
+ // reference / `op` CLI is missing.
11
+
12
+ import { spawnSync } from "node:child_process";
13
+ import { existsSync, writeFileSync } from "node:fs";
14
+
15
+ const AUTO = process.argv.includes("--auto");
16
+
17
+ // In --auto mode any unmet precondition exits 0 so the install never breaks.
18
+ const stop = (msg) => {
19
+ console.error(msg);
20
+ process.exit(AUTO ? 0 : 1);
21
+ };
22
+
23
+ // Onboarding skips: nothing to do, or the wrong place to do it.
24
+ if (AUTO && existsSync(".env.keys")) process.exit(0);
25
+ if (AUTO && (process.env.CI || process.env.VERCEL)) process.exit(0);
26
+
27
+ const OP_REF = process.env.DOTENV_PRIVATE_KEY_OP_REF;
28
+ if (!OP_REF) {
29
+ stop(
30
+ "✗ set DOTENV_PRIVATE_KEY_OP_REF to the 1Password reference for .env.keys\n" +
31
+ ' e.g. DOTENV_PRIVATE_KEY_OP_REF="op://Development/<item>/<field>" bun run env:setup',
32
+ );
33
+ }
34
+
35
+ const res = spawnSync("op", ["read", OP_REF], { encoding: "utf8" });
36
+
37
+ if (res.error) {
38
+ const why =
39
+ res.error.code === "ENOENT"
40
+ ? "install the 1Password CLI (`op`) and sign in — https://developer.1password.com/docs/cli/get-started/"
41
+ : res.error.message;
42
+ stop(`✗ failed to run \`op\`: ${why}`);
43
+ }
44
+
45
+ if (res.status !== 0) {
46
+ stop(`✗ \`op read\` failed:\n${res.stderr.trim()}`);
47
+ }
48
+
49
+ const HEADER = `#/------------------!DOTENV_PRIVATE_KEYS!-------------------/
50
+ #/ private decryption keys. DO NOT commit to source control /
51
+ #/ [how it works](https://dotenvx.com/encryption) /
52
+ #/ ⛨ ARMORED KEYS: \`dotenvx armor up\` /
53
+ #/----------------------------------------------------------/
54
+
55
+ # .env
56
+ `;
57
+
58
+ const value = res.stdout.trim();
59
+
60
+ // op may hold the full .env.keys file or just the bare private key. Pass full
61
+ // contents through; wrap a bare key in the standard .env.keys format.
62
+ const contents = value.includes("DOTENV_PRIVATE_KEY")
63
+ ? `${value}\n`
64
+ : `${HEADER}DOTENV_PRIVATE_KEY=${value}\n`;
65
+
66
+ writeFileSync(".env.keys", contents);
67
+ console.info("✓ wrote .env.keys from 1Password");
@@ -0,0 +1,4 @@
1
+ # Never upload the private keys or local env files on `vercel deploy`.
2
+ # (Encrypted .env / .env.production / .env.preview ARE uploaded — they're safe.)
3
+ .env.keys
4
+ .env*.local
@@ -0,0 +1,47 @@
1
+ // Add or (re)encrypt env values without remembering the dotenvx flags.
2
+ //
3
+ // bun run env:set KEY value # encrypt one value into .env
4
+ // bun run env:set KEY value --plain # store it plaintext (e.g. NEXT_PUBLIC_*)
5
+ // bun run env:set KEY value --prod # target .env.production (--preview too)
6
+ // bun run env:encrypt-all # bulk-encrypt every plaintext value in .env
7
+ // bun run env:encrypt-all --prod # bulk-encrypt .env.production
8
+
9
+ import { spawnSync } from "node:child_process";
10
+
11
+ const [mode, ...rest] = process.argv.slice(2);
12
+
13
+ // --prod / --preview pick the per-environment file; default is the shared .env.
14
+ const ENV_FLAGS = new Set(["--prod", "--production", "--preview"]);
15
+ const file =
16
+ rest.includes("--prod") || rest.includes("--production")
17
+ ? ".env.production"
18
+ : rest.includes("--preview")
19
+ ? ".env.preview"
20
+ : ".env";
21
+
22
+ // On bulk encrypt, keep browser-public NEXT_PUBLIC_* and the 1Password pointer
23
+ // plaintext (same exclusions the scaffold uses).
24
+ const EXCLUDE = ["--exclude-key", "DOTENV_PRIVATE_KEY_OP_REF", "NEXT_PUBLIC_*"];
25
+
26
+ function run(args) {
27
+ // `dotenvx` resolves from node_modules/.bin when run via `bun run`.
28
+ const res = spawnSync("dotenvx", args, { stdio: "inherit" });
29
+ process.exit(res.status ?? 1);
30
+ }
31
+
32
+ if (mode === "encrypt") {
33
+ run(["encrypt", "-f", file, ...EXCLUDE]);
34
+ } else if (mode === "set") {
35
+ // Forward KEY, value and any dotenvx flags (e.g. --plain) straight through.
36
+ const passthrough = rest.filter((arg) => !ENV_FLAGS.has(arg));
37
+ if (passthrough.filter((arg) => !arg.startsWith("-")).length < 2) {
38
+ console.error(
39
+ "Usage: bun run env:set KEY value [--plain] [--preview|--prod]",
40
+ );
41
+ process.exit(1);
42
+ }
43
+ run(["set", ...passthrough, "-f", file]);
44
+ } else {
45
+ console.error(`Unknown env command: ${mode ?? "(none)"} (use set | encrypt)`);
46
+ process.exit(1);
47
+ }
@@ -1,11 +1,19 @@
1
1
  # ============================================
2
2
  # Basement Next Starter - Environment Variables
3
3
  # ============================================
4
- # Copy this file to .env.local and fill in the values
5
- # Run: cp .env.example .env.local
4
+ # This project uses dotenvx (https://dotenvx.com): the committed `.env` holds
5
+ # shared values, with secrets encrypted there. The private keys live in
6
+ # `.env.keys` (gitignored) — restore them with `bun run env:setup`.
6
7
  #
7
- # Only configure the integrations you need.
8
- # Remove unused sections to keep your env file clean.
8
+ # Add or change a variable in `.env` (not .env.local):
9
+ # - Secret (API key, token, …): bun run env:set KEY value (--preview / --prod)
10
+ # - Trivial / NEXT_PUBLIC_*: bun run env:set KEY value --plain
11
+ # - Bulk-encrypt a file edited by hand: bun run env:encrypt-all (--preview / --prod)
12
+ #
13
+ # Edge runtime (middleware, `runtime = "edge"`): dotenvx can't decrypt there.
14
+ # Set any secret those read directly in Vercel's env vars, not here.
15
+ #
16
+ # This file just documents the available variables; real values live in `.env`.
9
17
  # ============================================
10
18
 
11
19
 
@@ -16,7 +24,7 @@
16
24
  # Base URL for your site (used for SEO, canonical URLs, etc.)
17
25
  # Development: http://localhost:3000
18
26
  # Production: https://your-domain.com
19
- NEXT_PUBLIC_BASE_URL="http://localhost:3000"
27
+ NEXT_PUBLIC_BASE_URL=""
20
28
 
21
29
  # Draft mode secret for preview functionality
22
30
  # Generate a random string: openssl rand -base64 32
@@ -32,7 +40,7 @@ DRAFT_MODE_TOKEN=""
32
40
 
33
41
  # Required for Sanity
34
42
  NEXT_PUBLIC_SANITY_PROJECT_ID=""
35
- NEXT_PUBLIC_SANITY_DATASET="production"
43
+ NEXT_PUBLIC_SANITY_DATASET=""
36
44
  NEXT_PUBLIC_SANITY_API_READ_TOKEN=""
37
45
 
38
46
  # Optional - for server-side mutations and visual editing
@@ -46,8 +46,29 @@ jobs:
46
46
  - name: Lint
47
47
  run: bun run lint
48
48
 
49
+ # dotenvx needs the private key to decrypt secrets at build time. It's
50
+ # pulled from 1Password via an org-wide service-account token + the op-ref
51
+ # committed in .env. Optional: builds pass without it (only plaintext /
52
+ # NEXT_PUBLIC_* is read), and a per-repo DOTENV_PRIVATE_KEY secret still
53
+ # works as a fallback.
54
+ - name: Install 1Password CLI
55
+ if: ${{ secrets.OP_SERVICE_ACCOUNT_TOKEN != '' }}
56
+ uses: 1password/install-cli-action@v1
57
+
49
58
  - name: Build
50
- run: bun run build
59
+ env:
60
+ OP_SERVICE_ACCOUNT_TOKEN: ${{ secrets.OP_SERVICE_ACCOUNT_TOKEN }}
61
+ DOTENV_PRIVATE_KEY: ${{ secrets.DOTENV_PRIVATE_KEY }}
62
+ run: |
63
+ if [ -n "$OP_SERVICE_ACCOUNT_TOKEN" ]; then
64
+ ref="$(grep -m1 '^DOTENV_PRIVATE_KEY_OP_REF=' .env | cut -d= -f2-)"
65
+ if [ -n "$ref" ]; then
66
+ DOTENV_PRIVATE_KEY="$(op read "$ref")"
67
+ echo "::add-mask::$DOTENV_PRIVATE_KEY"
68
+ export DOTENV_PRIVATE_KEY
69
+ fi
70
+ fi
71
+ bun run build
51
72
 
52
73
  # Runs your tests if any exist; a fresh project with none passes cleanly.
53
74
  - name: Test
@@ -10,6 +10,9 @@
10
10
  // Sanity CMS (GROQ syntax + validation)
11
11
  "sanity-io.vscode-sanity",
12
12
 
13
+ // Env (dotenvx — view decrypted .env values inline)
14
+ "dotenv.dotenvx-vscode",
15
+
13
16
  // DX Enhancements
14
17
  "yoavbls.pretty-ts-errors"
15
18
  ]
@@ -22,6 +22,35 @@ bun dev
22
22
  | `bun format` | Format the codebase |
23
23
  | `bun typecheck` | Run TypeScript |
24
24
  | `bun analyze` | Analyze the Next.js bundle |
25
+ | `bun env:set KEY value` | Encrypt one value into `.env` (`--plain`, `--preview`/`--prod`) |
26
+ | `bun env:encrypt-all` | Bulk-encrypt every plaintext value in a file (`--preview`/`--prod`) |
27
+ | `bun env:setup` | Restore `.env.keys` from 1Password |
28
+ | `bun env:view` | Print the decrypted `.env` (`--preview` / `--prod`) |
29
+ | `bun env:setup-remote` | Push private keys to GitHub / Vercel (`github`/`vercel`, or pick) |
30
+ | `bun env:save` | Save the private key to 1Password (Development vault) |
31
+
32
+ ## Environment variables
33
+
34
+ Secrets are encrypted at rest with [dotenvx](https://dotenvx.com): the committed `.env` holds encrypted values, the private keys live in `.env.keys` (gitignored), and dev/build run through `dotenvx run` so values decrypt automatically.
35
+
36
+ **First run.** `bun install` restores `.env.keys` from 1Password (via `DOTENV_PRIVATE_KEY_OP_REF` in `.env`), or run `bun run env:setup`. Needs 1Password access + the `op` CLI:
37
+
38
+ - macOS: `brew install --cask 1password-cli`
39
+ - Windows: `winget install 1password-cli`
40
+ - Linux / other: [1Password CLI docs](https://developer.1password.com/docs/cli/get-started/)
41
+
42
+ **Add or change a variable** (in `.env`):
43
+
44
+ - Secret: `bun run env:set KEY value` (`--preview`/`--prod` to target an environment)
45
+ - Public / `NEXT_PUBLIC_*`: `bun run env:set KEY value --plain`
46
+ - Bulk-encrypt a hand-edited file: `bun run env:encrypt-all`
47
+ - Inspect: `bun run env:view` (`--preview`/`--prod`)
48
+
49
+ **Per environment.** Scripts load `.env.$VERCEL_ENV` over `.env`. After adding a per-env file, push its key with `bun run env:setup-remote vercel` — `instrumentation.ts` uses it to decrypt at runtime.
50
+
51
+ **Edge runtime.** dotenvx decrypts only in the Node runtime; secrets read in `middleware.ts` or a `runtime = "edge"` route must be set as plain Vercel env vars.
52
+
53
+ A gitleaks pre-commit hook + CI scan block committed plaintext secrets (install: `brew install gitleaks`).
25
54
 
26
55
  ## Project Structure
27
56
 
@@ -60,6 +89,8 @@ The workflow takes its runtime versions from your manifests, so there's a single
60
89
 
61
90
  It also caches Bun's install cache between runs, and the test step is a no-op until you add real test files.
62
91
 
92
+ The build decrypts the committed `.env` with the dotenvx private key, pulled from 1Password: set an **org-level** `OP_SERVICE_ACCOUNT_TOKEN` (a read-only [service account](https://developer.1password.com/docs/service-accounts/) scoped to the `Development` vault) — one secret for every repo — and CI reads the key via the op-ref committed in `.env`. A per-repo `DOTENV_PRIVATE_KEY` secret (`bun run env:setup-remote github`) works as a fallback; builds pass without either (only plaintext / `NEXT_PUBLIC_*` is read).
93
+
63
94
  ### Branch protection
64
95
 
65
96
  To block merges when CI fails, enable branch protection on `main`:
@@ -1,9 +1,15 @@
1
1
  import type {
2
2
  Article,
3
3
  BreadcrumbList,
4
+ Event as EventSchema,
5
+ FAQPage,
6
+ ItemList,
7
+ JobPosting,
4
8
  Organization,
5
9
  SearchAction,
10
+ SoftwareApplication,
6
11
  Thing,
12
+ VideoObject,
7
13
  WebPage,
8
14
  WebSite,
9
15
  WithContext,
@@ -41,10 +47,10 @@ export function JsonLd<T extends Thing>({ data }: { data: WithContext<T> }) {
41
47
 
42
48
  interface WebSiteJsonLdOptions {
43
49
  name: string
44
- url?: string
45
- description?: string
50
+ url?: string | undefined
51
+ description?: string | undefined
46
52
  /** URL to site-wide search (e.g. "/search?q={search_term_string}") */
47
- searchUrl?: string
53
+ searchUrl?: string | undefined
48
54
  }
49
55
 
50
56
  export function generateWebSiteJsonLd(
@@ -70,21 +76,69 @@ export function generateWebSiteJsonLd(
70
76
  }
71
77
  }
72
78
 
79
+ interface OrganizationContactPoint {
80
+ email?: string | null
81
+ telephone?: string | null
82
+ contactType?: string | null
83
+ }
84
+
85
+ interface OrganizationAddress {
86
+ streetAddress?: string | null
87
+ locality?: string | null
88
+ region?: string | null
89
+ postalCode?: string | null
90
+ country?: string | null
91
+ }
92
+
73
93
  interface OrganizationJsonLdOptions {
74
94
  name: string
75
- url?: string
76
- logo?: string
77
- description?: string
78
- sameAs?: string[]
95
+ url?: string | undefined
96
+ logo?: string | undefined
97
+ description?: string | undefined
98
+ sameAs?: string[] | undefined
99
+ legalName?: string | undefined
100
+ contactPoint?: OrganizationContactPoint | null | undefined
101
+ address?: OrganizationAddress | null | undefined
79
102
  }
80
103
 
81
104
  export function generateOrganizationJsonLd(
82
105
  options: OrganizationJsonLdOptions
83
106
  ): WithContext<Organization> {
84
- const { name, url, logo, description, sameAs } = options
107
+ const { name, url, logo, description, sameAs, legalName, contactPoint, address } =
108
+ options
85
109
  const resolvedUrl = resolveUrl(url)
86
110
  const resolvedLogo = resolveUrl(logo)
87
111
 
112
+ const contactPointNode =
113
+ contactPoint &&
114
+ (contactPoint.email || contactPoint.telephone || contactPoint.contactType)
115
+ ? {
116
+ "@type": "ContactPoint" as const,
117
+ ...(contactPoint.email && { email: contactPoint.email }),
118
+ ...(contactPoint.telephone && { telephone: contactPoint.telephone }),
119
+ ...(contactPoint.contactType && {
120
+ contactType: contactPoint.contactType,
121
+ }),
122
+ }
123
+ : undefined
124
+
125
+ const addressNode =
126
+ address &&
127
+ (address.streetAddress ||
128
+ address.locality ||
129
+ address.region ||
130
+ address.postalCode ||
131
+ address.country)
132
+ ? {
133
+ "@type": "PostalAddress" as const,
134
+ ...(address.streetAddress && { streetAddress: address.streetAddress }),
135
+ ...(address.locality && { addressLocality: address.locality }),
136
+ ...(address.region && { addressRegion: address.region }),
137
+ ...(address.postalCode && { postalCode: address.postalCode }),
138
+ ...(address.country && { addressCountry: address.country }),
139
+ }
140
+ : undefined
141
+
88
142
  return {
89
143
  "@context": "https://schema.org",
90
144
  "@type": "Organization",
@@ -92,17 +146,20 @@ export function generateOrganizationJsonLd(
92
146
  ...(resolvedUrl && { url: resolvedUrl }),
93
147
  ...(resolvedLogo && { logo: resolvedLogo }),
94
148
  ...(description && { description }),
95
- ...(sameAs && { sameAs }),
149
+ ...(legalName && { legalName }),
150
+ ...(sameAs?.length && { sameAs }),
151
+ ...(contactPointNode && { contactPoint: contactPointNode }),
152
+ ...(addressNode && { address: addressNode }),
96
153
  }
97
154
  }
98
155
 
99
156
  interface WebPageJsonLdOptions {
100
157
  title: string
101
- url?: string
102
- description?: string
103
- image?: string
104
- datePublished?: string
105
- dateModified?: string
158
+ url?: string | undefined
159
+ description?: string | undefined
160
+ image?: string | undefined
161
+ datePublished?: string | undefined
162
+ dateModified?: string | undefined
106
163
  }
107
164
 
108
165
  export function generateWebPageJsonLd(
@@ -127,13 +184,13 @@ export function generateWebPageJsonLd(
127
184
 
128
185
  interface ArticleJsonLdOptions {
129
186
  title: string
130
- url?: string
131
- description?: string
132
- image?: string
133
- datePublished?: string
134
- dateModified?: string
135
- authorName?: string
136
- authorUrl?: string
187
+ url?: string | undefined
188
+ description?: string | undefined
189
+ image?: string | undefined
190
+ datePublished?: string | undefined
191
+ dateModified?: string | undefined
192
+ authorName?: string | undefined
193
+ authorUrl?: string | undefined
137
194
  }
138
195
 
139
196
  export function generateArticleJsonLd(
@@ -194,3 +251,252 @@ export function generateBreadcrumbJsonLd(
194
251
  }),
195
252
  }
196
253
  }
254
+
255
+ // Page-level generators for the schema.org blocks editors attach via
256
+ // `metadata.jsonLd`. Plain options keep them CMS-independent.
257
+
258
+ export interface FaqJsonLdItem {
259
+ question: string
260
+ answer: string
261
+ }
262
+
263
+ export function generateFaqJsonLd(
264
+ items: FaqJsonLdItem[]
265
+ ): WithContext<FAQPage> {
266
+ return {
267
+ "@context": "https://schema.org",
268
+ "@type": "FAQPage",
269
+ mainEntity: items.map((item) => ({
270
+ "@type": "Question" as const,
271
+ name: item.question,
272
+ acceptedAnswer: {
273
+ "@type": "Answer" as const,
274
+ text: item.answer,
275
+ },
276
+ })),
277
+ }
278
+ }
279
+
280
+ export interface JobPostingJsonLdOptions {
281
+ title: string
282
+ description: string
283
+ datePosted: string
284
+ validThrough?: string | undefined
285
+ employmentType?: string | undefined
286
+ organizationName?: string | undefined
287
+ organizationUrl?: string | undefined
288
+ remote?: boolean | undefined
289
+ locality?: string | undefined
290
+ region?: string | undefined
291
+ country?: string | undefined
292
+ }
293
+
294
+ export function generateJobPostingJsonLd(
295
+ options: JobPostingJsonLdOptions
296
+ ): WithContext<JobPosting> {
297
+ const {
298
+ title,
299
+ description,
300
+ datePosted,
301
+ validThrough,
302
+ employmentType,
303
+ organizationName,
304
+ organizationUrl,
305
+ remote,
306
+ locality,
307
+ region,
308
+ country,
309
+ } = options
310
+ const orgUrl = organizationUrl ? resolveUrl(organizationUrl) : undefined
311
+
312
+ const hiringOrganization = organizationName
313
+ ? {
314
+ "@type": "Organization" as const,
315
+ name: organizationName,
316
+ ...(orgUrl && { url: orgUrl }),
317
+ }
318
+ : undefined
319
+
320
+ const jobLocation =
321
+ locality || region || country
322
+ ? {
323
+ "@type": "Place" as const,
324
+ address: {
325
+ "@type": "PostalAddress" as const,
326
+ ...(locality && { addressLocality: locality }),
327
+ ...(region && { addressRegion: region }),
328
+ ...(country && { addressCountry: country }),
329
+ },
330
+ }
331
+ : undefined
332
+
333
+ return {
334
+ "@context": "https://schema.org",
335
+ "@type": "JobPosting",
336
+ title,
337
+ description,
338
+ datePosted,
339
+ ...(validThrough && { validThrough }),
340
+ ...(employmentType && { employmentType }),
341
+ ...(hiringOrganization && { hiringOrganization }),
342
+ ...(remote && { jobLocationType: "TELECOMMUTE" }),
343
+ ...(jobLocation && { jobLocation }),
344
+ }
345
+ }
346
+
347
+ export interface SoftwareApplicationJsonLdOptions {
348
+ name: string
349
+ description?: string | undefined
350
+ applicationCategory?: string | undefined
351
+ operatingSystem?: string | undefined
352
+ price?: number | undefined
353
+ currency?: string | undefined
354
+ url?: string | undefined
355
+ image?: string | undefined
356
+ }
357
+
358
+ export function generateSoftwareApplicationJsonLd(
359
+ options: SoftwareApplicationJsonLdOptions
360
+ ): WithContext<SoftwareApplication> {
361
+ const {
362
+ name,
363
+ description,
364
+ applicationCategory,
365
+ operatingSystem,
366
+ price,
367
+ currency,
368
+ url,
369
+ image,
370
+ } = options
371
+ const resolvedUrl = url ? resolveUrl(url) : undefined
372
+ const resolvedImage = image ? resolveUrl(image) : undefined
373
+
374
+ const offers =
375
+ typeof price === "number"
376
+ ? {
377
+ "@type": "Offer" as const,
378
+ price: price.toString(),
379
+ ...(currency && { priceCurrency: currency }),
380
+ }
381
+ : undefined
382
+
383
+ return {
384
+ "@context": "https://schema.org",
385
+ "@type": "SoftwareApplication",
386
+ name,
387
+ ...(description && { description }),
388
+ ...(applicationCategory && { applicationCategory }),
389
+ ...(operatingSystem && { operatingSystem }),
390
+ ...(resolvedUrl && { url: resolvedUrl }),
391
+ ...(resolvedImage && { image: resolvedImage }),
392
+ ...(offers && { offers }),
393
+ }
394
+ }
395
+
396
+ export interface ItemListEntry {
397
+ name: string
398
+ url?: string | undefined
399
+ }
400
+
401
+ export interface ItemListJsonLdOptions {
402
+ name?: string | undefined
403
+ items: ItemListEntry[]
404
+ }
405
+
406
+ export function generateItemListJsonLd(
407
+ options: ItemListJsonLdOptions
408
+ ): WithContext<ItemList> {
409
+ const { name, items } = options
410
+
411
+ return {
412
+ "@context": "https://schema.org",
413
+ "@type": "ItemList",
414
+ ...(name && { name }),
415
+ itemListElement: items.map((item, index) => {
416
+ const itemUrl = item.url ? resolveUrl(item.url) : undefined
417
+ return {
418
+ "@type": "ListItem" as const,
419
+ position: index + 1,
420
+ name: item.name,
421
+ ...(itemUrl && { url: itemUrl }),
422
+ }
423
+ }),
424
+ }
425
+ }
426
+
427
+ export interface EventJsonLdOptions {
428
+ name: string
429
+ description?: string | undefined
430
+ startDate: string
431
+ endDate?: string | undefined
432
+ locationName?: string | undefined
433
+ locationAddress?: string | undefined
434
+ url?: string | undefined
435
+ image?: string | undefined
436
+ }
437
+
438
+ export function generateEventJsonLd(
439
+ options: EventJsonLdOptions
440
+ ): WithContext<EventSchema> {
441
+ const {
442
+ name,
443
+ description,
444
+ startDate,
445
+ endDate,
446
+ locationName,
447
+ locationAddress,
448
+ url,
449
+ image,
450
+ } = options
451
+ const resolvedUrl = resolveUrl(url)
452
+ const resolvedImage = resolveUrl(image)
453
+
454
+ const location =
455
+ locationName || locationAddress
456
+ ? {
457
+ "@type": "Place" as const,
458
+ ...(locationName && { name: locationName }),
459
+ ...(locationAddress && { address: locationAddress }),
460
+ }
461
+ : undefined
462
+
463
+ return {
464
+ "@context": "https://schema.org",
465
+ "@type": "Event",
466
+ name,
467
+ startDate,
468
+ ...(endDate && { endDate }),
469
+ ...(description && { description }),
470
+ ...(resolvedUrl && { url: resolvedUrl }),
471
+ ...(resolvedImage && { image: resolvedImage }),
472
+ ...(location && { location }),
473
+ }
474
+ }
475
+
476
+ export interface VideoJsonLdOptions {
477
+ name: string
478
+ description: string
479
+ thumbnail: string
480
+ uploadDate: string
481
+ contentUrl?: string | undefined
482
+ embedUrl?: string | undefined
483
+ }
484
+
485
+ export function generateVideoJsonLd(
486
+ options: VideoJsonLdOptions
487
+ ): WithContext<VideoObject> {
488
+ const { name, description, thumbnail, uploadDate, contentUrl, embedUrl } =
489
+ options
490
+ const resolvedThumbnail = resolveUrl(thumbnail) ?? thumbnail
491
+
492
+ return {
493
+ "@context": "https://schema.org",
494
+ "@type": "VideoObject",
495
+ name,
496
+ description,
497
+ thumbnailUrl: resolvedThumbnail,
498
+ uploadDate,
499
+ ...(contentUrl && { contentUrl }),
500
+ ...(embedUrl && { embedUrl }),
501
+ }
502
+ }
@@ -5,6 +5,12 @@ const nextConfig: NextConfig = {
5
5
  reactStrictMode: true,
6
6
  reactCompiler: true,
7
7
  typedRoutes: true,
8
+ // Bundle the committed encrypted env files into serverless functions so
9
+ // instrumentation.ts can decrypt them at runtime. .env.keys is never listed
10
+ // (and is git/vercel-ignored); missing files are skipped.
11
+ outputFileTracingIncludes: {
12
+ "/**": ["./.env", "./.env.production", "./.env.preview"],
13
+ },
8
14
  turbopack: {
9
15
  rules: {
10
16
  "*.svg": {