bsmnt 0.12.0 → 0.13.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-integration/index.d.ts.map +1 -1
- package/dist/application/add-integration/index.js +23 -0
- package/dist/application/add-integration/index.js.map +1 -1
- package/dist/domain/consent.d.ts +17 -0
- package/dist/domain/consent.d.ts.map +1 -0
- package/dist/domain/consent.js +13 -0
- package/dist/domain/consent.js.map +1 -0
- package/dist/domain/integration.d.ts +19 -2
- package/dist/domain/integration.d.ts.map +1 -1
- package/dist/domain/integration.js +10 -1
- package/dist/domain/integration.js.map +1 -1
- package/dist/domain/jobs.d.ts +17 -0
- package/dist/domain/jobs.d.ts.map +1 -0
- package/dist/domain/jobs.js +13 -0
- package/dist/domain/jobs.js.map +1 -0
- package/dist/domain/skills.d.ts +23 -2
- package/dist/domain/skills.d.ts.map +1 -1
- package/dist/domain/skills.js +23 -2
- package/dist/domain/skills.js.map +1 -1
- package/dist/domain/templates.d.ts +7 -1
- package/dist/domain/templates.d.ts.map +1 -1
- package/dist/domain/templates.js +2 -0
- package/dist/domain/templates.js.map +1 -1
- package/dist/index.js +34 -9
- package/dist/index.js.map +1 -1
- package/dist/infrastructure/create/setup-agent.d.ts.map +1 -1
- package/dist/infrastructure/create/setup-agent.js +11 -4
- package/dist/infrastructure/create/setup-agent.js.map +1 -1
- package/dist/infrastructure/integrations/inject-integration.d.ts.map +1 -1
- package/dist/infrastructure/integrations/inject-integration.js +36 -2
- package/dist/infrastructure/integrations/inject-integration.js.map +1 -1
- package/dist/modules/features/consent/ketch/config.d.ts +18 -0
- package/dist/modules/features/consent/ketch/config.d.ts.map +1 -0
- package/dist/modules/features/consent/ketch/config.js +24 -0
- package/dist/modules/features/consent/ketch/config.js.map +1 -0
- package/dist/modules/features/consent/ketch/mergers/biome.d.ts +16 -0
- package/dist/modules/features/consent/ketch/mergers/biome.d.ts.map +1 -0
- package/dist/modules/features/consent/ketch/mergers/biome.js +31 -0
- package/dist/modules/features/consent/ketch/mergers/biome.js.map +1 -0
- package/dist/modules/features/consent/ketch/mergers/check-integration.d.ts +13 -0
- package/dist/modules/features/consent/ketch/mergers/check-integration.d.ts.map +1 -0
- package/dist/modules/features/consent/ketch/mergers/check-integration.js +64 -0
- package/dist/modules/features/consent/ketch/mergers/check-integration.js.map +1 -0
- package/dist/modules/features/consent/ketch/mergers/env-example.d.ts +12 -0
- package/dist/modules/features/consent/ketch/mergers/env-example.d.ts.map +1 -0
- package/dist/modules/features/consent/ketch/mergers/env-example.js +28 -0
- package/dist/modules/features/consent/ketch/mergers/env-example.js.map +1 -0
- package/dist/modules/features/consent/ketch/mergers/footer.d.ts +17 -0
- package/dist/modules/features/consent/ketch/mergers/footer.d.ts.map +1 -0
- package/dist/modules/features/consent/ketch/mergers/footer.js +40 -0
- package/dist/modules/features/consent/ketch/mergers/footer.js.map +1 -0
- package/dist/modules/features/consent/ketch/mergers/knip.d.ts +18 -0
- package/dist/modules/features/consent/ketch/mergers/knip.d.ts.map +1 -0
- package/dist/modules/features/consent/ketch/mergers/knip.js +37 -0
- package/dist/modules/features/consent/ketch/mergers/knip.js.map +1 -0
- package/dist/modules/features/consent/ketch/mergers/layout.d.ts +25 -0
- package/dist/modules/features/consent/ketch/mergers/layout.d.ts.map +1 -0
- package/dist/modules/features/consent/ketch/mergers/layout.js +55 -0
- package/dist/modules/features/consent/ketch/mergers/layout.js.map +1 -0
- package/dist/modules/features/jobs/ashby/config.d.ts +19 -0
- package/dist/modules/features/jobs/ashby/config.d.ts.map +1 -0
- package/dist/modules/features/jobs/ashby/config.js +29 -0
- package/dist/modules/features/jobs/ashby/config.js.map +1 -0
- package/dist/modules/features/jobs/ashby/mergers/check-integration.d.ts +14 -0
- package/dist/modules/features/jobs/ashby/mergers/check-integration.d.ts.map +1 -0
- package/dist/modules/features/jobs/ashby/mergers/check-integration.js +78 -0
- package/dist/modules/features/jobs/ashby/mergers/check-integration.js.map +1 -0
- package/dist/modules/features/jobs/ashby/mergers/env-example.d.ts +12 -0
- package/dist/modules/features/jobs/ashby/mergers/env-example.d.ts.map +1 -0
- package/dist/modules/features/jobs/ashby/mergers/env-example.js +27 -0
- package/dist/modules/features/jobs/ashby/mergers/env-example.js.map +1 -0
- package/dist/modules/features/jobs/ashby/mergers/knip.d.ts +18 -0
- package/dist/modules/features/jobs/ashby/mergers/knip.d.ts.map +1 -0
- package/dist/modules/features/jobs/ashby/mergers/knip.js +41 -0
- package/dist/modules/features/jobs/ashby/mergers/knip.js.map +1 -0
- package/dist/paths.d.ts +12 -0
- package/dist/paths.d.ts.map +1 -1
- package/dist/paths.js +12 -0
- package/dist/paths.js.map +1 -1
- package/package.json +3 -1
- package/src/agent-skills/ashby-pagebuilder-block/SKILL.md +277 -0
- package/src/agent-skills/hubspot-pagebuilder-block/SKILL.md +58 -25
- package/src/agent-skills/ketch-consent-check/SKILL.md +197 -0
- package/src/agent-skills/ketch-consent-setup/SKILL.md +326 -0
- package/src/agent-skills/ketch-consent-setup/references/ui-customization.md +329 -0
- package/src/agent-skills/page-builder-block/SKILL.md +150 -0
- package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/components/anchor-id-input.tsx +17 -0
- package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/components/presentation-input.tsx +146 -0
- package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/components/spacing-steps-input.tsx +84 -0
- package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/fetchers/layout.ts +6 -3
- package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/icons.ts +8 -0
- package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/queries.ts +12 -0
- package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/components/reusable/blog-content.ts +3 -3
- package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/components/reusable/description.ts +3 -3
- package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/components/reusable/gated-content.ts +3 -3
- package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/components/reusable/hero.ts +3 -3
- package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/components/reusable/rich-text-block.ts +3 -3
- package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/components/singleton/content-collection.ts +3 -3
- package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/index.ts +8 -0
- package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/layout/spacing-settings.ts +120 -0
- package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/shared/anchor.ts +75 -0
- package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/shared/block.ts +46 -0
- package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/shared/presentation.ts +44 -0
- package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/shared/spacing.ts +164 -0
- package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/singletons.ts +7 -0
- package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/components/anchor-id-input.tsx +17 -0
- package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/components/presentation-input.tsx +146 -0
- package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/components/spacing-steps-input.tsx +84 -0
- package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/fetchers/layout.ts +4 -2
- package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/icons.ts +8 -0
- package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/queries.ts +12 -0
- package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/components/reusable/blog-content.ts +3 -3
- package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/components/reusable/description.ts +3 -3
- package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/components/reusable/gated-content.ts +3 -3
- package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/components/reusable/hero.ts +3 -3
- package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/components/reusable/rich-text-block.ts +3 -3
- package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/components/singleton/content-collection.ts +3 -3
- package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/index.ts +8 -0
- package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/layout/spacing-settings.ts +120 -0
- package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/shared/anchor.ts +75 -0
- package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/shared/block.ts +46 -0
- package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/shared/presentation.ts +44 -0
- package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/shared/spacing.ts +164 -0
- package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/singletons.ts +7 -0
- package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/tags.ts +2 -0
- package/src/modules/features/consent/ketch/files/lib/integrations/check-integration.ts +25 -0
- package/src/modules/features/consent/ketch/files/lib/integrations/ketch/README.md +70 -0
- package/src/modules/features/consent/ketch/files/lib/integrations/ketch/categories.ts +20 -0
- package/src/modules/features/consent/ketch/files/lib/integrations/ketch/consent-store.ts +107 -0
- package/src/modules/features/consent/ketch/files/lib/integrations/ketch/env.ts +20 -0
- package/src/modules/features/consent/ketch/files/lib/integrations/ketch/ketch-experience.css +136 -0
- package/src/modules/features/consent/ketch/files/lib/integrations/ketch/preferences-button.tsx +51 -0
- package/src/modules/features/consent/ketch/files/lib/integrations/ketch/script.tsx +8 -0
- package/src/modules/features/consent/ketch/files/lib/integrations/ketch/types.ts +15 -0
- package/src/modules/features/consent/ketch/files/lib/integrations/ketch/use-consent.ts +26 -0
- package/src/modules/features/jobs/ashby/files/_sanity-adapter/job-board-block.ts +53 -0
- package/src/modules/features/jobs/ashby/files/lib/integrations/ashby/README.md +134 -0
- package/src/modules/features/jobs/ashby/files/lib/integrations/ashby/env.ts +3 -0
- package/src/modules/features/jobs/ashby/files/lib/integrations/ashby/index.ts +40 -0
- package/src/modules/features/jobs/ashby/files/lib/integrations/ashby/normalize.ts +124 -0
- package/src/modules/features/jobs/ashby/files/lib/integrations/ashby/types.ts +20 -0
- package/src/modules/features/jobs/ashby/files/lib/integrations/check-integration.ts +25 -0
- package/src/templates/next-pagebuilder/AGENTS.md +129 -0
- package/src/templates/next-pagebuilder/CLAUDE.md +3 -0
- package/src/templates/next-pagebuilder/README.md +70 -0
- package/src/templates/next-pagebuilder/app/(content)/layout.tsx +2 -0
- package/src/templates/next-pagebuilder/components/layout/spacing-scale/index.tsx +18 -0
- package/src/templates/next-pagebuilder/components/page-builder/components/article-content/index.tsx +1 -1
- package/src/templates/next-pagebuilder/components/page-builder/components/content-collection/index.tsx +2 -2
- package/src/templates/next-pagebuilder/components/page-builder/components/description.tsx +3 -7
- package/src/templates/next-pagebuilder/components/page-builder/components/gated-content/index.tsx +2 -2
- package/src/templates/next-pagebuilder/components/page-builder/components/hero.tsx +3 -7
- package/src/templates/next-pagebuilder/components/page-builder/components/rich-text-block.tsx +3 -5
- package/src/templates/next-pagebuilder/components/page-builder/registry.tsx +70 -0
- package/src/templates/next-pagebuilder/components/page-builder/renderer.tsx +33 -49
- package/src/templates/next-pagebuilder/components/page-builder/section.tsx +35 -0
- package/src/templates/next-pagebuilder/components/page-document/index.tsx +1 -1
- package/src/templates/next-pagebuilder/lib/integrations/sanity/sanity.types.ts +92 -4
- package/src/templates/next-pagebuilder-cache/AGENTS.md +136 -0
- package/src/templates/next-pagebuilder-cache/CLAUDE.md +3 -0
- package/src/templates/next-pagebuilder-cache/README.md +75 -0
- package/src/templates/next-pagebuilder-cache/app/(content)/layout.tsx +2 -0
- package/src/templates/next-pagebuilder-cache/components/layout/spacing-scale/index.tsx +46 -0
- package/src/templates/next-pagebuilder-cache/components/page-builder/components/article-content/index.tsx +1 -1
- package/src/templates/next-pagebuilder-cache/components/page-builder/components/content-collection/index.tsx +2 -2
- package/src/templates/next-pagebuilder-cache/components/page-builder/components/description.tsx +3 -7
- package/src/templates/next-pagebuilder-cache/components/page-builder/components/gated-content/index.tsx +2 -2
- package/src/templates/next-pagebuilder-cache/components/page-builder/components/hero.tsx +3 -7
- package/src/templates/next-pagebuilder-cache/components/page-builder/components/rich-text-block.tsx +3 -5
- package/src/templates/next-pagebuilder-cache/components/page-builder/registry.tsx +70 -0
- package/src/templates/next-pagebuilder-cache/components/page-builder/renderer.tsx +33 -49
- package/src/templates/next-pagebuilder-cache/components/page-builder/section.tsx +35 -0
- package/src/templates/next-pagebuilder-cache/components/page-document/index.tsx +1 -1
- package/src/templates/next-pagebuilder-cache/lib/integrations/sanity/sanity.types.ts +92 -4
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { CaseIcon } from "@sanity/icons"
|
|
2
|
+
import { defineField, defineType } from "sanity"
|
|
3
|
+
|
|
4
|
+
export const ashbyJobBoardBlock = defineType({
|
|
5
|
+
name: "ashbyJobBoardBlock",
|
|
6
|
+
title: "Ashby Job Board",
|
|
7
|
+
type: "document",
|
|
8
|
+
icon: CaseIcon,
|
|
9
|
+
description:
|
|
10
|
+
"Lists open roles pulled live from Ashby. There is no content to manage here beyond an optional heading.",
|
|
11
|
+
fields: [
|
|
12
|
+
defineField({
|
|
13
|
+
name: "internalTitle",
|
|
14
|
+
title: "Internal Title",
|
|
15
|
+
type: "string",
|
|
16
|
+
description: "Only used to identify this block in the Studio.",
|
|
17
|
+
validation: (Rule) => Rule.required(),
|
|
18
|
+
}),
|
|
19
|
+
defineField({
|
|
20
|
+
name: "heading",
|
|
21
|
+
title: "Heading",
|
|
22
|
+
type: "string",
|
|
23
|
+
description: "Optional heading shown above the list of positions.",
|
|
24
|
+
}),
|
|
25
|
+
defineField({
|
|
26
|
+
name: "boardName",
|
|
27
|
+
title: "Job Board Name",
|
|
28
|
+
type: "string",
|
|
29
|
+
description:
|
|
30
|
+
"Optional. Overrides the ASHBY_JOB_BOARD_NAME environment variable — the board's slug in its jobs.ashbyhq.com URL.",
|
|
31
|
+
}),
|
|
32
|
+
],
|
|
33
|
+
preview: {
|
|
34
|
+
select: {
|
|
35
|
+
title: "internalTitle",
|
|
36
|
+
heading: "heading",
|
|
37
|
+
boardName: "boardName",
|
|
38
|
+
},
|
|
39
|
+
prepare({ title, heading, boardName }) {
|
|
40
|
+
return {
|
|
41
|
+
title: title || "Ashby Job Board",
|
|
42
|
+
subtitle: heading || boardName || "Open positions",
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
export const ashbyJobBoardBlockReference = defineType({
|
|
49
|
+
name: "ashbyJobBoardBlockReference",
|
|
50
|
+
title: "Ashby Job Board",
|
|
51
|
+
type: "reference",
|
|
52
|
+
to: [{ type: "ashbyJobBoardBlock" }],
|
|
53
|
+
})
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
# Ashby job board integration
|
|
2
|
+
|
|
3
|
+
Agnostic, reusable [Ashby](https://www.ashbyhq.com/) job-board reader. The core
|
|
4
|
+
(`index.ts`, `normalize.ts`, `types.ts`) has **no CMS dependency** and adds **no
|
|
5
|
+
runtime packages** — it reads Ashby's public Job Posting API and normalizes it
|
|
6
|
+
into a stable `CareerJob[]`.
|
|
7
|
+
|
|
8
|
+
Fetching is **server-only**: `getCareerJobs` uses `react`'s `cache` and Next.js
|
|
9
|
+
`fetch` revalidation, so call it from a Server Component, Route Handler, or
|
|
10
|
+
`generateMetadata` — never from a `"use client"` component.
|
|
11
|
+
|
|
12
|
+
## 1. Environment
|
|
13
|
+
|
|
14
|
+
Ashby's job-board endpoint is public — no token or private app required. The
|
|
15
|
+
only configuration is the board name, which is the slug in your board's URL
|
|
16
|
+
(`https://jobs.ashbyhq.com/<board-name>`):
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
ASHBY_JOB_BOARD_NAME=your-board
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
The value is not a secret, so it can stay plaintext in the committed env file.
|
|
23
|
+
It is only read server-side (in `env.ts`); it is never exposed to the client.
|
|
24
|
+
You can also skip the variable entirely and pass the board name explicitly.
|
|
25
|
+
|
|
26
|
+
`env.ts` resolves it once at module scope as `ashbyBoardName`, and
|
|
27
|
+
`isAshbyConfigured()` in `lib/integrations/check-integration.ts` derives from
|
|
28
|
+
that same value — so a "configured" check can never disagree with what
|
|
29
|
+
`getCareerJobs` actually resolves.
|
|
30
|
+
|
|
31
|
+
## 2. Read the jobs
|
|
32
|
+
|
|
33
|
+
```tsx
|
|
34
|
+
import { getCareerJobs } from "@/lib/integrations/ashby"
|
|
35
|
+
|
|
36
|
+
export default async function Careers() {
|
|
37
|
+
const jobs = await getCareerJobs()
|
|
38
|
+
|
|
39
|
+
return (
|
|
40
|
+
<ul>
|
|
41
|
+
{jobs.map((job) => (
|
|
42
|
+
<li key={job.id}>
|
|
43
|
+
<a href={job.href}>{job.title}</a>
|
|
44
|
+
</li>
|
|
45
|
+
))}
|
|
46
|
+
</ul>
|
|
47
|
+
)
|
|
48
|
+
}
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Pass a board name to override `ASHBY_JOB_BOARD_NAME` (useful for multi-brand
|
|
52
|
+
sites, or to render a second board on one page):
|
|
53
|
+
|
|
54
|
+
```tsx
|
|
55
|
+
const jobs = await getCareerJobs("your-other-board")
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
`getCareerJobs` throws when the board name is missing or when Ashby responds
|
|
59
|
+
with a non-2xx status. Wrap the call so a job-board outage degrades gracefully
|
|
60
|
+
instead of failing the whole page:
|
|
61
|
+
|
|
62
|
+
```tsx
|
|
63
|
+
let jobs: CareerJob[] = []
|
|
64
|
+
try {
|
|
65
|
+
jobs = await getCareerJobs()
|
|
66
|
+
} catch (error) {
|
|
67
|
+
console.error("Failed to load Ashby jobs", error)
|
|
68
|
+
}
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## 3. What you get
|
|
72
|
+
|
|
73
|
+
`normalize.ts` tolerates both the current (`jobs`) and legacy (`jobPostings`)
|
|
74
|
+
payload shapes, drops unlisted postings, and resolves each field from the
|
|
75
|
+
several key spellings Ashby has used over time. Every optional field is
|
|
76
|
+
`string | null` — never `undefined` — so rendering stays branch-free:
|
|
77
|
+
|
|
78
|
+
| Field | Notes |
|
|
79
|
+
| --- | --- |
|
|
80
|
+
| `id` / `title` / `href` | Required; a posting missing any of them is dropped |
|
|
81
|
+
| `applyUrl` | Direct application link when Ashby exposes one |
|
|
82
|
+
| `location` / `department` / `team` | Resolved from the flat or nested variants |
|
|
83
|
+
| `employmentType` | e.g. `Full-time` |
|
|
84
|
+
| `workType` | `workplaceType`, or derived from `isRemote` |
|
|
85
|
+
| `isRemote` | `true` only when Ashby says so explicitly |
|
|
86
|
+
| `publishedAt` | ISO date string |
|
|
87
|
+
| `descriptionHtml` | Raw HTML — sanitize before injecting |
|
|
88
|
+
| `compensation` | `{ summary, display }`; respect `display` before rendering |
|
|
89
|
+
|
|
90
|
+
## 4. Caching
|
|
91
|
+
|
|
92
|
+
The core caches the board for 5 minutes via `next: { revalidate: 300 }`, and
|
|
93
|
+
deduplicates calls within a single request via React `cache`.
|
|
94
|
+
|
|
95
|
+
In a **Cache Components** project (`cacheComponents: true`), the fetch-level
|
|
96
|
+
revalidate is ignored — wrap the call in your own cached function instead:
|
|
97
|
+
|
|
98
|
+
```tsx
|
|
99
|
+
import { cacheLife } from "next/cache"
|
|
100
|
+
import { getCareerJobs } from "@/lib/integrations/ashby"
|
|
101
|
+
|
|
102
|
+
async function getCachedJobs() {
|
|
103
|
+
"use cache"
|
|
104
|
+
cacheLife("minutes")
|
|
105
|
+
return getCareerJobs()
|
|
106
|
+
}
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
## 5. Optional: Sanity Studio block
|
|
110
|
+
|
|
111
|
+
When this integration is added to a project that already has Sanity, a
|
|
112
|
+
`sanity/` folder ships alongside the core:
|
|
113
|
+
|
|
114
|
+
- `sanity/job-board-block.ts` — an `ashbyJobBoardBlock` document type with an
|
|
115
|
+
optional `heading` and an optional per-block `boardName` override
|
|
116
|
+
|
|
117
|
+
Register it by adding `ashbyJobBoardBlock` to your schema's document list, then
|
|
118
|
+
render it from your own component.
|
|
119
|
+
|
|
120
|
+
## 6. Use as a page-builder block
|
|
121
|
+
|
|
122
|
+
In a basement **Sanity page-builder** project, `ashbyJobBoardBlock` can be a
|
|
123
|
+
selectable block. Adding this integration installs the
|
|
124
|
+
**`ashby-pagebuilder-block`** agent skill — ask your coding agent to "wire the
|
|
125
|
+
Ashby job board into the page builder" and it makes the edits and verifies the
|
|
126
|
+
typecheck. To do it by hand:
|
|
127
|
+
|
|
128
|
+
1. `schemas/index.ts` — import `ashbyJobBoardBlock`, add it to `schema.types`
|
|
129
|
+
2. `page-builder-config.ts` — add `"ashbyJobBoardBlock"` to
|
|
130
|
+
`PageBuilderDocumentType` and an `ashbyJobBoardBlockReference` member
|
|
131
|
+
3. `structure.ts` — add `S.documentTypeListItem("ashbyJobBoardBlock")`
|
|
132
|
+
4. `components/page-builder/renderer.tsx` — add a `case "ashbyJobBoardBlock"`
|
|
133
|
+
that awaits `getCareerJobs(block.boardName)` and renders the list
|
|
134
|
+
5. `bun run sanity:typegen && bun typecheck`
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { cache } from "react"
|
|
2
|
+
import { ashbyBoardName } from "./env"
|
|
3
|
+
import { normalizeBoard } from "./normalize"
|
|
4
|
+
import type { CareerJob } from "./types"
|
|
5
|
+
|
|
6
|
+
const ASHBY_JOB_BOARD_API_URL = "https://api.ashbyhq.com/posting-api/job-board"
|
|
7
|
+
const BOARD_REVALIDATE_SECONDS = 300
|
|
8
|
+
|
|
9
|
+
export function resolveAshbyBoardName(boardName?: string | null): string {
|
|
10
|
+
const resolved = boardName?.trim() || ashbyBoardName
|
|
11
|
+
|
|
12
|
+
if (!resolved) {
|
|
13
|
+
throw new Error(
|
|
14
|
+
"Missing Ashby job board name. Pass it to getCareerJobs() or set ASHBY_JOB_BOARD_NAME."
|
|
15
|
+
)
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
return resolved
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export const getCareerJobs = cache(
|
|
22
|
+
async (boardName?: string | null): Promise<CareerJob[]> => {
|
|
23
|
+
const board = resolveAshbyBoardName(boardName)
|
|
24
|
+
|
|
25
|
+
const response = await fetch(
|
|
26
|
+
`${ASHBY_JOB_BOARD_API_URL}/${encodeURIComponent(board)}?includeCompensation=true`,
|
|
27
|
+
{ next: { revalidate: BOARD_REVALIDATE_SECONDS } }
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
if (!response.ok) {
|
|
31
|
+
throw new Error(`Ashby request failed with ${response.status}`)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return normalizeBoard(await response.json(), board)
|
|
35
|
+
}
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
export { ashbyBoardName } from "./env"
|
|
39
|
+
export { normalizeBoard } from "./normalize"
|
|
40
|
+
export type { CareerCompensation, CareerJob } from "./types"
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import type { CareerCompensation, CareerJob } from "./types"
|
|
2
|
+
|
|
3
|
+
const toRecord = (value: unknown): Record<string, unknown> | null =>
|
|
4
|
+
value && typeof value === "object" ? (value as Record<string, unknown>) : null
|
|
5
|
+
|
|
6
|
+
const string = (value: unknown): string | null =>
|
|
7
|
+
typeof value === "string" && value.trim() ? value.trim() : null
|
|
8
|
+
|
|
9
|
+
const getFieldValue = (
|
|
10
|
+
job: Record<string, unknown>,
|
|
11
|
+
key: string
|
|
12
|
+
): string | null => {
|
|
13
|
+
const value = job[key]
|
|
14
|
+
if (typeof value === "string") return string(value)
|
|
15
|
+
|
|
16
|
+
const nested = toRecord(value)
|
|
17
|
+
if (!nested) return null
|
|
18
|
+
|
|
19
|
+
return string(nested.name) ?? string(nested.title) ?? string(nested.value)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const idFromUrl = (url: string | null): string | null => {
|
|
23
|
+
if (!url) return null
|
|
24
|
+
|
|
25
|
+
try {
|
|
26
|
+
const parsed = new URL(url)
|
|
27
|
+
const fromQuery = string(parsed.searchParams.get("ashby_jid"))
|
|
28
|
+
if (fromQuery) return fromQuery
|
|
29
|
+
|
|
30
|
+
const segments = parsed.pathname.split("/").filter(Boolean)
|
|
31
|
+
const last = segments.at(-1)
|
|
32
|
+
if (last === "application") return string(segments.at(-2))
|
|
33
|
+
return string(last)
|
|
34
|
+
} catch {
|
|
35
|
+
return null
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const getCompensation = (
|
|
40
|
+
job: Record<string, unknown>
|
|
41
|
+
): CareerCompensation | null => {
|
|
42
|
+
const compensation = toRecord(job.compensation)
|
|
43
|
+
if (!compensation) return null
|
|
44
|
+
|
|
45
|
+
const summary =
|
|
46
|
+
string(compensation.compensationTierSummary) ??
|
|
47
|
+
string(compensation.scrapeableCompensationSalarySummary)
|
|
48
|
+
if (!summary) return null
|
|
49
|
+
|
|
50
|
+
return {
|
|
51
|
+
summary,
|
|
52
|
+
display: job.shouldDisplayCompensationOnJobPostings !== false,
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const getWorkType = (job: Record<string, unknown>): string | null => {
|
|
57
|
+
const workplaceType = getFieldValue(job, "workplaceType")
|
|
58
|
+
if (workplaceType) return workplaceType
|
|
59
|
+
if (job.isRemote === true) return "Remote"
|
|
60
|
+
return job.isRemote === false ? "On-site" : null
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const toCareerJob = (value: unknown, boardName: string): CareerJob | null => {
|
|
64
|
+
const job = toRecord(value)
|
|
65
|
+
if (!job) return null
|
|
66
|
+
|
|
67
|
+
if (job.isListed === false) return null
|
|
68
|
+
|
|
69
|
+
const jobUrl = string(job.jobUrl)
|
|
70
|
+
const applyUrl = string(job.applyUrl)
|
|
71
|
+
|
|
72
|
+
const id =
|
|
73
|
+
string(job.id) ??
|
|
74
|
+
string(job.jobId) ??
|
|
75
|
+
string(job.jobPostingId) ??
|
|
76
|
+
string(job.ashbyJid) ??
|
|
77
|
+
idFromUrl(jobUrl) ??
|
|
78
|
+
idFromUrl(applyUrl)
|
|
79
|
+
|
|
80
|
+
const title = getFieldValue(job, "title")
|
|
81
|
+
const href =
|
|
82
|
+
jobUrl ??
|
|
83
|
+
applyUrl ??
|
|
84
|
+
(id
|
|
85
|
+
? `https://jobs.ashbyhq.com/${encodeURIComponent(boardName)}/${id}`
|
|
86
|
+
: null)
|
|
87
|
+
|
|
88
|
+
if (!(id && title && href)) return null
|
|
89
|
+
|
|
90
|
+
return {
|
|
91
|
+
id,
|
|
92
|
+
title,
|
|
93
|
+
href,
|
|
94
|
+
applyUrl,
|
|
95
|
+
location:
|
|
96
|
+
getFieldValue(job, "location") ?? getFieldValue(job, "locationName"),
|
|
97
|
+
department:
|
|
98
|
+
getFieldValue(job, "department") ?? getFieldValue(job, "departmentName"),
|
|
99
|
+
team: getFieldValue(job, "team") ?? getFieldValue(job, "teamName"),
|
|
100
|
+
employmentType:
|
|
101
|
+
getFieldValue(job, "employmentType") ?? getFieldValue(job, "jobType"),
|
|
102
|
+
workType: getWorkType(job),
|
|
103
|
+
publishedAt: string(job.publishedAt) ?? string(job.publishedDate) ?? null,
|
|
104
|
+
isRemote: job.isRemote === true,
|
|
105
|
+
descriptionHtml: string(job.descriptionHtml),
|
|
106
|
+
compensation: getCompensation(job),
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export const normalizeBoard = (
|
|
111
|
+
payload: unknown,
|
|
112
|
+
boardName: string
|
|
113
|
+
): CareerJob[] => {
|
|
114
|
+
const board = toRecord(payload)
|
|
115
|
+
if (!board) return []
|
|
116
|
+
|
|
117
|
+
const jobs = Array.isArray(board.jobs) ? board.jobs : null
|
|
118
|
+
const legacy = Array.isArray(board.jobPostings) ? board.jobPostings : null
|
|
119
|
+
const raw = jobs ?? legacy ?? []
|
|
120
|
+
|
|
121
|
+
return raw
|
|
122
|
+
.map((job) => toCareerJob(job, boardName))
|
|
123
|
+
.filter((job): job is CareerJob => job !== null)
|
|
124
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export type CareerCompensation = {
|
|
2
|
+
summary: string
|
|
3
|
+
display: boolean
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export type CareerJob = {
|
|
7
|
+
id: string
|
|
8
|
+
title: string
|
|
9
|
+
href: string
|
|
10
|
+
applyUrl: string | null
|
|
11
|
+
location: string | null
|
|
12
|
+
department: string | null
|
|
13
|
+
team: string | null
|
|
14
|
+
employmentType: string | null
|
|
15
|
+
workType: string | null
|
|
16
|
+
publishedAt: string | null
|
|
17
|
+
isRemote: boolean
|
|
18
|
+
descriptionHtml: string | null
|
|
19
|
+
compensation: CareerCompensation | null
|
|
20
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { ashbyBoardName } from "@/lib/integrations/ashby/env"
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Integration Configuration Checker
|
|
5
|
+
*
|
|
6
|
+
* Utilities to check if integrations are configured via environment variables.
|
|
7
|
+
* This helps with tree-shaking unused integrations from the bundle.
|
|
8
|
+
*
|
|
9
|
+
* Usage:
|
|
10
|
+
* ```ts
|
|
11
|
+
* import { isAshbyConfigured } from '@/integrations/check-integration'
|
|
12
|
+
*
|
|
13
|
+
* if (isAshbyConfigured()) {
|
|
14
|
+
* // Only fetch the job board once a board name is set
|
|
15
|
+
* }
|
|
16
|
+
* ```
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Check if the Ashby job board is configured. Derived from env.ts, not a
|
|
21
|
+
* second read of `process.env`.
|
|
22
|
+
*/
|
|
23
|
+
export function isAshbyConfigured(): boolean {
|
|
24
|
+
return Boolean(ashbyBoardName)
|
|
25
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
# AGENTS.md
|
|
2
|
+
|
|
3
|
+
Instructions for AI agents working in this repository. Human-facing setup,
|
|
4
|
+
environment variables and the publish webhook live in the README — read it too.
|
|
5
|
+
|
|
6
|
+
This is a **Next.js + Sanity page builder**: all URL structure is managed in
|
|
7
|
+
Sanity, not in code, and a single catch-all route `[[...slug]]` renders every
|
|
8
|
+
page.
|
|
9
|
+
|
|
10
|
+
**Never add a page route under `app/`.** A new page is a `page` document in the
|
|
11
|
+
Studio with a slug, not a directory. `app/(content)/[[...slug]]/page.tsx`
|
|
12
|
+
resolves every one of them; a hand-written route shadows that resolver and
|
|
13
|
+
becomes a URL no editor can see, move or delete. The only things that belong in
|
|
14
|
+
`app/` are non-page endpoints — the Studio, API handlers, sitemap, feed.
|
|
15
|
+
|
|
16
|
+
## Page-builder blocks
|
|
17
|
+
|
|
18
|
+
**A block is `defineBlock` + a registry entry.** Define the schema with
|
|
19
|
+
`defineBlock` (`lib/integrations/sanity/schemas/shared/block.ts`), never
|
|
20
|
+
`defineType` — it implies `type: "document"` and appends **Anchor ID** and
|
|
21
|
+
**Presentation Mode** for you. Register the component in
|
|
22
|
+
`components/page-builder/registry.tsx`; the renderer is driven by that map, so
|
|
23
|
+
there is no switch to extend.
|
|
24
|
+
|
|
25
|
+
Both halves are enforced, not documented: the registry's key set is typed as
|
|
26
|
+
exactly `PageBuilderDocumentType`, so a block registered in
|
|
27
|
+
`page-builder-config.ts` without an entry there — or an entry there for a block
|
|
28
|
+
that was never registered — is a typecheck error.
|
|
29
|
+
|
|
30
|
+
**Every block declares two orthogonal axes** in `page-builder-config.ts`:
|
|
31
|
+
`pageTypes` (which page documents may host it — drives validation) and
|
|
32
|
+
`category` (what the block structurally is, one of `componentCategories` —
|
|
33
|
+
drives the insert menu; `misc` only when nothing else fits). Never collapse them
|
|
34
|
+
into one field: that is what made the menu a single flat list.
|
|
35
|
+
|
|
36
|
+
**And a preview image.** The insert menu renders one tile per block from a
|
|
37
|
+
`studioPreview` document (Components → Component Previews) keyed
|
|
38
|
+
`block.<memberName>`. A block without one falls back to its schema icon, which
|
|
39
|
+
is what made the old menu unreadable.
|
|
40
|
+
|
|
41
|
+
**Never hardcode vertical padding in a block component.** The renderer wraps
|
|
42
|
+
each block in `<Section>` (`components/page-builder/section.tsx`), which owns
|
|
43
|
+
the `<section>` element, the anchor `id`, `scroll-mt` and the responsive padding
|
|
44
|
+
top/bottom. A block component returns its content only — no outer `<section>`,
|
|
45
|
+
no `py-*` / `pt-*` / `pb-*` on its root. Internal gaps between a block's own
|
|
46
|
+
parts are fine.
|
|
47
|
+
|
|
48
|
+
**Never hardcode px for section spacing either.** The scale lives in the Studio
|
|
49
|
+
at **Layout → Spacing Scale**, where editors add or rename steps and mark one as
|
|
50
|
+
the default, and it reaches the page as CSS custom properties. `Section` only
|
|
51
|
+
references `var(--section-space-<key>, var(--section-space-default, 2.5rem))`.
|
|
52
|
+
|
|
53
|
+
**An empty Anchor ID is generated from the block's title** (`headline` /
|
|
54
|
+
`heading` / `teaserHeading` / `title` / `label` — never `internalTitle`, which is
|
|
55
|
+
Studio-only) and de-duplicated per page. Name your block's headline field one of
|
|
56
|
+
those and anchor links work for free.
|
|
57
|
+
|
|
58
|
+
Declare `isEmpty` in the registry when a block can be left unfilled — the
|
|
59
|
+
renderer then skips the block *and* its `<Section>`, so it leaves no padded gap
|
|
60
|
+
and claims no anchor id. Don't early-return `null` from the component instead;
|
|
61
|
+
the renderer has to know before it wraps anything.
|
|
62
|
+
|
|
63
|
+
Full recipe and a checklist for a new block: the `page-builder-block` skill, or
|
|
64
|
+
the "Page-builder blocks" section of the README.
|
|
65
|
+
|
|
66
|
+
## Sanity
|
|
67
|
+
|
|
68
|
+
- Everything Sanity lives in `lib/integrations/sanity/`.
|
|
69
|
+
- Schemas use `defineType` / `defineField` / `defineArrayMember` (blocks use
|
|
70
|
+
`defineBlock`). GROQ queries are `SCREAMING_SNAKE_CASE` and wrapped in
|
|
71
|
+
`defineQuery`.
|
|
72
|
+
- **Regenerate types after any schema or query change:** `bun sanity:typegen`,
|
|
73
|
+
and commit the result in the same PR. Never hand-edit `sanity.types.ts`.
|
|
74
|
+
- The page query spreads `...` for non-gated blocks, so new block fields flow
|
|
75
|
+
through on their own — except `gatedContent`, whose GROQ branch lists fields
|
|
76
|
+
explicitly in `lib/integrations/sanity/queries.ts`. Add fields there by hand.
|
|
77
|
+
- Read through `sanityFetch` / `fetchSanity`, not a bare client, so the
|
|
78
|
+
draft-mode perspective and cache tags are applied.
|
|
79
|
+
- Publishing a document must invalidate what renders it. Layout singletons are
|
|
80
|
+
listed in `SANITY_LAYOUT_TAGS`; a new one that isn't there serves stale.
|
|
81
|
+
- Add `data-sanity` attributes where visual editing should reach.
|
|
82
|
+
- Nested layouts (e.g. `app/studio/layout.tsx`) must NOT render `<html>` or
|
|
83
|
+
`<body>` — the root layout already provides them.
|
|
84
|
+
|
|
85
|
+
## React and Next.js
|
|
86
|
+
|
|
87
|
+
- **React Compiler is on.** Do not write `useMemo`, `useCallback` or
|
|
88
|
+
`React.memo`. The one exception is `useRef` to instantiate an object once:
|
|
89
|
+
|
|
90
|
+
```tsx
|
|
91
|
+
const instanceRef = useRef<SomeClass | null>(null)
|
|
92
|
+
if (!instanceRef.current) instanceRef.current = new SomeClass()
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
- React 19: refs are regular props, so no `forwardRef` (a lint rule enforces it).
|
|
96
|
+
- **Never call `headers()`, `cookies()`, `draftMode()` or read `searchParams` in
|
|
97
|
+
a root layout** — it takes the whole page out of the prerender. Detect the
|
|
98
|
+
route with `useSelectedLayoutSegment()` in a `"use client"` wrapper, and put
|
|
99
|
+
draft-mode / visual-editing behaviour in a child component with its own
|
|
100
|
+
`<Suspense>` boundary.
|
|
101
|
+
- Use `@/components/ui/image` rather than `next/image` directly, and
|
|
102
|
+
`@/components/ui/sanity-image` for Sanity assets.
|
|
103
|
+
- Compose classNames with `cn` from `@/lib/styles/cn`.
|
|
104
|
+
- Import with the `@/` alias; relative parent imports are a lint error.
|
|
105
|
+
- Gate debug UI behind `process.env.NODE_ENV === "development"` — it is not
|
|
106
|
+
stripped in production otherwise.
|
|
107
|
+
|
|
108
|
+
## House rules
|
|
109
|
+
|
|
110
|
+
- **Bun only.** `bun install`, `bun run <script>`, `bunx`. Never npm, pnpm or
|
|
111
|
+
yarn — a second lockfile next to `bun.lock` is how the versions drift.
|
|
112
|
+
- **Don't add a dependency without asking.** Check `bun info <package>` and the
|
|
113
|
+
current docs first.
|
|
114
|
+
<!-- bsmnt:if dotenvx -->
|
|
115
|
+
- **Secrets are encrypted at rest with dotenvx.** Never put a real secret in
|
|
116
|
+
plaintext in a committed env file — gitleaks blocks the commit. Use
|
|
117
|
+
`bun env:set KEY value` (`--plain` for trivial config and `NEXT_PUBLIC_*`).
|
|
118
|
+
The `manage-env` skill has the full procedure.
|
|
119
|
+
<!-- bsmnt:endif -->
|
|
120
|
+
- Don't leave commented-out code or `TODO`s behind; write the code so it reads
|
|
121
|
+
without them.
|
|
122
|
+
- **Verify before claiming done:**
|
|
123
|
+
|
|
124
|
+
```bash
|
|
125
|
+
bun sanity:typegen && bun typecheck && bun lint && bun run build && bun knip
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
(`bun run build`, not `bun build` — the bare form is Bun's bundler.) Say what
|
|
129
|
+
you ran and what it printed; don't report success you haven't seen.
|
|
@@ -12,9 +12,79 @@ bun dev
|
|
|
12
12
|
|
|
13
13
|
- Single catch-all route `[[...slug]]` handles all pages
|
|
14
14
|
- Sanity page builder with composable content blocks
|
|
15
|
+
- Editor-owned vertical spacing scale and per-block anchor links
|
|
15
16
|
- Blog with categories, filtering, and pagination
|
|
16
17
|
- Visual editing and live preview support
|
|
17
18
|
|
|
19
|
+
## Page-builder blocks
|
|
20
|
+
|
|
21
|
+
A block is two halves — a schema document and a React component — and each half
|
|
22
|
+
has one place that makes it a block rather than an ordinary document or a loose
|
|
23
|
+
component. Use them and Anchor ID, Presentation Mode and the spacing scale come
|
|
24
|
+
for free.
|
|
25
|
+
|
|
26
|
+
**Define the schema with `defineBlock`, never `defineType`**
|
|
27
|
+
(`lib/integrations/sanity/schemas/shared/block.ts`). It implies
|
|
28
|
+
`type: "document"` and appends two fields to every block:
|
|
29
|
+
|
|
30
|
+
- **Anchor ID** — optional. Left empty, the anchor is generated from the block's
|
|
31
|
+
own title at render time (`headline` / `heading` / `teaserHeading` / `title` /
|
|
32
|
+
`label` — never `internalTitle`, which is Studio-only) and de-duplicated per
|
|
33
|
+
page, so two blocks with the same heading get `-2`. A value typed by hand
|
|
34
|
+
always wins, and the Studio shows the generated one as the placeholder. Link
|
|
35
|
+
to it as `/<page>#<anchor>`.
|
|
36
|
+
- **Presentation Mode** — padding top and bottom, each picked from the shared
|
|
37
|
+
spacing scale. No free-form px. A block that never touches it follows whichever
|
|
38
|
+
step is marked as the default.
|
|
39
|
+
|
|
40
|
+
Blocks that declare `groups` get a **Presentation** tab for both fields
|
|
41
|
+
automatically — don't list it in `groups` yourself.
|
|
42
|
+
|
|
43
|
+
**Register the component in `components/page-builder/registry.tsx`.** One entry
|
|
44
|
+
per block, keyed by schema type; the renderer is driven by that map, so there is
|
|
45
|
+
no switch to extend. `isEmpty` declares when a block has nothing to show — the
|
|
46
|
+
renderer then skips the block *and* its `<Section>`, so an unfilled block leaves
|
|
47
|
+
no padded gap behind and claims no anchor id. The registry's key set is typed as
|
|
48
|
+
exactly `PageBuilderDocumentType`, so a block registered in one place and not the
|
|
49
|
+
other is a typecheck error.
|
|
50
|
+
|
|
51
|
+
**Never hardcode vertical padding in a block component.** The renderer wraps each
|
|
52
|
+
block in `<Section>` (`components/page-builder/section.tsx`), which owns the
|
|
53
|
+
`<section>` element, the anchor `id`, `scroll-mt` and the padding. A block
|
|
54
|
+
component returns its content only — no outer `<section>`, no `py-*` / `pt-*` /
|
|
55
|
+
`pb-*` on its root. Internal gaps between a block's own parts are fine.
|
|
56
|
+
|
|
57
|
+
### The spacing scale is content, not code
|
|
58
|
+
|
|
59
|
+
Editors own it at **Layout → Spacing Scale**: a list of steps, each with a name
|
|
60
|
+
and px per breakpoint (mobile / tablet / desktop, empty reuses Desktop), one
|
|
61
|
+
marked as the default. Ticking a second step as default unticks the previous, so
|
|
62
|
+
there is always exactly one. Add a "Hero 120px" step and it shows up in every
|
|
63
|
+
block's dropdown, no deploy.
|
|
64
|
+
|
|
65
|
+
- Blocks store the step's `_key`, so renaming a step breaks nothing and a deleted
|
|
66
|
+
step degrades to the default instead of breaking the page.
|
|
67
|
+
- The list arrives seeded with the built-in five (XS 16 · SM 24 · **MD 32,
|
|
68
|
+
default** · LG 48 · XL 80, from `schemas/shared/spacing.ts`), which also apply
|
|
69
|
+
as the fallback if you empty it. Hit **"Add the built-in steps"** in the field
|
|
70
|
+
to get them back as editable rows.
|
|
71
|
+
- Values reach the page as CSS custom properties injected server-side by
|
|
72
|
+
`components/layout/spacing-scale/`, one `--section-space-<key>` per step plus
|
|
73
|
+
`--section-space-default`, re-declared inside the `768px` and `1440px` media
|
|
74
|
+
queries. `Section` only ever references
|
|
75
|
+
`var(--section-space-<key>, var(--section-space-default, 2.5rem))`, so an
|
|
76
|
+
unknown key degrades instead of breaking.
|
|
77
|
+
- `spacingSettings` is a layout singleton, so publishing it busts the layout
|
|
78
|
+
cache the same way navbar and footer do.
|
|
79
|
+
|
|
80
|
+
After a schema change run `bun sanity:typegen`. The page query spreads `...` for
|
|
81
|
+
non-gated blocks, so new fields flow through on their own — except
|
|
82
|
+
`gatedContent`, whose GROQ branch lists fields explicitly in
|
|
83
|
+
`lib/integrations/sanity/queries.ts`.
|
|
84
|
+
|
|
85
|
+
The bundled `page-builder-block` agent skill has the full recipe and a checklist
|
|
86
|
+
for adding a new block.
|
|
87
|
+
|
|
18
88
|
## Commands
|
|
19
89
|
|
|
20
90
|
- `bun dev` - Start dev server
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { JsonLd } from "@/components/layout/json-ld"
|
|
2
|
+
import { SpacingScale } from "@/components/layout/spacing-scale"
|
|
2
3
|
import { Wrapper } from "@/components/layout/wrapper"
|
|
3
4
|
import { SanityVisualEditing } from "@/components/sanity/visual-editing"
|
|
4
5
|
|
|
5
6
|
const Layout = ({ children }: { children: React.ReactNode }) => {
|
|
6
7
|
return (
|
|
7
8
|
<>
|
|
9
|
+
<SpacingScale />
|
|
8
10
|
<JsonLd />
|
|
9
11
|
<Wrapper>{children}</Wrapper>
|
|
10
12
|
<SanityVisualEditing />
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { getLayoutData } from "@/lib/integrations/sanity/fetchers/layout"
|
|
2
|
+
import {
|
|
3
|
+
resolveSpacingSteps,
|
|
4
|
+
spacingScaleCss,
|
|
5
|
+
} from "@/lib/integrations/sanity/schemas/shared/spacing"
|
|
6
|
+
|
|
7
|
+
export const SpacingScale = async () => {
|
|
8
|
+
const { spacingSettings } = await getLayoutData()
|
|
9
|
+
|
|
10
|
+
return (
|
|
11
|
+
<style
|
|
12
|
+
// biome-ignore lint/security/noDangerouslySetInnerHtml: css custom properties from the spacing singleton
|
|
13
|
+
dangerouslySetInnerHTML={{
|
|
14
|
+
__html: spacingScaleCss(resolveSpacingSteps(spacingSettings?.steps)),
|
|
15
|
+
}}
|
|
16
|
+
/>
|
|
17
|
+
)
|
|
18
|
+
}
|
package/src/templates/next-pagebuilder/components/page-builder/components/article-content/index.tsx
CHANGED
|
@@ -7,7 +7,7 @@ import { RelatedPostItem } from "./related-post-item"
|
|
|
7
7
|
export function ArticleContent({ block }: ArticleContentProps) {
|
|
8
8
|
const { author, body, categories, date, relatedPosts, thumbnail } = block
|
|
9
9
|
return (
|
|
10
|
-
<div
|
|
10
|
+
<div>
|
|
11
11
|
<header className="mx-auto max-w-3xl space-y-6">
|
|
12
12
|
{categories && categories.length > 0 ? (
|
|
13
13
|
<div className="flex flex-wrap gap-2">
|
|
@@ -239,11 +239,11 @@ async function CollectionResults({
|
|
|
239
239
|
// clickable before the grid resolves.
|
|
240
240
|
export function ContentCollection(props: ContentCollectionProps) {
|
|
241
241
|
return (
|
|
242
|
-
<
|
|
242
|
+
<div className="flex flex-col gap-8">
|
|
243
243
|
<CollectionToolbar pinnedCategory={props.pinnedCategory} />
|
|
244
244
|
<Suspense fallback={<CollectionResultsSkeleton />}>
|
|
245
245
|
<CollectionResults {...props} />
|
|
246
246
|
</Suspense>
|
|
247
|
-
</
|
|
247
|
+
</div>
|
|
248
248
|
)
|
|
249
249
|
}
|