bsmnt 0.7.1 → 0.9.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/dist/application/add-hooks/index.d.ts.map +1 -1
- package/dist/application/add-hooks/index.js +1 -0
- package/dist/application/add-hooks/index.js.map +1 -1
- package/dist/application/add-integration/index.d.ts.map +1 -1
- package/dist/application/add-integration/index.js +1 -0
- package/dist/application/add-integration/index.js.map +1 -1
- package/dist/core/create/execute-plan.d.ts +2 -0
- package/dist/core/create/execute-plan.d.ts.map +1 -1
- package/dist/core/create/execute-plan.js +3 -0
- package/dist/core/create/execute-plan.js.map +1 -1
- package/dist/domain/version.d.ts +14 -0
- package/dist/domain/version.d.ts.map +1 -0
- package/dist/domain/version.js +81 -0
- package/dist/domain/version.js.map +1 -0
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/infrastructure/create/executor.d.ts.map +1 -1
- package/dist/infrastructure/create/executor.js +2 -0
- package/dist/infrastructure/create/executor.js.map +1 -1
- package/dist/infrastructure/create/render-generated-docs.d.ts +18 -0
- package/dist/infrastructure/create/render-generated-docs.d.ts.map +1 -0
- package/dist/infrastructure/create/render-generated-docs.js +72 -0
- package/dist/infrastructure/create/render-generated-docs.js.map +1 -0
- package/dist/infrastructure/create/setup-dotenvx.d.ts.map +1 -1
- package/dist/infrastructure/create/setup-dotenvx.js +2 -1
- package/dist/infrastructure/create/setup-dotenvx.js.map +1 -1
- package/dist/infrastructure/create/setup-remote.d.ts +6 -1
- package/dist/infrastructure/create/setup-remote.d.ts.map +1 -1
- package/dist/infrastructure/create/setup-remote.js +7 -2
- package/dist/infrastructure/create/setup-remote.js.map +1 -1
- package/dist/infrastructure/update/cache.d.ts +14 -0
- package/dist/infrastructure/update/cache.d.ts.map +1 -0
- package/dist/infrastructure/update/cache.js +41 -0
- package/dist/infrastructure/update/cache.js.map +1 -0
- package/dist/infrastructure/update/check-version.d.ts +2 -0
- package/dist/infrastructure/update/check-version.d.ts.map +1 -0
- package/dist/infrastructure/update/check-version.js +33 -0
- package/dist/infrastructure/update/check-version.js.map +1 -0
- package/dist/infrastructure/update/notifier.d.ts +8 -0
- package/dist/infrastructure/update/notifier.d.ts.map +1 -0
- package/dist/infrastructure/update/notifier.js +141 -0
- package/dist/infrastructure/update/notifier.js.map +1 -0
- package/dist/modules/features/cms/sanity-pagebuilder/config.d.ts.map +1 -1
- package/dist/modules/features/cms/sanity-pagebuilder/config.js +2 -0
- package/dist/modules/features/cms/sanity-pagebuilder/config.js.map +1 -1
- package/package.json +1 -1
- package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/components/block-actions-item.tsx +81 -0
- package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/icons.ts +2 -0
- package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/page-builder-config.ts +7 -0
- package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/queries.ts +72 -21
- package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/components/index.ts +1 -0
- package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/components/reusable/gated-content.ts +178 -0
- package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/index.ts +9 -1
- package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/shared/metadata.ts +54 -1
- package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/shared/page-builder.ts +4 -0
- package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/shared/page.ts +11 -0
- package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/structure.ts +4 -0
- package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/utils/page-builder-markdown.ts +20 -0
- package/src/modules/features/cms/sanity-pagebuilder/files/lib/utils/metadata.ts +27 -5
- package/src/modules/features/env/dotenvx/files/setup-remote.ts +3 -1
- package/src/templates/next-default/.env.example +8 -0
- package/src/templates/next-default/.github/workflows/ci.yml +7 -0
- package/src/templates/next-default/README.md +14 -0
- package/src/templates/next-default/knip.json +8 -6
- package/src/templates/next-default/lefthook.yml +0 -5
- package/src/templates/next-experiments/.env.example +8 -0
- package/src/templates/next-experiments/.github/workflows/ci.yml +7 -0
- package/src/templates/next-experiments/README.md +14 -0
- package/src/templates/next-experiments/knip.json +8 -6
- package/src/templates/next-experiments/lefthook.yml +0 -5
- package/src/templates/next-pagebuilder/.env.example +8 -0
- package/src/templates/next-pagebuilder/.github/PULL_REQUEST_TEMPLATE.md +13 -0
- package/src/templates/next-pagebuilder/.github/workflows/ci.yml +15 -0
- package/src/templates/next-pagebuilder/README.md +14 -0
- package/src/templates/next-pagebuilder/app/(content)/[[...slug]]/page.tsx +2 -2
- package/src/templates/next-pagebuilder/app/actions/unlock-gated-content.ts +39 -0
- package/src/templates/next-pagebuilder/components/page-builder/components/gated-content/index.tsx +169 -0
- package/src/templates/next-pagebuilder/components/page-builder/renderer.tsx +3 -0
- package/src/templates/next-pagebuilder/knip.json +8 -6
- package/src/templates/next-pagebuilder/lefthook.yml +0 -5
- package/src/templates/next-pagebuilder/lib/integrations/sanity/sanity.types.ts +81 -0
- package/src/templates/next-webgl/.env.example +8 -0
- package/src/templates/next-webgl/.github/workflows/ci.yml +7 -0
- package/src/templates/next-webgl/README.md +14 -0
- package/src/templates/next-webgl/knip.json +8 -6
- package/src/templates/next-webgl/lefthook.yml +0 -5
|
@@ -22,6 +22,7 @@ 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
|
+
<!-- bsmnt:if dotenvx -->
|
|
25
26
|
| `bun env:set KEY value` | Encrypt one value into `.env` (`--plain`, `--preview`/`--prod`) |
|
|
26
27
|
| `bun env:encrypt-all` | Bulk-encrypt every plaintext value in a file (`--preview`/`--prod`) |
|
|
27
28
|
| `bun env:setup` | Restore `.env.keys` from 1Password |
|
|
@@ -29,7 +30,9 @@ bun dev
|
|
|
29
30
|
| `bun env:setup-remote` | Push private keys to GitHub / Vercel (`github`/`vercel`, or pick) |
|
|
30
31
|
| `bun setup-remote` | Create the GitHub repo, link a Vercel project, and push the key to Vercel env |
|
|
31
32
|
| `bun env:save` | Save the private key to 1Password (Development vault) |
|
|
33
|
+
<!-- bsmnt:endif -->
|
|
32
34
|
|
|
35
|
+
<!-- bsmnt:if dotenvx -->
|
|
33
36
|
## Environment variables
|
|
34
37
|
|
|
35
38
|
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.
|
|
@@ -53,6 +56,15 @@ Secrets are encrypted at rest with [dotenvx](https://dotenvx.com): the committed
|
|
|
53
56
|
|
|
54
57
|
A gitleaks pre-commit hook + CI scan block committed plaintext secrets (install: `brew install gitleaks`).
|
|
55
58
|
|
|
59
|
+
<!-- bsmnt:endif -->
|
|
60
|
+
<!-- bsmnt:if !dotenvx -->
|
|
61
|
+
## Environment variables
|
|
62
|
+
|
|
63
|
+
Copy `.env.example` to `.env.local` and fill in the values you need. `.env.local` is gitignored, so it stays off version control. Only `NEXT_PUBLIC_*` variables are exposed to the browser — everything else is server-only.
|
|
64
|
+
|
|
65
|
+
For deploys, set the same variables in your host's dashboard (e.g. Vercel → Settings → Environment Variables).
|
|
66
|
+
|
|
67
|
+
<!-- bsmnt:endif -->
|
|
56
68
|
## Project Structure
|
|
57
69
|
|
|
58
70
|
```txt
|
|
@@ -90,8 +102,10 @@ The workflow takes its runtime versions from your manifests, so there's a single
|
|
|
90
102
|
|
|
91
103
|
It also caches Bun's install cache between runs, and the test step is a no-op until you add real test files.
|
|
92
104
|
|
|
105
|
+
<!-- bsmnt:if dotenvx -->
|
|
93
106
|
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).
|
|
94
107
|
|
|
108
|
+
<!-- bsmnt:endif -->
|
|
95
109
|
### Branch protection
|
|
96
110
|
|
|
97
111
|
To block merges when CI fails, enable branch protection on `main`:
|
|
@@ -12,11 +12,13 @@
|
|
|
12
12
|
"@svgr/webpack",
|
|
13
13
|
"gsap",
|
|
14
14
|
"@gsap/react",
|
|
15
|
-
"motion"
|
|
15
|
+
"motion",
|
|
16
|
+
"detect-gpu",
|
|
17
|
+
"lodash-es",
|
|
18
|
+
"@types/lodash-es",
|
|
19
|
+
"react-device-detect",
|
|
20
|
+
"zustand"
|
|
16
21
|
],
|
|
17
|
-
"ignoreBinaries": [
|
|
18
|
-
|
|
19
|
-
"gh",
|
|
20
|
-
"vercel"
|
|
21
|
-
]
|
|
22
|
+
"ignoreBinaries": ["op", "gh", "vercel"],
|
|
23
|
+
"ignoreFiles": [".agents/**", ".claude/**", "hooks/**"]
|
|
22
24
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# ============================================
|
|
2
2
|
# Basement Next Starter - Environment Variables
|
|
3
3
|
# ============================================
|
|
4
|
+
# bsmnt:if dotenvx
|
|
4
5
|
# This project uses dotenvx (https://dotenvx.com): the committed `.env` holds
|
|
5
6
|
# shared values, with secrets encrypted there. The private keys live in
|
|
6
7
|
# `.env.keys` (gitignored) — restore them with `bun run env:setup`.
|
|
@@ -15,6 +16,13 @@
|
|
|
15
16
|
#
|
|
16
17
|
# This file just documents the available variables; real values live in `.env`.
|
|
17
18
|
# ============================================
|
|
19
|
+
# bsmnt:endif
|
|
20
|
+
# bsmnt:if !dotenvx
|
|
21
|
+
# Copy this file to `.env.local` and fill in the values you need.
|
|
22
|
+
# `.env.local` is gitignored, so real values stay off version control.
|
|
23
|
+
# Only NEXT_PUBLIC_* variables are exposed to the browser.
|
|
24
|
+
# ============================================
|
|
25
|
+
# bsmnt:endif
|
|
18
26
|
|
|
19
27
|
|
|
20
28
|
# ============================================
|
|
@@ -46,6 +46,7 @@ jobs:
|
|
|
46
46
|
- name: Lint
|
|
47
47
|
run: bun run lint
|
|
48
48
|
|
|
49
|
+
# bsmnt:if dotenvx
|
|
49
50
|
# dotenvx needs the private key to decrypt secrets at build time. It's
|
|
50
51
|
# pulled from 1Password via an org-wide service-account token + the op-ref
|
|
51
52
|
# committed in .env. Optional: builds pass without it (only plaintext /
|
|
@@ -70,6 +71,12 @@ jobs:
|
|
|
70
71
|
fi
|
|
71
72
|
bun run build
|
|
72
73
|
|
|
74
|
+
# bsmnt:endif
|
|
75
|
+
# bsmnt:if !dotenvx
|
|
76
|
+
- name: Build
|
|
77
|
+
run: bun run build
|
|
78
|
+
|
|
79
|
+
# bsmnt:endif
|
|
73
80
|
# Runs your tests if any exist; a fresh project with none passes cleanly.
|
|
74
81
|
- name: Test
|
|
75
82
|
run: |
|
|
@@ -22,6 +22,7 @@ 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
|
+
<!-- bsmnt:if dotenvx -->
|
|
25
26
|
| `bun env:set KEY value` | Encrypt one value into `.env` (`--plain`, `--preview`/`--prod`) |
|
|
26
27
|
| `bun env:encrypt-all` | Bulk-encrypt every plaintext value in a file (`--preview`/`--prod`) |
|
|
27
28
|
| `bun env:setup` | Restore `.env.keys` from 1Password |
|
|
@@ -29,7 +30,9 @@ bun dev
|
|
|
29
30
|
| `bun env:setup-remote` | Push private keys to GitHub / Vercel (`github`/`vercel`, or pick) |
|
|
30
31
|
| `bun setup-remote` | Create the GitHub repo, link a Vercel project, and push the key to Vercel env |
|
|
31
32
|
| `bun env:save` | Save the private key to 1Password (Development vault) |
|
|
33
|
+
<!-- bsmnt:endif -->
|
|
32
34
|
|
|
35
|
+
<!-- bsmnt:if dotenvx -->
|
|
33
36
|
## Environment variables
|
|
34
37
|
|
|
35
38
|
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.
|
|
@@ -53,6 +56,15 @@ Secrets are encrypted at rest with [dotenvx](https://dotenvx.com): the committed
|
|
|
53
56
|
|
|
54
57
|
A gitleaks pre-commit hook + CI scan block committed plaintext secrets (install: `brew install gitleaks`).
|
|
55
58
|
|
|
59
|
+
<!-- bsmnt:endif -->
|
|
60
|
+
<!-- bsmnt:if !dotenvx -->
|
|
61
|
+
## Environment variables
|
|
62
|
+
|
|
63
|
+
Copy `.env.example` to `.env.local` and fill in the values you need. `.env.local` is gitignored, so it stays off version control. Only `NEXT_PUBLIC_*` variables are exposed to the browser — everything else is server-only.
|
|
64
|
+
|
|
65
|
+
For deploys, set the same variables in your host's dashboard (e.g. Vercel → Settings → Environment Variables).
|
|
66
|
+
|
|
67
|
+
<!-- bsmnt:endif -->
|
|
56
68
|
## Project Structure
|
|
57
69
|
|
|
58
70
|
```txt
|
|
@@ -90,8 +102,10 @@ The workflow takes its runtime versions from your manifests, so there's a single
|
|
|
90
102
|
|
|
91
103
|
It also caches Bun's install cache between runs, and the test step is a no-op until you add real test files.
|
|
92
104
|
|
|
105
|
+
<!-- bsmnt:if dotenvx -->
|
|
93
106
|
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).
|
|
94
107
|
|
|
108
|
+
<!-- bsmnt:endif -->
|
|
95
109
|
### Branch protection
|
|
96
110
|
|
|
97
111
|
To block merges when CI fails, enable branch protection on `main`:
|
|
@@ -12,11 +12,13 @@
|
|
|
12
12
|
"@svgr/webpack",
|
|
13
13
|
"gsap",
|
|
14
14
|
"@gsap/react",
|
|
15
|
-
"motion"
|
|
15
|
+
"motion",
|
|
16
|
+
"detect-gpu",
|
|
17
|
+
"lodash-es",
|
|
18
|
+
"@types/lodash-es",
|
|
19
|
+
"react-device-detect",
|
|
20
|
+
"zustand"
|
|
16
21
|
],
|
|
17
|
-
"ignoreBinaries": [
|
|
18
|
-
|
|
19
|
-
"gh",
|
|
20
|
-
"vercel"
|
|
21
|
-
]
|
|
22
|
+
"ignoreBinaries": ["op", "gh", "vercel"],
|
|
23
|
+
"ignoreFiles": [".agents/**", ".claude/**", "hooks/**"]
|
|
22
24
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# ============================================
|
|
2
2
|
# Basement Next Starter - Environment Variables
|
|
3
3
|
# ============================================
|
|
4
|
+
# bsmnt:if dotenvx
|
|
4
5
|
# This project uses dotenvx (https://dotenvx.com): the committed `.env` holds
|
|
5
6
|
# shared values, with secrets encrypted there. The private keys live in
|
|
6
7
|
# `.env.keys` (gitignored) — restore them with `bun run env:setup`.
|
|
@@ -15,6 +16,13 @@
|
|
|
15
16
|
#
|
|
16
17
|
# This file just documents the available variables; real values live in `.env`.
|
|
17
18
|
# ============================================
|
|
19
|
+
# bsmnt:endif
|
|
20
|
+
# bsmnt:if !dotenvx
|
|
21
|
+
# Copy this file to `.env.local` and fill in the values you need.
|
|
22
|
+
# `.env.local` is gitignored, so real values stay off version control.
|
|
23
|
+
# Only NEXT_PUBLIC_* variables are exposed to the browser.
|
|
24
|
+
# ============================================
|
|
25
|
+
# bsmnt:endif
|
|
18
26
|
|
|
19
27
|
# Base URL
|
|
20
28
|
NEXT_PUBLIC_BASE_URL=
|
|
@@ -46,6 +46,7 @@ jobs:
|
|
|
46
46
|
- name: Lint
|
|
47
47
|
run: bun run lint
|
|
48
48
|
|
|
49
|
+
# bsmnt:if dotenvx
|
|
49
50
|
# dotenvx needs the private key to decrypt secrets at build time. It's
|
|
50
51
|
# pulled from 1Password via an org-wide service-account token + the op-ref
|
|
51
52
|
# committed in .env. Optional: builds pass without it (only plaintext /
|
|
@@ -77,6 +78,20 @@ jobs:
|
|
|
77
78
|
fi
|
|
78
79
|
bun run build
|
|
79
80
|
|
|
81
|
+
# bsmnt:endif
|
|
82
|
+
# bsmnt:if !dotenvx
|
|
83
|
+
- name: Build
|
|
84
|
+
env:
|
|
85
|
+
# Sanity project ID and dataset are public (they ship in the client
|
|
86
|
+
# bundle), so they are stored as repository variables. The Sanity read
|
|
87
|
+
# token is secret — add it under Settings → Secrets and variables →
|
|
88
|
+
# Actions. All three are empty (and harmless) until Sanity is set up.
|
|
89
|
+
NEXT_PUBLIC_SANITY_PROJECT_ID: ${{ vars.NEXT_PUBLIC_SANITY_PROJECT_ID }}
|
|
90
|
+
NEXT_PUBLIC_SANITY_DATASET: ${{ vars.NEXT_PUBLIC_SANITY_DATASET }}
|
|
91
|
+
SANITY_API_READ_TOKEN: ${{ secrets.SANITY_API_READ_TOKEN }}
|
|
92
|
+
run: bun run build
|
|
93
|
+
|
|
94
|
+
# bsmnt:endif
|
|
80
95
|
# Runs your tests if any exist; a fresh project with none passes cleanly.
|
|
81
96
|
- name: Test
|
|
82
97
|
run: |
|
|
@@ -21,6 +21,7 @@ bun dev
|
|
|
21
21
|
- `bun build` - Production build
|
|
22
22
|
- `bun lint` - Run linter
|
|
23
23
|
- `bun sanity:typegen` - Generate Sanity types
|
|
24
|
+
<!-- bsmnt:if dotenvx -->
|
|
24
25
|
- `bun env:set KEY value` - Encrypt one value into `.env` (`--plain`, `--preview`/`--prod`)
|
|
25
26
|
- `bun env:encrypt-all` - Bulk-encrypt every plaintext value in a file (`--preview`/`--prod`)
|
|
26
27
|
- `bun env:setup` - Restore `.env.keys` from 1Password
|
|
@@ -28,7 +29,9 @@ bun dev
|
|
|
28
29
|
- `bun env:setup-remote` - Push private keys to GitHub / Vercel (`github`/`vercel`, or pick)
|
|
29
30
|
- `bun setup-remote` - Create the GitHub repo, link a Vercel project, and push the key to Vercel env
|
|
30
31
|
- `bun env:save` - Save the private key to 1Password (Development vault)
|
|
32
|
+
<!-- bsmnt:endif -->
|
|
31
33
|
|
|
34
|
+
<!-- bsmnt:if dotenvx -->
|
|
32
35
|
## Environment variables
|
|
33
36
|
|
|
34
37
|
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.
|
|
@@ -52,6 +55,15 @@ Secrets are encrypted at rest with [dotenvx](https://dotenvx.com): the committed
|
|
|
52
55
|
|
|
53
56
|
A gitleaks pre-commit hook + CI scan block committed plaintext secrets (install: `brew install gitleaks`).
|
|
54
57
|
|
|
58
|
+
<!-- bsmnt:endif -->
|
|
59
|
+
<!-- bsmnt:if !dotenvx -->
|
|
60
|
+
## Environment variables
|
|
61
|
+
|
|
62
|
+
Copy `.env.example` to `.env.local` and fill in the values you need. `.env.local` is gitignored, so it stays off version control. Only `NEXT_PUBLIC_*` variables are exposed to the browser — everything else is server-only.
|
|
63
|
+
|
|
64
|
+
For deploys, set the same variables in your host's dashboard (e.g. Vercel → Settings → Environment Variables).
|
|
65
|
+
|
|
66
|
+
<!-- bsmnt:endif -->
|
|
55
67
|
## Continuous Integration
|
|
56
68
|
|
|
57
69
|
A GitHub Actions workflow (`.github/workflows/ci.yml`) runs on every pull request to `main` and on pushes to `main`. It runs type checking, linting, the production build, tests, and `knip`.
|
|
@@ -63,8 +75,10 @@ The workflow takes its runtime versions from your manifests, so there's a single
|
|
|
63
75
|
|
|
64
76
|
It also caches Bun's install cache between runs, and the test step is a no-op until you add real test files.
|
|
65
77
|
|
|
78
|
+
<!-- bsmnt:if dotenvx -->
|
|
66
79
|
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).
|
|
67
80
|
|
|
81
|
+
<!-- bsmnt:endif -->
|
|
68
82
|
### Sanity environment
|
|
69
83
|
|
|
70
84
|
The build reads Sanity config from GitHub Actions variables/secrets (empty and harmless until you configure Sanity). Once your repo is on GitHub, the quickest way to set them is:
|
|
@@ -14,7 +14,7 @@ import { checkRedirect } from "@/lib/utils/check-redirect"
|
|
|
14
14
|
import { generateSanityMetadata } from "@/lib/utils/metadata"
|
|
15
15
|
import { getSlugTag } from "@/lib/utils/slug-tag"
|
|
16
16
|
|
|
17
|
-
const getPageDocument = async (slug: string | null, stega
|
|
17
|
+
const getPageDocument = async (slug: string | null, stega?: boolean) =>
|
|
18
18
|
fetchSanity<PAGE_QUERY_RESULT>({
|
|
19
19
|
query: PAGE_QUERY,
|
|
20
20
|
params: { slug },
|
|
@@ -27,7 +27,7 @@ const toPath = (segments?: string[]) =>
|
|
|
27
27
|
|
|
28
28
|
const getResolvedPage = async (
|
|
29
29
|
path: string | null,
|
|
30
|
-
stega
|
|
30
|
+
stega?: boolean
|
|
31
31
|
): Promise<NonNullable<PAGE_QUERY_RESULT> | null> => {
|
|
32
32
|
const { data: page } = await getPageDocument(path, stega)
|
|
33
33
|
return (page as NonNullable<PAGE_QUERY_RESULT> | null) ?? null
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"use server"
|
|
2
|
+
|
|
3
|
+
import { fetchSanity } from "@/lib/integrations/sanity/live"
|
|
4
|
+
import { GATED_CONTENT_QUERY } from "@/lib/integrations/sanity/queries"
|
|
5
|
+
import type { GATED_CONTENT_QUERY_RESULT } from "@/lib/integrations/sanity/sanity.types"
|
|
6
|
+
|
|
7
|
+
export type GatedContentPayload = NonNullable<GATED_CONTENT_QUERY_RESULT>
|
|
8
|
+
|
|
9
|
+
export type UnlockResult =
|
|
10
|
+
| { ok: true; payload: GatedContentPayload }
|
|
11
|
+
| { ok: false; error: string }
|
|
12
|
+
|
|
13
|
+
type UnlockInput = { documentId: string; email: string }
|
|
14
|
+
|
|
15
|
+
export async function unlockGatedContent({
|
|
16
|
+
documentId,
|
|
17
|
+
email,
|
|
18
|
+
}: UnlockInput): Promise<UnlockResult> {
|
|
19
|
+
if (!documentId) return { ok: false, error: "Missing content id." }
|
|
20
|
+
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
|
|
21
|
+
return { ok: false, error: "Enter a valid email address." }
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Lead-capture seam: forward `email` to your CRM / email provider here.
|
|
25
|
+
// This is not access control — the payload is withheld from the page until
|
|
26
|
+
// this action runs, but anyone who submits the form receives it.
|
|
27
|
+
|
|
28
|
+
const { data } = await fetchSanity<GATED_CONTENT_QUERY_RESULT>({
|
|
29
|
+
query: GATED_CONTENT_QUERY,
|
|
30
|
+
params: { id: documentId },
|
|
31
|
+
// stega must stay off: it injects invisible characters into strings, which
|
|
32
|
+
// would corrupt the asset URLs returned for download/playback.
|
|
33
|
+
stega: false,
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
if (!data) return { ok: false, error: "Content not found." }
|
|
37
|
+
|
|
38
|
+
return { ok: true, payload: data }
|
|
39
|
+
}
|
package/src/templates/next-pagebuilder/components/page-builder/components/gated-content/index.tsx
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import { useEffect, useState } from "react"
|
|
4
|
+
import {
|
|
5
|
+
type GatedContentPayload,
|
|
6
|
+
unlockGatedContent,
|
|
7
|
+
} from "@/app/actions/unlock-gated-content"
|
|
8
|
+
import type { PageBuilderBlock } from "@/components/page-builder/types"
|
|
9
|
+
import { RichText } from "@/components/sanity/rich-text"
|
|
10
|
+
import { Link } from "@/components/ui/link"
|
|
11
|
+
|
|
12
|
+
type GatedContentBlock = Extract<PageBuilderBlock, { _type: "gatedContent" }>
|
|
13
|
+
|
|
14
|
+
type GatedContentProps = {
|
|
15
|
+
block: GatedContentBlock
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
type GateState = "locked" | "loading" | "unlocked"
|
|
19
|
+
|
|
20
|
+
const STORAGE_PREFIX = "gated-content:"
|
|
21
|
+
|
|
22
|
+
export function GatedContent({ block }: GatedContentProps) {
|
|
23
|
+
const storageKey = `${STORAGE_PREFIX}${block._id}`
|
|
24
|
+
const [state, setState] = useState<GateState>("locked")
|
|
25
|
+
const [payload, setPayload] = useState<GatedContentPayload | null>(null)
|
|
26
|
+
const [error, setError] = useState<string | null>(null)
|
|
27
|
+
const [justUnlocked, setJustUnlocked] = useState(false)
|
|
28
|
+
|
|
29
|
+
// The payload is withheld from the page query, so it never reaches the client
|
|
30
|
+
// until this action runs. We remember the submitted email so a returning
|
|
31
|
+
// visitor re-fetches without seeing the form again.
|
|
32
|
+
const unlock = async (email: string, persist: boolean) => {
|
|
33
|
+
setState("loading")
|
|
34
|
+
setError(null)
|
|
35
|
+
|
|
36
|
+
const result = await unlockGatedContent({ documentId: block._id, email })
|
|
37
|
+
|
|
38
|
+
if (!result.ok) {
|
|
39
|
+
setError(result.error)
|
|
40
|
+
setState("locked")
|
|
41
|
+
if (!persist) window.localStorage.removeItem(storageKey)
|
|
42
|
+
return
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
setPayload(result.payload)
|
|
46
|
+
setState("unlocked")
|
|
47
|
+
if (persist) {
|
|
48
|
+
window.localStorage.setItem(storageKey, email)
|
|
49
|
+
setJustUnlocked(true)
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// biome-ignore lint/correctness/useExhaustiveDependencies: re-fetch only when the block changes; unlock closes over stable values
|
|
54
|
+
useEffect(() => {
|
|
55
|
+
const savedEmail = window.localStorage.getItem(storageKey)
|
|
56
|
+
if (savedEmail) void unlock(savedEmail, false)
|
|
57
|
+
}, [storageKey])
|
|
58
|
+
|
|
59
|
+
const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
|
|
60
|
+
event.preventDefault()
|
|
61
|
+
const email =
|
|
62
|
+
new FormData(event.currentTarget).get("email")?.toString() ?? ""
|
|
63
|
+
void unlock(email, true)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return (
|
|
67
|
+
<section className="py-8 md:py-12">
|
|
68
|
+
{block.teaserHeading ? (
|
|
69
|
+
<h2 className="mb-4 font-semibold text-2xl tracking-tight md:text-4xl">
|
|
70
|
+
{block.teaserHeading}
|
|
71
|
+
</h2>
|
|
72
|
+
) : null}
|
|
73
|
+
|
|
74
|
+
{block.teaserBody ? (
|
|
75
|
+
<div className="max-w-[62ch] space-y-4 text-base/7 text-current/80 md:text-lg/8">
|
|
76
|
+
<RichText content={block.teaserBody} />
|
|
77
|
+
</div>
|
|
78
|
+
) : null}
|
|
79
|
+
|
|
80
|
+
{state === "unlocked" && payload ? (
|
|
81
|
+
<div className="mt-8">
|
|
82
|
+
{justUnlocked && block.successMessage ? (
|
|
83
|
+
<p className="mb-6 rounded-lg bg-black/[0.04] px-4 py-3 text-current/72 text-sm">
|
|
84
|
+
{block.successMessage}
|
|
85
|
+
</p>
|
|
86
|
+
) : null}
|
|
87
|
+
<GatedReveal payload={payload} />
|
|
88
|
+
</div>
|
|
89
|
+
) : (
|
|
90
|
+
<form
|
|
91
|
+
onSubmit={handleSubmit}
|
|
92
|
+
className="mt-8 flex max-w-md flex-col gap-4 rounded-[1.5rem] border border-black/10 bg-black/[0.02] p-6"
|
|
93
|
+
>
|
|
94
|
+
{block.gateHeadline ? (
|
|
95
|
+
<h3 className="font-semibold text-lg">{block.gateHeadline}</h3>
|
|
96
|
+
) : null}
|
|
97
|
+
{block.gateDescription ? (
|
|
98
|
+
<p className="text-current/72 text-sm">{block.gateDescription}</p>
|
|
99
|
+
) : null}
|
|
100
|
+
<input
|
|
101
|
+
type="email"
|
|
102
|
+
name="email"
|
|
103
|
+
required
|
|
104
|
+
placeholder="you@example.com"
|
|
105
|
+
className="rounded-lg border border-black/10 bg-white px-4 py-2.5 text-sm outline-none focus:border-black/40"
|
|
106
|
+
/>
|
|
107
|
+
{error ? <p className="text-red-600 text-sm">{error}</p> : null}
|
|
108
|
+
<button
|
|
109
|
+
type="submit"
|
|
110
|
+
disabled={state === "loading"}
|
|
111
|
+
className="rounded-lg bg-black px-4 py-2.5 font-medium text-sm text-white transition-colors hover:bg-black/80 disabled:opacity-60"
|
|
112
|
+
>
|
|
113
|
+
{state === "loading" ? "Unlocking…" : block.submitLabel || "Unlock"}
|
|
114
|
+
</button>
|
|
115
|
+
</form>
|
|
116
|
+
)}
|
|
117
|
+
</section>
|
|
118
|
+
)
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function GatedReveal({ payload }: { payload: GatedContentPayload }) {
|
|
122
|
+
if (payload.kind === "richText" && payload.content) {
|
|
123
|
+
return (
|
|
124
|
+
<div className="max-w-[62ch] space-y-4 text-base/7 text-current/80 md:text-lg/8">
|
|
125
|
+
<RichText content={payload.content} />
|
|
126
|
+
</div>
|
|
127
|
+
)
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (payload.kind === "file" && payload.file?.url) {
|
|
131
|
+
return (
|
|
132
|
+
<Link
|
|
133
|
+
href={payload.file.url}
|
|
134
|
+
download
|
|
135
|
+
className="inline-flex items-center gap-2 rounded-lg bg-black px-4 py-2.5 font-medium text-sm text-white transition-colors hover:bg-black/80"
|
|
136
|
+
>
|
|
137
|
+
Download {payload.file.originalFilename ?? "file"}
|
|
138
|
+
</Link>
|
|
139
|
+
)
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (payload.kind === "video") {
|
|
143
|
+
if (payload.video.url) {
|
|
144
|
+
return (
|
|
145
|
+
<div className="aspect-video w-full max-w-3xl overflow-hidden rounded-lg">
|
|
146
|
+
<iframe
|
|
147
|
+
src={payload.video.url}
|
|
148
|
+
title="Gated video"
|
|
149
|
+
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
|
|
150
|
+
allowFullScreen
|
|
151
|
+
className="h-full w-full border-0"
|
|
152
|
+
/>
|
|
153
|
+
</div>
|
|
154
|
+
)
|
|
155
|
+
}
|
|
156
|
+
if (payload.video.file?.url) {
|
|
157
|
+
return (
|
|
158
|
+
// biome-ignore lint/a11y/useMediaCaption: editor-supplied video without a caption track
|
|
159
|
+
<video
|
|
160
|
+
controls
|
|
161
|
+
src={payload.video.file.url}
|
|
162
|
+
className="w-full max-w-3xl rounded-lg"
|
|
163
|
+
/>
|
|
164
|
+
)
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return null
|
|
169
|
+
}
|
|
@@ -2,6 +2,7 @@ import { ArticleContent } from "./components/article-content"
|
|
|
2
2
|
import { ContentCollection } from "./components/content-collection"
|
|
3
3
|
import type { ContentCollectionSearchParams } from "./components/content-collection/types"
|
|
4
4
|
import { Description } from "./components/description"
|
|
5
|
+
import { GatedContent } from "./components/gated-content"
|
|
5
6
|
import { Hero } from "./components/hero"
|
|
6
7
|
import type { PageBuilderBlock, PageBuilderBlocks } from "./types"
|
|
7
8
|
|
|
@@ -42,6 +43,8 @@ function PageBuilderNode({
|
|
|
42
43
|
return <Hero block={block} />
|
|
43
44
|
case "description":
|
|
44
45
|
return <Description block={block} />
|
|
46
|
+
case "gatedContent":
|
|
47
|
+
return <GatedContent block={block} />
|
|
45
48
|
default:
|
|
46
49
|
return null
|
|
47
50
|
}
|
|
@@ -22,11 +22,13 @@
|
|
|
22
22
|
"rxjs",
|
|
23
23
|
"gsap",
|
|
24
24
|
"@gsap/react",
|
|
25
|
-
"motion"
|
|
25
|
+
"motion",
|
|
26
|
+
"detect-gpu",
|
|
27
|
+
"lodash-es",
|
|
28
|
+
"@types/lodash-es",
|
|
29
|
+
"react-device-detect",
|
|
30
|
+
"zustand"
|
|
26
31
|
],
|
|
27
|
-
"ignoreBinaries": [
|
|
28
|
-
|
|
29
|
-
"op",
|
|
30
|
-
"vercel"
|
|
31
|
-
]
|
|
32
|
+
"ignoreBinaries": ["gh", "op", "vercel"],
|
|
33
|
+
"ignoreFiles": [".agents/**", ".claude/**", "hooks/**"]
|
|
32
34
|
}
|