bsmnt 0.13.1 → 0.15.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/core/create/internal-ids.d.ts +1 -0
- package/dist/core/create/internal-ids.d.ts.map +1 -1
- package/dist/core/create/internal-ids.js +1 -0
- package/dist/core/create/internal-ids.js.map +1 -1
- package/dist/domain/skills.d.ts +5 -0
- package/dist/domain/skills.d.ts.map +1 -1
- package/dist/domain/skills.js +9 -1
- package/dist/domain/skills.js.map +1 -1
- package/dist/infrastructure/create/format-project.d.ts +9 -0
- package/dist/infrastructure/create/format-project.d.ts.map +1 -0
- package/dist/infrastructure/create/format-project.js +21 -0
- package/dist/infrastructure/create/format-project.js.map +1 -0
- package/dist/infrastructure/create/lifecycle-registry.d.ts.map +1 -1
- package/dist/infrastructure/create/lifecycle-registry.js +5 -0
- package/dist/infrastructure/create/lifecycle-registry.js.map +1 -1
- package/dist/modules/features/cms/sanity-pagebuilder/config.d.ts.map +1 -1
- package/dist/modules/features/cms/sanity-pagebuilder/config.js +1 -0
- package/dist/modules/features/cms/sanity-pagebuilder/config.js.map +1 -1
- package/dist/modules/features/cms/sanity-pagebuilder/index.d.ts.map +1 -1
- package/dist/modules/features/cms/sanity-pagebuilder/index.js +1 -0
- package/dist/modules/features/cms/sanity-pagebuilder/index.js.map +1 -1
- package/dist/modules/features/cms/sanity-pagebuilder-cache/config.d.ts.map +1 -1
- package/dist/modules/features/cms/sanity-pagebuilder-cache/config.js +1 -0
- package/dist/modules/features/cms/sanity-pagebuilder-cache/config.js.map +1 -1
- package/dist/modules/features/cms/sanity-pagebuilder-cache/index.d.ts.map +1 -1
- package/dist/modules/features/cms/sanity-pagebuilder-cache/index.js +1 -0
- package/dist/modules/features/cms/sanity-pagebuilder-cache/index.js.map +1 -1
- package/package.json +1 -1
- package/src/agent-skills/ashby-pagebuilder-block/SKILL.md +29 -11
- package/src/agent-skills/hubspot-pagebuilder-block/SKILL.md +31 -12
- package/src/agent-skills/launch-ready/SKILL.md +114 -0
- package/src/agent-skills/launch-ready/references/a11y.md +74 -0
- package/src/agent-skills/launch-ready/references/content.md +65 -0
- package/src/agent-skills/launch-ready/references/crawlers.md +89 -0
- package/src/agent-skills/launch-ready/references/launch.md +44 -0
- package/src/agent-skills/launch-ready/references/metadata.md +83 -0
- package/src/agent-skills/launch-ready/references/performance.md +36 -0
- package/src/agent-skills/launch-ready/references/routing.md +47 -0
- package/src/agent-skills/launch-ready/references/security.md +39 -0
- package/src/agent-skills/page-builder-block/SKILL.md +26 -13
- package/src/modules/features/cms/sanity/files/app/layout.tsx +2 -2
- package/src/modules/features/cms/sanity/files/lib/utils/metadata.ts +2 -2
- package/src/modules/features/cms/sanity/files/lib/utils/url.ts +6 -3
- package/src/modules/features/cms/sanity-pagebuilder/files/app/api/revalidate/route.ts +2 -2
- package/src/modules/features/cms/sanity-pagebuilder/files/components/sanity/rich-text.tsx +4 -2
- package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/components/block-actions-item.tsx +2 -2
- package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/components/component-preview-frame.tsx +124 -0
- package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/components/component-preview-pane.tsx +25 -0
- package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/components/page-builder-input.tsx +3 -26
- package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/components/page-builder-insert-menu.tsx +121 -93
- package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/components/redirect-import-dialog.tsx +538 -0
- package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/csv.ts +88 -0
- package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/fetchers/breadcrumbs.ts +22 -0
- package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/icons.ts +2 -2
- package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/page-builder-config.ts +36 -49
- package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/presentation.ts +1 -50
- package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/queries.ts +76 -64
- package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/redirect-import.ts +651 -0
- package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/sanity.config.ts +6 -8
- package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/components/index.ts +0 -1
- package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/index.ts +1 -4
- package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/layout/redirect.ts +9 -6
- package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/layout/structured-data.ts +8 -0
- package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/shared/block.ts +4 -4
- package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/shared/metadata.ts +60 -6
- package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/shared/page-builder.ts +4 -18
- package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/shared/page.ts +33 -38
- package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/shared/richText.ts +0 -1
- package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/singletons.ts +0 -16
- package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/structure.ts +52 -35
- package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/tags.ts +5 -2
- package/src/modules/features/cms/sanity-pagebuilder/files/lib/scripts/sanity-typegen.ts +8 -11
- package/src/modules/features/cms/sanity-pagebuilder/files/lib/utils/metadata.ts +109 -136
- package/src/modules/features/cms/sanity-pagebuilder/files/lib/utils/studio-embed.ts +22 -0
- package/src/modules/features/cms/sanity-pagebuilder-cache/files/app/api/revalidate/route.ts +2 -2
- package/src/modules/features/cms/sanity-pagebuilder-cache/files/components/sanity/rich-text.tsx +4 -2
- package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/components/block-actions-item.tsx +2 -2
- package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/components/component-preview-frame.tsx +124 -0
- package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/components/component-preview-pane.tsx +25 -0
- package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/components/page-builder-input.tsx +3 -26
- package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/components/page-builder-insert-menu.tsx +121 -93
- package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/components/redirect-import-dialog.tsx +538 -0
- package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/csv.ts +88 -0
- package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/fetchers/breadcrumbs.ts +25 -0
- package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/icons.ts +2 -2
- package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/page-builder-config.ts +36 -49
- package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/presentation.ts +1 -50
- package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/queries.ts +76 -64
- package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/redirect-import.ts +651 -0
- package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/sanity.config.ts +6 -8
- package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/components/index.ts +0 -1
- package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/index.ts +1 -4
- package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/layout/redirect.ts +9 -6
- package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/layout/structured-data.ts +8 -0
- package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/shared/block.ts +4 -4
- package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/shared/metadata.ts +60 -6
- package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/shared/page-builder.ts +4 -18
- package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/shared/page.ts +33 -38
- package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/shared/richText.ts +0 -1
- package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/singletons.ts +0 -16
- package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/structure.ts +52 -35
- package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/tags.ts +6 -2
- package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/scripts/sanity-typegen.ts +8 -11
- package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/utils/metadata.ts +109 -136
- package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/utils/studio-embed.ts +22 -0
- package/src/modules/features/forms/hubspot/files/_sanity-adapter/form-block.ts +4 -10
- package/src/modules/features/forms/hubspot/files/lib/integrations/hubspot/README.md +10 -5
- package/src/modules/features/jobs/ashby/files/_sanity-adapter/job-board-block.ts +0 -7
- package/src/modules/features/jobs/ashby/files/lib/integrations/ashby/README.md +11 -5
- package/src/templates/next-default/.env.example +3 -1
- package/src/templates/next-default/app/layout.tsx +2 -1
- package/src/templates/next-default/app/robots.ts +9 -3
- package/src/templates/next-default/lib/utils/metadata.ts +2 -1
- package/src/templates/next-default/lib/utils/url.ts +20 -3
- package/src/templates/next-default/next.config.ts +16 -0
- package/src/templates/next-experiments/.env.example +3 -1
- package/src/templates/next-experiments/app/layout.tsx +2 -1
- package/src/templates/next-experiments/app/robots.ts +9 -3
- package/src/templates/next-experiments/lib/utils/metadata.ts +2 -1
- package/src/templates/next-experiments/lib/utils/url.ts +20 -3
- package/src/templates/next-experiments/next.config.ts +16 -0
- package/src/templates/next-pagebuilder/.env.example +3 -1
- package/src/templates/next-pagebuilder/.github/workflows/ci.yml +32 -8
- package/src/templates/next-pagebuilder/AGENTS.md +22 -7
- package/src/templates/next-pagebuilder/README.md +15 -3
- package/src/templates/next-pagebuilder/app/(content)/[[...slug]]/page.tsx +45 -7
- package/src/templates/next-pagebuilder/app/actions/unlock-gated-content.ts +5 -4
- package/src/templates/next-pagebuilder/app/components/page.tsx +81 -0
- package/src/templates/next-pagebuilder/app/layout.tsx +3 -41
- package/src/templates/next-pagebuilder/app/robots.ts +9 -3
- package/src/templates/next-pagebuilder/biome.json +2 -1
- package/src/templates/next-pagebuilder/components/layout/footer/index.tsx +1 -1
- package/src/templates/next-pagebuilder/components/page-builder/components/gated-content/index.tsx +8 -3
- package/src/templates/next-pagebuilder/components/page-builder/library.ts +183 -0
- package/src/templates/next-pagebuilder/components/page-builder/registry.tsx +4 -1
- package/src/templates/next-pagebuilder/components/page-builder/renderer.tsx +3 -1
- package/src/templates/next-pagebuilder/components/page-builder/stage.tsx +10 -0
- package/src/templates/next-pagebuilder/components/page-document/index.tsx +28 -35
- package/src/templates/next-pagebuilder/lefthook.yml +1 -1
- package/src/templates/next-pagebuilder/lib/integrations/sanity/sanity.types.ts +170 -299
- package/src/templates/next-pagebuilder/lib/utils/base-url.ts +18 -3
- package/src/templates/next-pagebuilder/lib/utils/breadcrumbs.ts +70 -0
- package/src/templates/next-pagebuilder/next.config.ts +16 -0
- package/src/templates/next-pagebuilder/package.json +1 -1
- package/src/templates/next-pagebuilder-cache/.env.example +3 -1
- package/src/templates/next-pagebuilder-cache/.github/workflows/ci.yml +32 -8
- package/src/templates/next-pagebuilder-cache/AGENTS.md +22 -7
- package/src/templates/next-pagebuilder-cache/README.md +15 -3
- package/src/templates/next-pagebuilder-cache/app/(content)/[[...slug]]/page.tsx +46 -7
- package/src/templates/next-pagebuilder-cache/app/actions/unlock-gated-content.ts +5 -4
- package/src/templates/next-pagebuilder-cache/app/components/page.tsx +91 -0
- package/src/templates/next-pagebuilder-cache/app/layout.tsx +5 -46
- package/src/templates/next-pagebuilder-cache/app/robots.ts +9 -3
- package/src/templates/next-pagebuilder-cache/biome.json +2 -1
- package/src/templates/next-pagebuilder-cache/components/layout/footer/index.tsx +1 -1
- package/src/templates/next-pagebuilder-cache/components/page-builder/components/gated-content/index.tsx +8 -3
- package/src/templates/next-pagebuilder-cache/components/page-builder/library.ts +183 -0
- package/src/templates/next-pagebuilder-cache/components/page-builder/registry.tsx +4 -1
- package/src/templates/next-pagebuilder-cache/components/page-builder/renderer.tsx +3 -1
- package/src/templates/next-pagebuilder-cache/components/page-builder/stage.tsx +10 -0
- package/src/templates/next-pagebuilder-cache/components/page-document/index.tsx +28 -35
- package/src/templates/next-pagebuilder-cache/lefthook.yml +1 -1
- package/src/templates/next-pagebuilder-cache/lib/integrations/sanity/sanity.types.ts +170 -299
- package/src/templates/next-pagebuilder-cache/lib/utils/base-url.ts +18 -3
- package/src/templates/next-pagebuilder-cache/lib/utils/breadcrumbs.ts +70 -0
- package/src/templates/next-pagebuilder-cache/next.config.ts +16 -0
- package/src/templates/next-pagebuilder-cache/package.json +1 -1
- package/src/templates/next-webgl/.env.example +3 -1
- package/src/templates/next-webgl/app/layout.tsx +2 -1
- package/src/templates/next-webgl/app/robots.ts +9 -3
- package/src/templates/next-webgl/lib/utils/metadata.ts +2 -1
- package/src/templates/next-webgl/lib/utils/url.ts +20 -3
- package/src/templates/next-webgl/next.config.ts +16 -0
- package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/components/use-studio-preview-urls.ts +0 -62
- package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/components/studio-preview.ts +0 -37
- package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/components/use-studio-preview-urls.ts +0 -62
- package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/components/studio-preview.ts +0 -37
|
@@ -3,15 +3,30 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Priority:
|
|
5
5
|
* 1. NEXT_PUBLIC_BASE_URL (explicit production URL)
|
|
6
|
-
* 2. VERCEL_PROJECT_PRODUCTION_URL
|
|
7
|
-
* 3. VERCEL_URL (
|
|
6
|
+
* 2. VERCEL_PROJECT_PRODUCTION_URL, on production deployments only
|
|
7
|
+
* 3. VERCEL_URL (the deployment's own URL, so previews never emit production URLs)
|
|
8
8
|
* 4. http://localhost:3000 (local development)
|
|
9
9
|
*/
|
|
10
10
|
export const getBaseUrl = () =>
|
|
11
11
|
process.env.NEXT_PUBLIC_BASE_URL ||
|
|
12
|
-
(process.env.
|
|
12
|
+
(process.env.VERCEL_ENV === "production" &&
|
|
13
|
+
process.env.VERCEL_PROJECT_PRODUCTION_URL
|
|
13
14
|
? `https://${process.env.VERCEL_PROJECT_PRODUCTION_URL}`
|
|
14
15
|
: undefined) ||
|
|
15
16
|
(process.env.VERCEL_URL
|
|
16
17
|
? `https://${process.env.VERCEL_URL}`
|
|
17
18
|
: "http://localhost:3000")
|
|
19
|
+
|
|
20
|
+
// The production build also answers on *.vercel.app; only the launch domain is indexed.
|
|
21
|
+
export const getIndexableHost = () => {
|
|
22
|
+
const baseUrl = process.env.NEXT_PUBLIC_BASE_URL
|
|
23
|
+
// Off Vercel VERCEL_ENV is unset, and the configured host is the launch domain.
|
|
24
|
+
const vercelEnv = process.env.VERCEL_ENV
|
|
25
|
+
if (!baseUrl || (vercelEnv && vercelEnv !== "production")) return null
|
|
26
|
+
return URL.canParse(baseUrl) ? new URL(baseUrl).host : null
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export const isIndexableHost = (host: string | null | undefined) => {
|
|
30
|
+
const indexable = getIndexableHost()
|
|
31
|
+
return indexable !== null && host?.toLowerCase() === indexable
|
|
32
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import type { BREADCRUMB_PAGES_QUERY_RESULT } from "@/lib/integrations/sanity/sanity.types"
|
|
2
|
+
|
|
3
|
+
type BreadcrumbPage = BREADCRUMB_PAGES_QUERY_RESULT[number]
|
|
4
|
+
|
|
5
|
+
type Breadcrumb = {
|
|
6
|
+
name: string
|
|
7
|
+
url: string
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const HOME_NAME = "Home"
|
|
11
|
+
|
|
12
|
+
const trimSlashes = (path: string) => path.replace(/^\/+|\/+$/g, "")
|
|
13
|
+
|
|
14
|
+
const firstNonEmpty = (...values: (string | null | undefined)[]) =>
|
|
15
|
+
values.find((value) => value?.trim())?.trim()
|
|
16
|
+
|
|
17
|
+
const urlFor = (path: string) => (path ? `/${path}` : "/")
|
|
18
|
+
|
|
19
|
+
const labelFor = (page: BreadcrumbPage | undefined) =>
|
|
20
|
+
page && firstNonEmpty(page.shortTitle, page.folderTitle, page.title)
|
|
21
|
+
|
|
22
|
+
// Every parent path, not the path itself: "a/b/c" → ["a", "a/b"].
|
|
23
|
+
const ancestorPaths = (path: string) => {
|
|
24
|
+
const segments = path.split("/").filter(Boolean)
|
|
25
|
+
return segments
|
|
26
|
+
.slice(0, -1)
|
|
27
|
+
.map((_, index) => segments.slice(0, index + 1).join("/"))
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// The paths a page's crumbs are named from, besides the homepage: its
|
|
31
|
+
// ancestors and itself.
|
|
32
|
+
export const breadcrumbPaths = (path: string | null) => {
|
|
33
|
+
const current = trimSlashes(path ?? "")
|
|
34
|
+
return current ? [...ancestorPaths(current), current] : []
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Crumbs are named from the pages at each level, the current one included:
|
|
38
|
+
// Short Title, then the folder name for a folder's index page, then Title. A
|
|
39
|
+
// level with no page is skipped, so a crumb never links to a 404.
|
|
40
|
+
export const buildBreadcrumbs = ({
|
|
41
|
+
path,
|
|
42
|
+
name,
|
|
43
|
+
pages,
|
|
44
|
+
}: {
|
|
45
|
+
path: string | null
|
|
46
|
+
name: string
|
|
47
|
+
pages: readonly BreadcrumbPage[]
|
|
48
|
+
}): Breadcrumb[] => {
|
|
49
|
+
const pageAt = (target: string) =>
|
|
50
|
+
pages.find((page) => trimSlashes(page.path ?? "") === target)
|
|
51
|
+
|
|
52
|
+
const home = pageAt("")
|
|
53
|
+
const crumbs: Breadcrumb[] = [
|
|
54
|
+
{
|
|
55
|
+
name: firstNonEmpty(home?.shortTitle, home?.title) ?? HOME_NAME,
|
|
56
|
+
url: "/",
|
|
57
|
+
},
|
|
58
|
+
]
|
|
59
|
+
|
|
60
|
+
const current = trimSlashes(path ?? "")
|
|
61
|
+
if (!current) return crumbs
|
|
62
|
+
|
|
63
|
+
for (const ancestor of ancestorPaths(current)) {
|
|
64
|
+
const ancestorName = labelFor(pageAt(ancestor))
|
|
65
|
+
if (ancestorName) crumbs.push({ name: ancestorName, url: urlFor(ancestor) })
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
crumbs.push({ name: labelFor(pageAt(current)) ?? name, url: urlFor(current) })
|
|
69
|
+
return crumbs
|
|
70
|
+
}
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import withBundleAnalyzer from "@next/bundle-analyzer"
|
|
2
2
|
import type { NextConfig } from "next"
|
|
3
|
+
import { getIndexableHost } from "./lib/utils/base-url"
|
|
4
|
+
|
|
5
|
+
// Only the launch domain is indexed; every other host is sent noindex.
|
|
6
|
+
const indexableHost = getIndexableHost()
|
|
3
7
|
|
|
4
8
|
const nextConfig: NextConfig = {
|
|
5
9
|
reactStrictMode: true,
|
|
@@ -92,6 +96,18 @@ const nextConfig: NextConfig = {
|
|
|
92
96
|
imageSizes: [16, 32, 48, 64, 96, 128, 256, 384],
|
|
93
97
|
},
|
|
94
98
|
headers: async () => [
|
|
99
|
+
{
|
|
100
|
+
source: "/:path*",
|
|
101
|
+
...(indexableHost && {
|
|
102
|
+
missing: [
|
|
103
|
+
{
|
|
104
|
+
type: "host" as const,
|
|
105
|
+
value: indexableHost.replaceAll(".", "\\."),
|
|
106
|
+
},
|
|
107
|
+
],
|
|
108
|
+
}),
|
|
109
|
+
headers: [{ key: "X-Robots-Tag", value: "noindex" }],
|
|
110
|
+
},
|
|
95
111
|
{
|
|
96
112
|
// Excludes /studio* — must stay mutually exclusive with the rule below.
|
|
97
113
|
source: "/((?!studio(?:/|$)).*)",
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
"knip": "knip",
|
|
18
18
|
"knip:fix": "knip --fix",
|
|
19
19
|
"lighthouse": "bunx @unlighthouse/cli --site http://localhost:3000",
|
|
20
|
-
"lint": "biome
|
|
20
|
+
"lint": "biome ci --error-on-warnings --max-diagnostics=200 .",
|
|
21
21
|
"lint:fix": "biome check --write --unsafe --max-diagnostics=200",
|
|
22
22
|
"prepare": "lefthook install",
|
|
23
23
|
"start": "next start",
|
|
@@ -26,7 +26,9 @@
|
|
|
26
26
|
|
|
27
27
|
# Base URL
|
|
28
28
|
# Inlined at build time — scope this to the Production environment only in
|
|
29
|
-
# Vercel so Preview deployments fall through to
|
|
29
|
+
# Vercel so Preview deployments fall through to their own VERCEL_URL. On a
|
|
30
|
+
# production deployment it is also the only host that gets indexed: every other
|
|
31
|
+
# host is sent `X-Robots-Tag: noindex` and a disallow-all robots.txt.
|
|
30
32
|
NEXT_PUBLIC_BASE_URL=
|
|
31
33
|
|
|
32
34
|
# Draft Mode
|
|
@@ -8,6 +8,13 @@ on:
|
|
|
8
8
|
branches:
|
|
9
9
|
- main
|
|
10
10
|
|
|
11
|
+
permissions:
|
|
12
|
+
contents: read
|
|
13
|
+
|
|
14
|
+
concurrency:
|
|
15
|
+
group: ci-${{ github.ref }}
|
|
16
|
+
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
|
|
17
|
+
|
|
11
18
|
jobs:
|
|
12
19
|
ci:
|
|
13
20
|
runs-on: ubuntu-latest
|
|
@@ -43,17 +50,33 @@ jobs:
|
|
|
43
50
|
- name: Type check
|
|
44
51
|
run: bun run typecheck
|
|
45
52
|
|
|
46
|
-
- name: Lint
|
|
53
|
+
- name: Lint and format
|
|
47
54
|
run: bun run lint
|
|
48
55
|
|
|
56
|
+
# Runs your tests if any exist; a fresh project with none passes cleanly.
|
|
57
|
+
- name: Test
|
|
58
|
+
run: |
|
|
59
|
+
if git ls-files '*.test.*' '*.spec.*' '*_test.*' '*_spec.*' | grep -q .; then
|
|
60
|
+
bun test
|
|
61
|
+
else
|
|
62
|
+
echo "No test files found — skipping bun test."
|
|
63
|
+
fi
|
|
64
|
+
|
|
49
65
|
# bsmnt:if dotenvx
|
|
50
66
|
# dotenvx needs the private key to decrypt secrets at build time. It's
|
|
51
67
|
# pulled from 1Password via an org-wide service-account token + the op-ref
|
|
52
68
|
# committed in .env. Optional: builds pass without it (only plaintext /
|
|
53
69
|
# NEXT_PUBLIC_* is read), and a per-repo DOTENV_PRIVATE_KEY secret still
|
|
54
70
|
# works as a fallback.
|
|
71
|
+
# The `secrets` context isn't available in step `if:`, so the check runs as its own step.
|
|
72
|
+
- name: Detect 1Password token
|
|
73
|
+
id: op
|
|
74
|
+
env:
|
|
75
|
+
OP_SERVICE_ACCOUNT_TOKEN: ${{ secrets.OP_SERVICE_ACCOUNT_TOKEN }}
|
|
76
|
+
run: '[ -n "$OP_SERVICE_ACCOUNT_TOKEN" ] && echo "enabled=true" >> "$GITHUB_OUTPUT" || true'
|
|
77
|
+
|
|
55
78
|
- name: Install 1Password CLI
|
|
56
|
-
if:
|
|
79
|
+
if: steps.op.outputs.enabled == 'true'
|
|
57
80
|
uses: 1password/install-cli-action@v1
|
|
58
81
|
|
|
59
82
|
- name: Build
|
|
@@ -92,13 +115,14 @@ jobs:
|
|
|
92
115
|
run: bun run build
|
|
93
116
|
|
|
94
117
|
# bsmnt:endif
|
|
95
|
-
#
|
|
96
|
-
- name:
|
|
118
|
+
# `prebuild` regenerated these; any change (or untracked file) means they were committed stale.
|
|
119
|
+
- name: Generated Sanity files are committed
|
|
97
120
|
run: |
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
echo "
|
|
121
|
+
cd lib/integrations/sanity
|
|
122
|
+
if [ -n "$(git status --porcelain -- sanity.types.ts schema.json)" ]; then
|
|
123
|
+
git status --short -- sanity.types.ts schema.json
|
|
124
|
+
echo "::error::Run \`bun sanity:typegen\` and commit the result."
|
|
125
|
+
exit 1
|
|
102
126
|
fi
|
|
103
127
|
|
|
104
128
|
- name: Knip
|
|
@@ -11,7 +11,9 @@ page.
|
|
|
11
11
|
Studio with a slug, not a directory. `app/(content)/[[...slug]]/page.tsx`
|
|
12
12
|
resolves every one of them; a hand-written route shadows that resolver and
|
|
13
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
|
|
14
|
+
`app/` are non-page endpoints — the Studio, API handlers, sitemap, feed, and
|
|
15
|
+
`app/components`, the Studio's live block preview: it renders fixtures rather
|
|
16
|
+
than content, is gated to Studio embeds by `isStudioEmbed()` and is `noindex`.
|
|
15
17
|
|
|
16
18
|
It runs with [Cache Components](https://nextjs.org/docs/app/api-reference/config/next-config-js/cacheComponents)
|
|
17
19
|
(`cacheComponents: true`), so every Sanity read sits behind a `'use cache'`
|
|
@@ -22,10 +24,18 @@ boundaries where they are unless you mean to move them.
|
|
|
22
24
|
|
|
23
25
|
## Page-builder blocks
|
|
24
26
|
|
|
27
|
+
**Blocks are inline objects, never documents.** A placement belongs to exactly
|
|
28
|
+
one page, so the page's `pageBuilder` array stores the block itself and nothing
|
|
29
|
+
is dereferenced at read time. Don't reintroduce a block-as-document with a
|
|
30
|
+
reference member — that model was removed deliberately: no block was ever
|
|
31
|
+
reused, and it cost a hop on every read plus a second id to address.
|
|
32
|
+
|
|
25
33
|
**A block is `defineBlock` + a registry entry.** Define the schema with
|
|
26
34
|
`defineBlock` (`lib/integrations/sanity/schemas/shared/block.ts`), never
|
|
27
|
-
`defineType` — it implies `type: "
|
|
28
|
-
**Presentation Mode** for you.
|
|
35
|
+
`defineType` — it implies `type: "object"` and appends **Anchor ID** and
|
|
36
|
+
**Presentation Mode** for you. The array member's type name *is* the block's
|
|
37
|
+
`_type`, so the registry, the insert menu and the fixture library all key off
|
|
38
|
+
the same string. Register the component in
|
|
29
39
|
`components/page-builder/registry.tsx`; the renderer is driven by that map, so
|
|
30
40
|
there is no switch to extend.
|
|
31
41
|
|
|
@@ -40,10 +50,12 @@ that was never registered — is a typecheck error.
|
|
|
40
50
|
drives the insert menu; `misc` only when nothing else fits). Never collapse them
|
|
41
51
|
into one field: that is what made the menu a single flat list.
|
|
42
52
|
|
|
43
|
-
**And a
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
53
|
+
**And a fixture.** A block isn't done until `components/page-builder/library.ts`
|
|
54
|
+
has an entry for it: a stand-in value typed as the block's query result, plus a
|
|
55
|
+
one-line note. That entry is the block's preview everywhere — the live pane in
|
|
56
|
+
the insert menu, its row in Studio → Components, and the `/components` route,
|
|
57
|
+
all rendered by the production renderer. Without one the picker just says the
|
|
58
|
+
component has no preview.
|
|
47
59
|
|
|
48
60
|
**Never hardcode vertical padding in a block component.** The renderer wraps
|
|
49
61
|
each block in `<Section>` (`components/page-builder/section.tsx`), which owns
|
|
@@ -134,3 +146,6 @@ the "Page-builder blocks" section of the README.
|
|
|
134
146
|
|
|
135
147
|
(`bun run build`, not `bun build` — the bare form is Bun's bundler.) Say what
|
|
136
148
|
you ran and what it printed; don't report success you haven't seen.
|
|
149
|
+
- **Before a commit or PR, or when told to ship, check the `launch-ready`
|
|
150
|
+
skill's rules against the diff.** Run all of them when asked whether the site
|
|
151
|
+
is ready to launch.
|
|
@@ -25,14 +25,18 @@ bun dev
|
|
|
25
25
|
|
|
26
26
|
## Page-builder blocks
|
|
27
27
|
|
|
28
|
-
A block is two halves — a schema
|
|
29
|
-
|
|
28
|
+
A block is two halves — a schema type and a React component — and each half has
|
|
29
|
+
one place that makes it a block rather than an ordinary object type or a loose
|
|
30
30
|
component. Use them and Anchor ID, Presentation Mode and the spacing scale come
|
|
31
31
|
for free.
|
|
32
32
|
|
|
33
|
+
Blocks are **inline objects** in the page's `pageBuilder` array, never documents
|
|
34
|
+
and never referenced: a placement belongs to exactly one page, so the block
|
|
35
|
+
lives on that page and nothing is dereferenced at read time.
|
|
36
|
+
|
|
33
37
|
**Define the schema with `defineBlock`, never `defineType`**
|
|
34
38
|
(`lib/integrations/sanity/schemas/shared/block.ts`). It implies
|
|
35
|
-
`type: "
|
|
39
|
+
`type: "object"` and appends two fields to every block:
|
|
36
40
|
|
|
37
41
|
- **Anchor ID** — optional. Left empty, the anchor is generated from the block's
|
|
38
42
|
own title at render time (`headline` / `heading` / `teaserHeading` / `title` /
|
|
@@ -61,6 +65,14 @@ block in `<Section>` (`components/page-builder/section.tsx`), which owns the
|
|
|
61
65
|
component returns its content only — no outer `<section>`, no `py-*` / `pt-*` /
|
|
62
66
|
`pb-*` on its root. Internal gaps between a block's own parts are fine.
|
|
63
67
|
|
|
68
|
+
**Every block ships a fixture in `components/page-builder/library.ts`**: a
|
|
69
|
+
stand-in value typed as the block's query result, plus a one-line note. It is
|
|
70
|
+
the block's only preview, and it is rendered by the production renderer at
|
|
71
|
+
`/components` — a route the Studio embeds in an iframe, so the insert menu and
|
|
72
|
+
**Components** in the Studio show the real block rather than a screenshot. The
|
|
73
|
+
route is gated to Studio embeds and `noindex`; a block with no entry simply has
|
|
74
|
+
no preview.
|
|
75
|
+
|
|
64
76
|
### The spacing scale is content, not code
|
|
65
77
|
|
|
66
78
|
Editors own it at **Layout → Spacing Scale**: a list of steps, each with a name
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { cacheLife, cacheTag } from "next/cache"
|
|
2
2
|
import { notFound } from "next/navigation"
|
|
3
|
-
import {
|
|
3
|
+
import { toPlainText } from "next-sanity"
|
|
4
|
+
import { findBlogBlock, PageDocument } from "@/components/page-document"
|
|
5
|
+
import { getBreadcrumbPages } from "@/lib/integrations/sanity/fetchers/breadcrumbs"
|
|
6
|
+
import { getLayoutData } from "@/lib/integrations/sanity/fetchers/layout"
|
|
4
7
|
import {
|
|
5
8
|
type DynamicFetchOptions,
|
|
6
9
|
getDynamicFetchOptions,
|
|
@@ -80,6 +83,23 @@ export const generateStaticParams = async () => {
|
|
|
80
83
|
]
|
|
81
84
|
}
|
|
82
85
|
|
|
86
|
+
// Blog posts fall back to their body and thumbnail when the SEO fields are
|
|
87
|
+
// empty, and are tagged as articles.
|
|
88
|
+
const blogPostMetadata = (page: NonNullable<PAGE_QUERY_RESULT>) => {
|
|
89
|
+
const post = findBlogBlock(page)
|
|
90
|
+
if (!post) return {}
|
|
91
|
+
|
|
92
|
+
return {
|
|
93
|
+
fallbackDescription: post.body ? toPlainText(post.body) : undefined,
|
|
94
|
+
fallbackImage: post.thumbnail,
|
|
95
|
+
article: {
|
|
96
|
+
publishedTime: post.date,
|
|
97
|
+
modifiedTime: page._updatedAt,
|
|
98
|
+
authors: [post.author?.name],
|
|
99
|
+
},
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
83
103
|
export const generateMetadata = async ({ params }: Props) => {
|
|
84
104
|
const [{ slug }, fetchOptions] = await Promise.all([
|
|
85
105
|
params,
|
|
@@ -91,11 +111,20 @@ export const generateMetadata = async ({ params }: Props) => {
|
|
|
91
111
|
|
|
92
112
|
// stega off: it would corrupt a <title> or og:image URL, and metadata renders no
|
|
93
113
|
// DOM for <VisualEditing> anyway. In published mode this is the body's key.
|
|
94
|
-
const
|
|
114
|
+
const metadataFetchOptions = { ...fetchOptions, stega: false }
|
|
115
|
+
const [page, { structuredData }] = await Promise.all([
|
|
116
|
+
getCachedPage(path, metadataFetchOptions),
|
|
117
|
+
getLayoutData(metadataFetchOptions),
|
|
118
|
+
])
|
|
95
119
|
|
|
96
|
-
return
|
|
97
|
-
|
|
98
|
-
|
|
120
|
+
if (!page) return {}
|
|
121
|
+
|
|
122
|
+
return generateSanityMetadata({
|
|
123
|
+
document: page,
|
|
124
|
+
url: path ? `/${path}` : "/",
|
|
125
|
+
site: structuredData,
|
|
126
|
+
...blogPostMetadata(page),
|
|
127
|
+
})
|
|
99
128
|
}
|
|
100
129
|
|
|
101
130
|
// No Suspense wrapper needed: `getDynamicFetchOptions` is prerender-safe and
|
|
@@ -112,7 +141,10 @@ export default async function CatchAllPage({
|
|
|
112
141
|
|
|
113
142
|
if (slug?.length && path === null) notFound()
|
|
114
143
|
|
|
115
|
-
const page = await
|
|
144
|
+
const [page, breadcrumbPages] = await Promise.all([
|
|
145
|
+
getCachedPage(path, fetchOptions),
|
|
146
|
+
getBreadcrumbPages(path, fetchOptions),
|
|
147
|
+
])
|
|
116
148
|
|
|
117
149
|
if (!page) {
|
|
118
150
|
// Fall back to a CMS redirect before 404ing.
|
|
@@ -120,5 +152,12 @@ export default async function CatchAllPage({
|
|
|
120
152
|
notFound()
|
|
121
153
|
}
|
|
122
154
|
|
|
123
|
-
return
|
|
155
|
+
return (
|
|
156
|
+
<PageDocument
|
|
157
|
+
page={page}
|
|
158
|
+
path={path}
|
|
159
|
+
searchParams={searchParams}
|
|
160
|
+
breadcrumbPages={breadcrumbPages}
|
|
161
|
+
/>
|
|
162
|
+
)
|
|
124
163
|
}
|
|
@@ -14,13 +14,14 @@ export type UnlockResult =
|
|
|
14
14
|
| { ok: true; payload: GatedContentPayload }
|
|
15
15
|
| { ok: false; error: string }
|
|
16
16
|
|
|
17
|
-
type UnlockInput = {
|
|
17
|
+
type UnlockInput = { pageId: string; blockKey: string; email: string }
|
|
18
18
|
|
|
19
19
|
export async function unlockGatedContent({
|
|
20
|
-
|
|
20
|
+
pageId,
|
|
21
|
+
blockKey,
|
|
21
22
|
email,
|
|
22
23
|
}: UnlockInput): Promise<UnlockResult> {
|
|
23
|
-
if (!
|
|
24
|
+
if (!(pageId && blockKey)) return { ok: false, error: "Missing content id." }
|
|
24
25
|
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
|
|
25
26
|
return { ok: false, error: "Enter a valid email address." }
|
|
26
27
|
}
|
|
@@ -38,7 +39,7 @@ export async function unlockGatedContent({
|
|
|
38
39
|
// is how `variant` goes missing.
|
|
39
40
|
const payload = await client.fetch<GATED_CONTENT_QUERY_RESULT>(
|
|
40
41
|
GATED_CONTENT_QUERY,
|
|
41
|
-
{
|
|
42
|
+
{ pageId, blockKey },
|
|
42
43
|
toSanityFetchOptions({
|
|
43
44
|
...fetchOptions,
|
|
44
45
|
// stega must stay off: it injects invisible characters into strings, which
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import type { Metadata } from "next"
|
|
2
|
+
import { draftMode } from "next/headers"
|
|
3
|
+
import { notFound } from "next/navigation"
|
|
4
|
+
import { Suspense } from "react"
|
|
5
|
+
import { SpacingScale } from "@/components/layout/spacing-scale"
|
|
6
|
+
import {
|
|
7
|
+
COMPONENT_LIBRARY,
|
|
8
|
+
COMPONENT_PREVIEW_PARAM,
|
|
9
|
+
getLibraryEntry,
|
|
10
|
+
type LibraryEntry,
|
|
11
|
+
} from "@/components/page-builder/library"
|
|
12
|
+
import { Renderer } from "@/components/page-builder/renderer"
|
|
13
|
+
import { Stage } from "@/components/page-builder/stage"
|
|
14
|
+
import { isStudioEmbed } from "@/lib/utils/studio-embed"
|
|
15
|
+
|
|
16
|
+
export const metadata: Metadata = {
|
|
17
|
+
title: "Components",
|
|
18
|
+
robots: { index: false, follow: false, nocache: true },
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
type Props = {
|
|
22
|
+
searchParams: Promise<Record<string, string | string[] | undefined>>
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const firstValue = (value: string | string[] | undefined) =>
|
|
26
|
+
Array.isArray(value) ? value[0] : value
|
|
27
|
+
|
|
28
|
+
const PREVIEW_PAGE_ID = "component-preview"
|
|
29
|
+
|
|
30
|
+
const EntryHeader = ({ entry }: { entry: LibraryEntry }) => (
|
|
31
|
+
<header className="sticky top-0 z-10 flex items-baseline gap-3 border-neutral-200 border-b bg-white/85 px-6 py-3 backdrop-blur">
|
|
32
|
+
<h2 className="font-mono text-neutral-900 text-xs uppercase">
|
|
33
|
+
{entry.title}
|
|
34
|
+
</h2>
|
|
35
|
+
<p className="text-neutral-500 text-xs">{entry.notes}</p>
|
|
36
|
+
</header>
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
const PreviewStage = ({ entry }: { entry: LibraryEntry }) => (
|
|
40
|
+
<Stage background={entry.background}>
|
|
41
|
+
<Renderer
|
|
42
|
+
blocks={[entry.block]}
|
|
43
|
+
pageId={PREVIEW_PAGE_ID}
|
|
44
|
+
pinnedCategory={null}
|
|
45
|
+
searchParams={undefined}
|
|
46
|
+
/>
|
|
47
|
+
</Stage>
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
// Both `isStudioEmbed()` (headers) and `searchParams` are request-time data, so
|
|
51
|
+
// under Cache Components this has to sit inside the page's <Suspense> boundary —
|
|
52
|
+
// at the top of the page it fails the prerender with blocking-prerender-dynamic.
|
|
53
|
+
const Library = async ({ searchParams }: Props) => {
|
|
54
|
+
const [params, embedded] = await Promise.all([searchParams, isStudioEmbed()])
|
|
55
|
+
|
|
56
|
+
if (!embedded) notFound()
|
|
57
|
+
|
|
58
|
+
const requested = firstValue(params[COMPONENT_PREVIEW_PARAM])
|
|
59
|
+
const entry = getLibraryEntry(requested)
|
|
60
|
+
|
|
61
|
+
if (requested && !entry) notFound()
|
|
62
|
+
|
|
63
|
+
if (entry) return <PreviewStage entry={entry} />
|
|
64
|
+
|
|
65
|
+
return (
|
|
66
|
+
<>
|
|
67
|
+
{COMPONENT_LIBRARY.map((item) => (
|
|
68
|
+
<section key={item.documentType}>
|
|
69
|
+
<EntryHeader entry={item} />
|
|
70
|
+
<PreviewStage entry={item} />
|
|
71
|
+
</section>
|
|
72
|
+
))}
|
|
73
|
+
</>
|
|
74
|
+
)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Outside the `(content)` group, so `<SpacingScale />` is on this route.
|
|
78
|
+
const Page = async ({ searchParams }: Props) => {
|
|
79
|
+
const { isEnabled: isDraftMode } = await draftMode()
|
|
80
|
+
|
|
81
|
+
return (
|
|
82
|
+
<main className="flex flex-col">
|
|
83
|
+
<SpacingScale isDraftMode={isDraftMode} />
|
|
84
|
+
<Suspense fallback={null}>
|
|
85
|
+
<Library searchParams={searchParams} />
|
|
86
|
+
</Suspense>
|
|
87
|
+
</main>
|
|
88
|
+
)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export default Page
|
|
@@ -4,17 +4,11 @@ import type { Metadata } from "next"
|
|
|
4
4
|
import { Geist } from "next/font/google"
|
|
5
5
|
import type { PropsWithChildren } from "react"
|
|
6
6
|
import { cn } from "@/lib/styles/cn"
|
|
7
|
+
import { getBaseUrl } from "@/lib/utils/base-url"
|
|
7
8
|
import AppData from "@/package.json"
|
|
8
9
|
|
|
9
10
|
const APP_NAME = AppData.name
|
|
10
|
-
const
|
|
11
|
-
const APP_TITLE_TEMPLATE = "%s"
|
|
12
|
-
const APP_DESCRIPTION = AppData.description
|
|
13
|
-
// Build-time-stable so static routes (Studio, /_not-found) stay prerenderable
|
|
14
|
-
// under Cache Components. NEXT_PUBLIC_* is inlined at build; reading runtime env
|
|
15
|
-
// (VERCEL_URL) here would taint every route's metadata as request-time data.
|
|
16
|
-
const APP_BASE_URL =
|
|
17
|
-
process.env.NEXT_PUBLIC_BASE_URL ?? "http://localhost:3000"
|
|
11
|
+
const APP_BASE_URL = getBaseUrl()
|
|
18
12
|
|
|
19
13
|
const geist = Geist({
|
|
20
14
|
subsets: ["latin"],
|
|
@@ -22,55 +16,20 @@ const geist = Geist({
|
|
|
22
16
|
})
|
|
23
17
|
|
|
24
18
|
export const metadata: Metadata = {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
languages: {
|
|
28
|
-
"en-US": "/en-US",
|
|
29
|
-
},
|
|
30
|
-
},
|
|
19
|
+
metadataBase: new URL(APP_BASE_URL),
|
|
20
|
+
applicationName: APP_NAME,
|
|
31
21
|
appleWebApp: {
|
|
32
22
|
capable: true,
|
|
33
23
|
statusBarStyle: "default",
|
|
34
|
-
title: APP_DEFAULT_TITLE,
|
|
35
24
|
},
|
|
36
|
-
applicationName: APP_NAME,
|
|
37
|
-
authors: [{ name: "basement.studio", url: "https://basement.studio" }],
|
|
38
|
-
description: APP_DESCRIPTION,
|
|
39
25
|
formatDetection: { telephone: false },
|
|
40
|
-
metadataBase: new URL(APP_BASE_URL),
|
|
41
26
|
openGraph: {
|
|
42
|
-
description: APP_DESCRIPTION,
|
|
43
|
-
images: [
|
|
44
|
-
{
|
|
45
|
-
alt: APP_DEFAULT_TITLE,
|
|
46
|
-
height: 630,
|
|
47
|
-
url: "/opengraph-image.jpg",
|
|
48
|
-
width: 1200,
|
|
49
|
-
},
|
|
50
|
-
],
|
|
51
|
-
locale: "en_US",
|
|
52
27
|
siteName: APP_NAME,
|
|
53
|
-
|
|
54
|
-
default: APP_DEFAULT_TITLE,
|
|
55
|
-
template: APP_TITLE_TEMPLATE,
|
|
56
|
-
},
|
|
28
|
+
locale: "en_US",
|
|
57
29
|
type: "website",
|
|
58
|
-
url: APP_BASE_URL,
|
|
59
|
-
},
|
|
60
|
-
other: {
|
|
61
|
-
"fb:app_id": process.env.NEXT_PUBLIC_FACEBOOK_APP_ID || "",
|
|
62
|
-
},
|
|
63
|
-
title: {
|
|
64
|
-
default: APP_DEFAULT_TITLE,
|
|
65
|
-
template: APP_TITLE_TEMPLATE,
|
|
66
30
|
},
|
|
67
31
|
twitter: {
|
|
68
32
|
card: "summary_large_image",
|
|
69
|
-
description: APP_DESCRIPTION,
|
|
70
|
-
title: {
|
|
71
|
-
default: APP_DEFAULT_TITLE,
|
|
72
|
-
template: APP_TITLE_TEMPLATE,
|
|
73
|
-
},
|
|
74
33
|
},
|
|
75
34
|
}
|
|
76
35
|
|
|
@@ -1,12 +1,18 @@
|
|
|
1
1
|
import type { MetadataRoute } from "next"
|
|
2
|
-
import {
|
|
2
|
+
import { headers } from "next/headers"
|
|
3
|
+
import { getBaseUrl, isIndexableHost } from "@/lib/utils/base-url"
|
|
4
|
+
|
|
5
|
+
export default async function robots(): Promise<MetadataRoute.Robots> {
|
|
6
|
+
const host = (await headers()).get("host")
|
|
7
|
+
if (!isIndexableHost(host)) {
|
|
8
|
+
return { rules: { userAgent: "*", disallow: "/" } }
|
|
9
|
+
}
|
|
3
10
|
|
|
4
|
-
export default function robots(): MetadataRoute.Robots {
|
|
5
11
|
return {
|
|
6
12
|
rules: {
|
|
7
13
|
userAgent: "*",
|
|
8
14
|
allow: "/",
|
|
9
|
-
disallow: [],
|
|
15
|
+
disallow: ["/api/", "/studio"],
|
|
10
16
|
},
|
|
11
17
|
sitemap: `${getBaseUrl()}/sitemap.xml`,
|
|
12
18
|
}
|
|
@@ -60,7 +60,7 @@ async function CachedFooter(fetchOptions: DynamicFetchOptions) {
|
|
|
60
60
|
<div className="grid grid-cols-5 gap-x-8 gap-y-6 md:grid-cols-3 lg:grid-cols-5">
|
|
61
61
|
{links.map((linkGroup: FooterLinkGroup) => (
|
|
62
62
|
<div key={linkGroup._key} className="flex flex-col gap-2">
|
|
63
|
-
<
|
|
63
|
+
<h2>{linkGroup.title}</h2>
|
|
64
64
|
{linkGroup.items && linkGroup.items.length > 0 ? (
|
|
65
65
|
<ul className="space-y-1">
|
|
66
66
|
{linkGroup.items
|