bsmnt 0.11.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/files/components/ui/sanity-image/index.tsx +36 -4
- package/src/modules/features/cms/sanity/files/lib/integrations/sanity/components/rich-text.tsx +7 -1
- package/src/modules/features/cms/sanity-pagebuilder/files/components/sanity/rich-text.tsx +7 -1
- package/src/modules/features/cms/sanity-pagebuilder/files/components/ui/sanity-image/index.tsx +36 -4
- 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/page-builder-input.tsx +26 -7
- package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/components/page-builder-insert-menu.tsx +242 -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/components/use-studio-preview-urls.ts +62 -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/page-builder-config.ts +72 -34
- 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/index.ts +1 -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/components/studio-preview.ts +37 -0
- package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/index.ts +11 -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 +9 -0
- package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/structure.ts +5 -0
- package/src/modules/features/cms/sanity-pagebuilder-cache/files/components/sanity/rich-text.tsx +7 -1
- package/src/modules/features/cms/sanity-pagebuilder-cache/files/components/ui/sanity-image/index.tsx +36 -4
- 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/page-builder-input.tsx +26 -7
- package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/components/page-builder-insert-menu.tsx +242 -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/components/use-studio-preview-urls.ts +62 -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/page-builder-config.ts +72 -34
- 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/index.ts +1 -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/components/studio-preview.ts +37 -0
- package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/index.ts +11 -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 +9 -0
- package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/structure.ts +5 -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-default/.biome/plugins/no-fill-without-sizes.grit +11 -0
- package/src/templates/next-default/biome.json +2 -1
- package/src/templates/next-default/components/ui/image/index.tsx +20 -12
- package/src/templates/next-default/next.config.ts +10 -4
- package/src/templates/next-experiments/.biome/plugins/no-fill-without-sizes.grit +11 -0
- package/src/templates/next-experiments/biome.json +2 -1
- package/src/templates/next-experiments/components/ui/image/index.tsx +20 -12
- package/src/templates/next-experiments/next.config.ts +10 -4
- package/src/templates/next-pagebuilder/.biome/plugins/no-fill-without-sizes.grit +11 -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/biome.json +2 -1
- 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 +6 -1
- package/src/templates/next-pagebuilder/components/page-builder/components/article-content/related-post-item.tsx +1 -0
- 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/components/ui/image/index.tsx +20 -12
- package/src/templates/next-pagebuilder/lib/integrations/sanity/sanity.types.ts +92 -4
- package/src/templates/next-pagebuilder/next.config.ts +4 -3
- package/src/templates/next-pagebuilder-cache/.biome/plugins/no-fill-without-sizes.grit +11 -0
- 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/biome.json +2 -1
- 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 +6 -1
- package/src/templates/next-pagebuilder-cache/components/page-builder/components/article-content/related-post-item.tsx +1 -0
- 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/components/ui/image/index.tsx +21 -12
- package/src/templates/next-pagebuilder-cache/lib/integrations/sanity/sanity.types.ts +92 -4
- package/src/templates/next-pagebuilder-cache/lib/utils/image-sizes.ts +72 -0
- package/src/templates/next-pagebuilder-cache/next.config.ts +4 -3
- package/src/templates/next-webgl/.biome/plugins/no-fill-without-sizes.grit +11 -0
- package/src/templates/next-webgl/biome.json +2 -1
- package/src/templates/next-webgl/components/ui/image/index.tsx +20 -12
- package/src/templates/next-webgl/next.config.ts +10 -4
package/src/modules/features/consent/ketch/files/lib/integrations/ketch/preferences-button.tsx
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import { cva, type VariantProps } from "class-variance-authority"
|
|
4
|
+
import type { ComponentPropsWithoutRef } from "react"
|
|
5
|
+
import { isKetchConfigured } from "@/lib/integrations/check-integration"
|
|
6
|
+
import { cn } from "@/lib/styles/cn"
|
|
7
|
+
import { useConsent } from "./use-consent"
|
|
8
|
+
|
|
9
|
+
const preferencesButton = cva("w-fit text-left transition-colors", {
|
|
10
|
+
variants: {
|
|
11
|
+
variant: {
|
|
12
|
+
footer: "text-secondary/60 hover:text-secondary",
|
|
13
|
+
strong: "text-secondary hover:text-secondary/60",
|
|
14
|
+
link: "text-secondary underline underline-offset-4 hover:no-underline",
|
|
15
|
+
},
|
|
16
|
+
size: {
|
|
17
|
+
sm: "text-sm",
|
|
18
|
+
md: "text-base",
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
defaultVariants: {
|
|
22
|
+
variant: "footer",
|
|
23
|
+
size: "md",
|
|
24
|
+
},
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
type ConsentPreferencesButtonProps = ComponentPropsWithoutRef<"button"> &
|
|
28
|
+
VariantProps<typeof preferencesButton>
|
|
29
|
+
|
|
30
|
+
export const ConsentPreferencesButton = ({
|
|
31
|
+
className,
|
|
32
|
+
variant,
|
|
33
|
+
size,
|
|
34
|
+
children,
|
|
35
|
+
...props
|
|
36
|
+
}: ConsentPreferencesButtonProps) => {
|
|
37
|
+
const { openPreferences } = useConsent()
|
|
38
|
+
|
|
39
|
+
if (!isKetchConfigured()) return null
|
|
40
|
+
|
|
41
|
+
return (
|
|
42
|
+
<button
|
|
43
|
+
type="button"
|
|
44
|
+
onClick={openPreferences}
|
|
45
|
+
className={cn(preferencesButton({ variant, size }), className)}
|
|
46
|
+
{...props}
|
|
47
|
+
>
|
|
48
|
+
{children ?? "Privacy Choices"}
|
|
49
|
+
</button>
|
|
50
|
+
)
|
|
51
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export type KetchPurposeValue = boolean | "true" | "false" | undefined
|
|
2
|
+
|
|
3
|
+
export type KetchConsent = {
|
|
4
|
+
purposes?: Record<string, KetchPurposeValue>
|
|
5
|
+
vendors?: string[]
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export type KetchJurisdictionEvent = string | { code?: string } | undefined
|
|
9
|
+
|
|
10
|
+
export type KetchSemaphore = unknown[] & { loaded?: boolean }
|
|
11
|
+
|
|
12
|
+
export type KetchWindow = Window & {
|
|
13
|
+
ketch?: (...command: unknown[]) => void
|
|
14
|
+
semaphore?: KetchSemaphore
|
|
15
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import { useCallback, useSyncExternalStore } from "react"
|
|
4
|
+
import type { ConsentCategory } from "./categories"
|
|
5
|
+
import { consentStore } from "./consent-store"
|
|
6
|
+
|
|
7
|
+
export const useConsent = () => {
|
|
8
|
+
const { isResolved, jurisdiction, granted } = useSyncExternalStore(
|
|
9
|
+
consentStore.subscribe,
|
|
10
|
+
consentStore.getSnapshot,
|
|
11
|
+
consentStore.getServerSnapshot
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
const hasConsent = useCallback(
|
|
15
|
+
(category: ConsentCategory) => granted[category],
|
|
16
|
+
[granted]
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
return {
|
|
20
|
+
hasConsent,
|
|
21
|
+
granted,
|
|
22
|
+
isResolved,
|
|
23
|
+
jurisdiction,
|
|
24
|
+
openPreferences: consentStore.openPreferences,
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -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,11 @@
|
|
|
1
|
+
language js
|
|
2
|
+
|
|
3
|
+
or {
|
|
4
|
+
`<Image $attrs />`,
|
|
5
|
+
`<SanityImage $attrs />`
|
|
6
|
+
} as $image where {
|
|
7
|
+
$attrs <: contains `fill`,
|
|
8
|
+
! $attrs <: contains `fill={false}`,
|
|
9
|
+
! $attrs <: contains or { `sizes`, `mobileSize`, `desktopSize` },
|
|
10
|
+
register_diagnostic(span=$image, message="A `fill` image has no intrinsic width, so without `sizes` the browser assumes 100vw and requests the largest candidate. Pass `sizes`, or `mobileSize`/`desktopSize`.", severity="error")
|
|
11
|
+
}
|
|
@@ -234,7 +234,8 @@
|
|
|
234
234
|
"plugins": [
|
|
235
235
|
"./.biome/plugins/no-anchor-element.grit",
|
|
236
236
|
"./.biome/plugins/no-unnecessary-forwardref.grit",
|
|
237
|
-
"./.biome/plugins/no-relative-parent-imports.grit"
|
|
237
|
+
"./.biome/plugins/no-relative-parent-imports.grit",
|
|
238
|
+
"./.biome/plugins/no-fill-without-sizes.grit"
|
|
238
239
|
],
|
|
239
240
|
"vcs": {
|
|
240
241
|
"clientKind": "git",
|
|
@@ -98,7 +98,6 @@ const getFinalPlaceholder = (
|
|
|
98
98
|
* Enhanced Image component with responsive sizing and automatic optimizations.
|
|
99
99
|
*
|
|
100
100
|
* Always use this component instead of next/image directly. Provides:
|
|
101
|
-
* - Automatic responsive sizes generation
|
|
102
101
|
* - Smart blur placeholders with aspect ratio support
|
|
103
102
|
* - Performance optimizations (lazy loading by default)
|
|
104
103
|
* - Priority flag for LCP images
|
|
@@ -147,14 +146,14 @@ export function Image({
|
|
|
147
146
|
style,
|
|
148
147
|
className,
|
|
149
148
|
objectFit = "cover",
|
|
150
|
-
quality =
|
|
149
|
+
quality = 75,
|
|
151
150
|
alt = "",
|
|
152
151
|
fill,
|
|
153
152
|
block = !fill,
|
|
154
|
-
width
|
|
155
|
-
height
|
|
156
|
-
mobileSize
|
|
157
|
-
desktopSize
|
|
153
|
+
width,
|
|
154
|
+
height,
|
|
155
|
+
mobileSize,
|
|
156
|
+
desktopSize,
|
|
158
157
|
sizes,
|
|
159
158
|
src,
|
|
160
159
|
unoptimized,
|
|
@@ -164,9 +163,18 @@ export function Image({
|
|
|
164
163
|
priority = false,
|
|
165
164
|
...props
|
|
166
165
|
}: ImageProps) {
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
166
|
+
const responsiveSizes =
|
|
167
|
+
mobileSize || desktopSize
|
|
168
|
+
? buildImageSizes({
|
|
169
|
+
base: mobileSize ?? "100vw",
|
|
170
|
+
desktop: desktopSize ?? mobileSize ?? "100vw",
|
|
171
|
+
})
|
|
172
|
+
: undefined
|
|
173
|
+
|
|
174
|
+
const hasIntrinsicSize = width !== undefined && height !== undefined
|
|
175
|
+
const finalSizes = sizes ?? responsiveSizes ?? (hasIntrinsicSize ? undefined : "100vw")
|
|
176
|
+
const finalWidth = width ?? (block ? 1 : undefined)
|
|
177
|
+
const finalHeight = height ?? (block ? 1 : undefined)
|
|
170
178
|
|
|
171
179
|
// Early return after hooks
|
|
172
180
|
if (!src) return null
|
|
@@ -194,8 +202,8 @@ export function Image({
|
|
|
194
202
|
<NextImage
|
|
195
203
|
ref={ref}
|
|
196
204
|
fill={!block}
|
|
197
|
-
{...(
|
|
198
|
-
{...(
|
|
205
|
+
{...(finalWidth !== undefined && { width: finalWidth })}
|
|
206
|
+
{...(finalHeight !== undefined && { height: finalHeight })}
|
|
199
207
|
priority={priority}
|
|
200
208
|
quality={quality}
|
|
201
209
|
alt={alt}
|
|
@@ -204,7 +212,7 @@ export function Image({
|
|
|
204
212
|
...style,
|
|
205
213
|
}}
|
|
206
214
|
className={cn(className, block && "block w-full")}
|
|
207
|
-
|
|
215
|
+
{...(finalSizes && { sizes: finalSizes })}
|
|
208
216
|
src={src}
|
|
209
217
|
unoptimized={unoptimized || isSvg}
|
|
210
218
|
draggable={false}
|
|
@@ -77,12 +77,18 @@ const nextConfig: NextConfig = {
|
|
|
77
77
|
],
|
|
78
78
|
},
|
|
79
79
|
images: {
|
|
80
|
-
|
|
81
|
-
|
|
80
|
+
remotePatterns: [
|
|
81
|
+
{
|
|
82
|
+
protocol: "https",
|
|
83
|
+
hostname: "cdn.sanity.io",
|
|
84
|
+
},
|
|
85
|
+
],
|
|
82
86
|
minimumCacheTTL: 60 * 60 * 24 * 30, // 30 days
|
|
83
|
-
qualities: [90],
|
|
87
|
+
qualities: [75, 90, 100],
|
|
84
88
|
formats: ["image/avif", "image/webp"],
|
|
85
|
-
deviceSizes: [
|
|
89
|
+
deviceSizes: [
|
|
90
|
+
640, 750, 828, 1080, 1200, 1320, 1620, 1920, 2048, 2560, 3024, 3456, 3840,
|
|
91
|
+
],
|
|
86
92
|
imageSizes: [16, 32, 48, 64, 96, 128, 256, 384],
|
|
87
93
|
},
|
|
88
94
|
headers: async () => [
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
language js
|
|
2
|
+
|
|
3
|
+
or {
|
|
4
|
+
`<Image $attrs />`,
|
|
5
|
+
`<SanityImage $attrs />`
|
|
6
|
+
} as $image where {
|
|
7
|
+
$attrs <: contains `fill`,
|
|
8
|
+
! $attrs <: contains `fill={false}`,
|
|
9
|
+
! $attrs <: contains or { `sizes`, `mobileSize`, `desktopSize` },
|
|
10
|
+
register_diagnostic(span=$image, message="A `fill` image has no intrinsic width, so without `sizes` the browser assumes 100vw and requests the largest candidate. Pass `sizes`, or `mobileSize`/`desktopSize`.", severity="error")
|
|
11
|
+
}
|
|
@@ -229,7 +229,8 @@
|
|
|
229
229
|
"plugins": [
|
|
230
230
|
"./.biome/plugins/no-anchor-element.grit",
|
|
231
231
|
"./.biome/plugins/no-unnecessary-forwardref.grit",
|
|
232
|
-
"./.biome/plugins/no-relative-parent-imports.grit"
|
|
232
|
+
"./.biome/plugins/no-relative-parent-imports.grit",
|
|
233
|
+
"./.biome/plugins/no-fill-without-sizes.grit"
|
|
233
234
|
],
|
|
234
235
|
"vcs": {
|
|
235
236
|
"clientKind": "git",
|