bsmnt 0.7.1 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/application/add-hooks/index.d.ts.map +1 -1
- package/dist/application/add-hooks/index.js +1 -0
- package/dist/application/add-hooks/index.js.map +1 -1
- package/dist/application/add-integration/index.d.ts.map +1 -1
- package/dist/application/add-integration/index.js +20 -2
- package/dist/application/add-integration/index.js.map +1 -1
- package/dist/core/create/execute-plan.d.ts +2 -0
- package/dist/core/create/execute-plan.d.ts.map +1 -1
- package/dist/core/create/execute-plan.js +3 -0
- package/dist/core/create/execute-plan.js.map +1 -1
- package/dist/domain/version.d.ts +14 -0
- package/dist/domain/version.d.ts.map +1 -0
- package/dist/domain/version.js +81 -0
- package/dist/domain/version.js.map +1 -0
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/infrastructure/create/executor.d.ts.map +1 -1
- package/dist/infrastructure/create/executor.js +2 -0
- package/dist/infrastructure/create/executor.js.map +1 -1
- package/dist/infrastructure/create/render-generated-docs.d.ts +18 -0
- package/dist/infrastructure/create/render-generated-docs.d.ts.map +1 -0
- package/dist/infrastructure/create/render-generated-docs.js +72 -0
- package/dist/infrastructure/create/render-generated-docs.js.map +1 -0
- package/dist/infrastructure/create/setup-dotenvx.d.ts.map +1 -1
- package/dist/infrastructure/create/setup-dotenvx.js +2 -1
- package/dist/infrastructure/create/setup-dotenvx.js.map +1 -1
- package/dist/infrastructure/create/setup-remote.d.ts +6 -1
- package/dist/infrastructure/create/setup-remote.d.ts.map +1 -1
- package/dist/infrastructure/create/setup-remote.js +7 -2
- package/dist/infrastructure/create/setup-remote.js.map +1 -1
- package/dist/infrastructure/integrations/ensure-studio-headers.d.ts +6 -0
- package/dist/infrastructure/integrations/ensure-studio-headers.d.ts.map +1 -0
- package/dist/infrastructure/integrations/ensure-studio-headers.js +153 -0
- package/dist/infrastructure/integrations/ensure-studio-headers.js.map +1 -0
- package/dist/infrastructure/update/cache.d.ts +14 -0
- package/dist/infrastructure/update/cache.d.ts.map +1 -0
- package/dist/infrastructure/update/cache.js +41 -0
- package/dist/infrastructure/update/cache.js.map +1 -0
- package/dist/infrastructure/update/check-version.d.ts +2 -0
- package/dist/infrastructure/update/check-version.d.ts.map +1 -0
- package/dist/infrastructure/update/check-version.js +33 -0
- package/dist/infrastructure/update/check-version.js.map +1 -0
- package/dist/infrastructure/update/notifier.d.ts +8 -0
- package/dist/infrastructure/update/notifier.d.ts.map +1 -0
- package/dist/infrastructure/update/notifier.js +141 -0
- package/dist/infrastructure/update/notifier.js.map +1 -0
- package/dist/modules/features/cms/sanity/config.d.ts.map +1 -1
- package/dist/modules/features/cms/sanity/config.js +2 -1
- package/dist/modules/features/cms/sanity/config.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 +4 -1
- package/dist/modules/features/cms/sanity-pagebuilder/config.js.map +1 -1
- package/package.json +2 -2
- package/src/modules/features/cms/sanity/files/app/api/blog/[slug]/route.ts +4 -1
- package/src/modules/features/cms/sanity/files/app/sitemap.md/route.ts +2 -1
- package/src/modules/features/cms/sanity/files/components/ui/sanity-image/index.tsx +19 -5
- package/src/modules/features/cms/sanity/files/lib/integrations/sanity/README.md +31 -0
- package/src/modules/features/cms/sanity/files/proxy.ts +9 -0
- package/src/modules/features/cms/sanity-pagebuilder/files/app/api/[[...slug]]/route.ts +8 -3
- package/src/modules/features/cms/sanity-pagebuilder/files/app/sitemap.md/route.ts +7 -1
- package/src/modules/features/cms/sanity-pagebuilder/files/components/ui/sanity-image/index.tsx +15 -5
- package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/components/block-actions-item.tsx +81 -0
- package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/components/slug-field.tsx +13 -7
- package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/components/slug-notices.tsx +135 -0
- package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/icons.ts +2 -0
- package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/live/index.tsx +1 -1
- package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/markdown-proxy.config.ts +1 -1
- package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/page-builder-config.ts +7 -0
- package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/queries.ts +72 -21
- package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/components/index.ts +1 -0
- package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/components/reusable/gated-content.ts +178 -0
- package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/index.ts +9 -1
- package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/layout/redirect.ts +35 -4
- package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/shared/metadata.ts +54 -1
- package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/shared/page-builder.ts +4 -0
- package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/shared/page.ts +11 -0
- package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/structure.ts +4 -0
- package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/utils/page-builder-markdown.ts +20 -0
- package/src/modules/features/cms/sanity-pagebuilder/files/lib/utils/metadata.ts +27 -5
- package/src/modules/features/cms/sanity-pagebuilder/files/proxy.ts +9 -0
- package/src/modules/features/env/dotenvx/files/setup-remote.ts +3 -1
- package/src/templates/next-default/.env.example +10 -0
- package/src/templates/next-default/.github/workflows/ci.yml +7 -0
- package/src/templates/next-default/README.md +14 -0
- package/src/templates/next-default/app/robots.ts +2 -3
- package/src/templates/next-default/knip.json +8 -6
- package/src/templates/next-default/lefthook.yml +0 -5
- package/src/templates/next-default/next.config.ts +36 -2
- package/src/templates/next-default/package.json +9 -9
- package/src/templates/next-experiments/.env.example +10 -0
- package/src/templates/next-experiments/.github/workflows/ci.yml +7 -0
- package/src/templates/next-experiments/README.md +14 -0
- package/src/templates/next-experiments/app/robots.ts +2 -3
- package/src/templates/next-experiments/knip.json +8 -6
- package/src/templates/next-experiments/lefthook.yml +0 -5
- package/src/templates/next-experiments/next.config.ts +36 -2
- package/src/templates/next-experiments/package.json +9 -9
- package/src/templates/next-pagebuilder/.env.example +10 -0
- package/src/templates/next-pagebuilder/.github/PULL_REQUEST_TEMPLATE.md +13 -0
- package/src/templates/next-pagebuilder/.github/workflows/ci.yml +15 -0
- package/src/templates/next-pagebuilder/README.md +45 -0
- package/src/templates/next-pagebuilder/app/(content)/[[...slug]]/page.tsx +26 -11
- package/src/templates/next-pagebuilder/app/actions/unlock-gated-content.ts +39 -0
- package/src/templates/next-pagebuilder/app/robots.ts +2 -3
- package/src/templates/next-pagebuilder/app/sitemap.xml/route.ts +6 -3
- package/src/templates/next-pagebuilder/components/page-builder/components/content-collection/skeleton.tsx +53 -0
- package/src/templates/next-pagebuilder/components/page-builder/components/gated-content/index.tsx +169 -0
- package/src/templates/next-pagebuilder/components/page-builder/renderer.tsx +10 -1
- package/src/templates/next-pagebuilder/knip.json +8 -6
- package/src/templates/next-pagebuilder/lefthook.yml +0 -5
- package/src/templates/next-pagebuilder/lib/integrations/sanity/sanity.types.ts +81 -0
- package/src/templates/next-pagebuilder/next.config.ts +33 -2
- package/src/templates/next-pagebuilder/package.json +4 -4
- package/src/templates/next-webgl/.env.example +10 -0
- package/src/templates/next-webgl/.github/workflows/ci.yml +7 -0
- package/src/templates/next-webgl/README.md +14 -0
- package/src/templates/next-webgl/app/robots.ts +2 -3
- package/src/templates/next-webgl/knip.json +8 -6
- package/src/templates/next-webgl/lefthook.yml +0 -5
- package/src/templates/next-webgl/next.config.ts +36 -2
- package/src/templates/next-webgl/package.json +9 -9
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../../../../src/modules/features/cms/sanity-pagebuilder/config.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,MAAM,WAAW,kCAAkC;IAClD,IAAI,EAAE,MAAM,CAAC;IACb,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACrC,eAAe,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACxC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAChC,aAAa,EAAE,MAAM,EAAE,CAAC;CACxB;AAED,eAAO,MAAM,uBAAuB,EAAE,
|
|
1
|
+
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../../../../src/modules/features/cms/sanity-pagebuilder/config.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,MAAM,WAAW,kCAAkC;IAClD,IAAI,EAAE,MAAM,CAAC;IACb,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACrC,eAAe,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACxC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAChC,aAAa,EAAE,MAAM,EAAE,CAAC;CACxB;AAED,eAAO,MAAM,uBAAuB,EAAE,kCAgDrC,CAAC"}
|
|
@@ -9,15 +9,18 @@ export const sanityPagebuilderConfig = {
|
|
|
9
9
|
"@phosphor-icons/react": "^2.1.10",
|
|
10
10
|
"@portabletext/types": "^4.0.2",
|
|
11
11
|
"@sanity/asset-utils": "^2.3.0",
|
|
12
|
+
"@sanity/icons": "^3.7.4",
|
|
12
13
|
"@sanity/image-url": "^2.1.1",
|
|
13
14
|
"@sanity/table": "^2.0.1",
|
|
15
|
+
"@sanity/ui": "^3.1.14",
|
|
14
16
|
"@sanity/vision": "^5.30.0",
|
|
15
17
|
"@sanity/visual-editing": "^5.4.3",
|
|
16
|
-
"next-sanity": "^13.
|
|
18
|
+
"next-sanity": "^13.3.1",
|
|
17
19
|
sanity: "^5.30.0",
|
|
18
20
|
"sanity-plugin-media": "^4.1.1",
|
|
19
21
|
},
|
|
20
22
|
devDependencies: {
|
|
23
|
+
"@sanity/browserslist-config": "^1.0.5",
|
|
21
24
|
"schema-dts": "^2.0.0",
|
|
22
25
|
},
|
|
23
26
|
scripts: {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"config.js","sourceRoot":"","sources":["../../../../../src/modules/features/cms/sanity-pagebuilder/config.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAUH,MAAM,CAAC,MAAM,uBAAuB,GAAuC;IAC1E,IAAI,EAAE,oBAAoB;IAE1B,YAAY,EAAE;QACb,uBAAuB,EAAE,SAAS;QAClC,qBAAqB,EAAE,QAAQ;QAC/B,qBAAqB,EAAE,QAAQ;QAC/B,mBAAmB,EAAE,QAAQ;QAC7B,eAAe,EAAE,QAAQ;QACzB,gBAAgB,EAAE,SAAS;QAC3B,wBAAwB,EAAE,QAAQ;QAClC,aAAa,EAAE,
|
|
1
|
+
{"version":3,"file":"config.js","sourceRoot":"","sources":["../../../../../src/modules/features/cms/sanity-pagebuilder/config.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAUH,MAAM,CAAC,MAAM,uBAAuB,GAAuC;IAC1E,IAAI,EAAE,oBAAoB;IAE1B,YAAY,EAAE;QACb,uBAAuB,EAAE,SAAS;QAClC,qBAAqB,EAAE,QAAQ;QAC/B,qBAAqB,EAAE,QAAQ;QAC/B,eAAe,EAAE,QAAQ;QACzB,mBAAmB,EAAE,QAAQ;QAC7B,eAAe,EAAE,QAAQ;QACzB,YAAY,EAAE,SAAS;QACvB,gBAAgB,EAAE,SAAS;QAC3B,wBAAwB,EAAE,QAAQ;QAClC,aAAa,EAAE,SAAS;QACxB,MAAM,EAAE,SAAS;QACjB,qBAAqB,EAAE,QAAQ;KAC/B;IAED,eAAe,EAAE;QAChB,6BAA6B,EAAE,QAAQ;QACvC,YAAY,EAAE,QAAQ;KACtB;IAED,OAAO,EAAE;QACR,gBAAgB,EACf,wFAAwF;QACzF,gBAAgB,EAAE,qCAAqC;QACvD,MAAM,EAAE,wBAAwB;QAChC,QAAQ,EAAE,wBAAwB;KAClC;IAED,aAAa,EAAE;QACd,yBAAyB;QACzB,uCAAuC;QACvC,6BAA6B;QAC7B,uBAAuB;QACvB,uBAAuB;QACvB,kBAAkB;QAClB,sBAAsB;QACtB,+BAA+B;QAC/B,YAAY;QACZ,SAAS;QACT,cAAc;QACd,gBAAgB;QAChB,UAAU;QACV,mBAAmB;QACnB,4BAA4B;KAC5B;CACD,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bsmnt",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.0",
|
|
4
4
|
"packageManager": "bun@1.3.14",
|
|
5
5
|
"description": "CLI to scaffold basement projects and add integrations",
|
|
6
6
|
"type": "module",
|
|
@@ -81,7 +81,7 @@
|
|
|
81
81
|
"commander": "^13.1.0",
|
|
82
82
|
"dotenv": "^17.4.2",
|
|
83
83
|
"fs-extra": "^11.3.3",
|
|
84
|
-
"next-sanity": "^13.
|
|
84
|
+
"next-sanity": "^13.3.1",
|
|
85
85
|
"ora": "^9.1.0",
|
|
86
86
|
"picocolors": "^1.1.1",
|
|
87
87
|
"prompts": "^2.4.2",
|
|
@@ -4,6 +4,7 @@ import { SanityFetch } from "@/lib/integrations/sanity/live";
|
|
|
4
4
|
import { ARTICLE_QUERY } from "@/lib/integrations/sanity/queries";
|
|
5
5
|
import type { Article } from "@/lib/integrations/sanity/sanity.types";
|
|
6
6
|
import { portableTextToMarkdown } from "@/lib/utils/portable-text-to-markdown";
|
|
7
|
+
import { getBaseUrl } from "@/lib/utils/url";
|
|
7
8
|
|
|
8
9
|
export async function GET(
|
|
9
10
|
_request: Request,
|
|
@@ -16,7 +17,8 @@ export async function GET(
|
|
|
16
17
|
}
|
|
17
18
|
|
|
18
19
|
const slug = rawSlug.slice(0, -3);
|
|
19
|
-
const baseUrl =
|
|
20
|
+
const baseUrl = getBaseUrl().replace(/\/$/, "");
|
|
21
|
+
const canonicalPath = `/blog/${slug}`;
|
|
20
22
|
|
|
21
23
|
if (!isSanityConfigured()) {
|
|
22
24
|
return new NextResponse("# 404 Not Found\n\nCMS not configured.", {
|
|
@@ -64,6 +66,7 @@ export async function GET(
|
|
|
64
66
|
"Content-Type": "text/markdown; charset=utf-8",
|
|
65
67
|
Vary: "Accept",
|
|
66
68
|
"X-Content-Type-Options": "nosniff",
|
|
69
|
+
Link: `<${baseUrl}${canonicalPath}>; rel="canonical"`,
|
|
67
70
|
},
|
|
68
71
|
});
|
|
69
72
|
} catch (error) {
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { NextResponse } from "next/server";
|
|
2
2
|
import { isSanityConfigured } from "@/lib/integrations/check-integration";
|
|
3
|
+
import { getBaseUrl } from "@/lib/utils/url";
|
|
3
4
|
|
|
4
5
|
export async function GET() {
|
|
5
|
-
const baseUrl =
|
|
6
|
+
const baseUrl = getBaseUrl();
|
|
6
7
|
|
|
7
8
|
if (!isSanityConfigured()) {
|
|
8
9
|
return new NextResponse(
|
|
@@ -4,13 +4,13 @@ import { urlForImage } from "@/lib/integrations/sanity/utils/image";
|
|
|
4
4
|
|
|
5
5
|
interface SanityImageProps extends Omit<ImageProps, "src" | "aspectRatio"> {
|
|
6
6
|
image: {
|
|
7
|
-
asset
|
|
7
|
+
asset?: {
|
|
8
8
|
_ref: string;
|
|
9
9
|
_type: "reference";
|
|
10
10
|
};
|
|
11
11
|
alt?: string;
|
|
12
12
|
hotspot?: object;
|
|
13
|
-
crop?:
|
|
13
|
+
crop?: { left?: number; right?: number; top?: number; bottom?: number };
|
|
14
14
|
};
|
|
15
15
|
maxWidth?: number;
|
|
16
16
|
}
|
|
@@ -19,18 +19,32 @@ export function SanityImage({
|
|
|
19
19
|
image,
|
|
20
20
|
maxWidth = 1920,
|
|
21
21
|
alt,
|
|
22
|
+
fill,
|
|
22
23
|
...props
|
|
23
24
|
}: SanityImageProps) {
|
|
24
25
|
if (!image?.asset) return null;
|
|
25
26
|
|
|
27
|
+
// getImageDimensions is uncropped; the URL builder applies the crop.
|
|
28
|
+
// Rounding order must match @sanity/image-url: origin first, then subtract.
|
|
26
29
|
const { width, height } = getImageDimensions(image.asset);
|
|
27
|
-
const
|
|
30
|
+
const { left = 0, right = 0, top = 0, bottom = 0 } = image.crop ?? {};
|
|
31
|
+
const cropLeft = Math.round(left * width);
|
|
32
|
+
const cropTop = Math.round(top * height);
|
|
33
|
+
const sourceWidth = Math.round(width - right * width - cropLeft);
|
|
34
|
+
const sourceHeight = Math.round(height - bottom * height - cropTop);
|
|
35
|
+
|
|
36
|
+
const imageProps = fill
|
|
37
|
+
? { fill: true }
|
|
38
|
+
: { width: sourceWidth, height: sourceHeight };
|
|
28
39
|
|
|
29
40
|
return (
|
|
30
41
|
<Image
|
|
31
|
-
src={urlForImage(image)
|
|
42
|
+
src={urlForImage(image)
|
|
43
|
+
.width(Math.min(maxWidth, sourceWidth))
|
|
44
|
+
.url()}
|
|
32
45
|
alt={alt || image.alt || ""}
|
|
33
|
-
aspectRatio={
|
|
46
|
+
aspectRatio={sourceWidth / sourceHeight}
|
|
47
|
+
{...imageProps}
|
|
34
48
|
{...props}
|
|
35
49
|
/>
|
|
36
50
|
);
|
|
@@ -128,6 +128,37 @@ export const landing = defineType({
|
|
|
128
128
|
|
|
129
129
|
See [ARCHITECTURE.md](../../../ARCHITECTURE.md) for cache gotchas.
|
|
130
130
|
|
|
131
|
+
## Sanity Dashboard (Studio in an iframe)
|
|
132
|
+
|
|
133
|
+
The Studio at `/studio` can be embedded in the Sanity Dashboard
|
|
134
|
+
(`https://www.sanity.io/@<org>/studio/<appId>/<workspace>`), which loads it in an
|
|
135
|
+
iframe from `www.sanity.io`. Three things have to be true for that to work:
|
|
136
|
+
|
|
137
|
+
1. **Headers.** `next.config.ts` already exempts `/studio/:path*` from the
|
|
138
|
+
site-wide `X-Frame-Options` / `frame-ancestors 'self'` headers, sending
|
|
139
|
+
`frame-ancestors 'self' https://*.sanity.io` there instead and dropping
|
|
140
|
+
`X-Frame-Options` entirely for that path (it has no "allow this origin"
|
|
141
|
+
form — it must be absent, not overridden). Nothing to do here unless
|
|
142
|
+
you've hand-edited `headers()`.
|
|
143
|
+
2. **Bridge script.** Already wired in: `next-sanity/studio`'s `NextStudio`
|
|
144
|
+
re-exports `NextStudioWithBridge`, which renders the
|
|
145
|
+
`core.sanity-cdn.com/bridge.js` tag the Dashboard needs. Don't hand-roll
|
|
146
|
+
that script tag, and don't switch the Studio route to
|
|
147
|
+
`next-sanity/studio/client-component` (the bridge-less variant).
|
|
148
|
+
3. **Registered URL.** In [Sanity Manage](https://sanity.io/manage), the
|
|
149
|
+
Studio application's URL must be the **full path**
|
|
150
|
+
(`https://example.com/studio`), not just the domain. This can't be set
|
|
151
|
+
from code — either click through Manage, or run
|
|
152
|
+
`sanity deploy --external --url https://example.com/studio --yes` from
|
|
153
|
+
`lib/integrations/sanity`. Writing it via the API needs the
|
|
154
|
+
`sanity.project.deployStudio` grant, which read/write tokens don't have.
|
|
155
|
+
|
|
156
|
+
Verify against a production build (`bun build && bun start`), not dev. One trap:
|
|
157
|
+
the Dashboard reports `Failed to load iframe due to X-Frame-Options: SAMEORIGIN`
|
|
158
|
+
for *any* framing failure, including a wrong registered URL with already-correct
|
|
159
|
+
headers. Trust the browser console's `Framing '<url>' violates …` line instead —
|
|
160
|
+
it names the URL actually being framed.
|
|
161
|
+
|
|
131
162
|
## Troubleshooting
|
|
132
163
|
|
|
133
164
|
**Visual editor not loading:**
|
|
@@ -14,6 +14,15 @@ export function proxy(request: NextRequest) {
|
|
|
14
14
|
return NextResponse.next();
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
+
// Collapse repeated slashes before any rewrite, or the rewrite below would
|
|
18
|
+
// mask the alias (e.g. /blog//foo.md silently resolving to /blog/foo.md).
|
|
19
|
+
const collapsedPathname = pathname.replace(/\/{2,}/g, "/");
|
|
20
|
+
if (collapsedPathname !== pathname) {
|
|
21
|
+
const url = request.nextUrl.clone();
|
|
22
|
+
url.pathname = collapsedPathname;
|
|
23
|
+
return NextResponse.redirect(url, 308);
|
|
24
|
+
}
|
|
25
|
+
|
|
17
26
|
// 1. Rewrite .md extension URLs to API routes
|
|
18
27
|
// e.g. /blog/my-post.md → /api/blog/my-post.md
|
|
19
28
|
for (const route of mdExtensionRoutes) {
|
|
@@ -8,6 +8,9 @@ import { getBaseUrl } from "@/lib/utils/base-url";
|
|
|
8
8
|
|
|
9
9
|
type PageResult = NonNullable<PAGE_QUERY_RESULT>;
|
|
10
10
|
|
|
11
|
+
const isValidSegment = (segment: string) =>
|
|
12
|
+
segment.length > 0 && segment !== "." && segment !== "..";
|
|
13
|
+
|
|
11
14
|
const getSlugPath = (slugParts?: string[]) => {
|
|
12
15
|
if (!slugParts?.length) return null;
|
|
13
16
|
|
|
@@ -18,8 +21,9 @@ const getSlugPath = (slugParts?: string[]) => {
|
|
|
18
21
|
const pathSegments = [...slugParts];
|
|
19
22
|
pathSegments[pathSegments.length - 1] = lastSegment.slice(0, -3);
|
|
20
23
|
|
|
21
|
-
|
|
22
|
-
|
|
24
|
+
if (!pathSegments.every(isValidSegment)) return null;
|
|
25
|
+
|
|
26
|
+
const slug = pathSegments.join("/");
|
|
23
27
|
|
|
24
28
|
return slug === "index" ? null : slug;
|
|
25
29
|
};
|
|
@@ -57,7 +61,7 @@ export async function GET(
|
|
|
57
61
|
|
|
58
62
|
const page = data as PageResult | null;
|
|
59
63
|
|
|
60
|
-
const baseUrl = getBaseUrl();
|
|
64
|
+
const baseUrl = getBaseUrl().replace(/\/$/, "");
|
|
61
65
|
|
|
62
66
|
if (!page) {
|
|
63
67
|
return new NextResponse("# 404 Not Found\n\nPage not found.", {
|
|
@@ -94,6 +98,7 @@ export async function GET(
|
|
|
94
98
|
"Content-Type": "text/markdown; charset=utf-8",
|
|
95
99
|
Vary: "Accept",
|
|
96
100
|
"X-Content-Type-Options": "nosniff",
|
|
101
|
+
Link: `<${baseUrl}${canonicalPath}>; rel="canonical"`,
|
|
97
102
|
},
|
|
98
103
|
},
|
|
99
104
|
);
|
|
@@ -113,7 +113,13 @@ export const GET = async () => {
|
|
|
113
113
|
|
|
114
114
|
if (pages.length === 0) parts.push("No content published yet.", "");
|
|
115
115
|
|
|
116
|
-
return new NextResponse(parts.join("\n")
|
|
116
|
+
return new NextResponse(parts.join("\n"), {
|
|
117
|
+
headers: {
|
|
118
|
+
"Content-Type": "text/markdown; charset=utf-8",
|
|
119
|
+
"X-Content-Type-Options": "nosniff",
|
|
120
|
+
"Cache-Control": "public, s-maxage=3600, stale-while-revalidate=600",
|
|
121
|
+
},
|
|
122
|
+
});
|
|
117
123
|
} catch (error) {
|
|
118
124
|
console.error("Error generating sitemap.md:", error);
|
|
119
125
|
return new NextResponse("# Sitemap\n\nError fetching content.", {
|
package/src/modules/features/cms/sanity-pagebuilder/files/components/ui/sanity-image/index.tsx
CHANGED
|
@@ -10,7 +10,7 @@ interface SanityImageProps extends Omit<ImageProps, "src" | "aspectRatio"> {
|
|
|
10
10
|
};
|
|
11
11
|
alt?: string;
|
|
12
12
|
hotspot?: object;
|
|
13
|
-
crop?:
|
|
13
|
+
crop?: { left?: number; right?: number; top?: number; bottom?: number };
|
|
14
14
|
};
|
|
15
15
|
maxWidth?: number;
|
|
16
16
|
}
|
|
@@ -24,16 +24,26 @@ export function SanityImage({
|
|
|
24
24
|
}: SanityImageProps) {
|
|
25
25
|
if (!image?.asset) return null;
|
|
26
26
|
|
|
27
|
+
// getImageDimensions is uncropped; the URL builder applies the crop.
|
|
28
|
+
// Rounding order must match @sanity/image-url: origin first, then subtract.
|
|
27
29
|
const { width, height } = getImageDimensions(image.asset);
|
|
28
|
-
const
|
|
30
|
+
const { left = 0, right = 0, top = 0, bottom = 0 } = image.crop ?? {};
|
|
31
|
+
const cropLeft = Math.round(left * width);
|
|
32
|
+
const cropTop = Math.round(top * height);
|
|
33
|
+
const sourceWidth = Math.round(width - right * width - cropLeft);
|
|
34
|
+
const sourceHeight = Math.round(height - bottom * height - cropTop);
|
|
29
35
|
|
|
30
|
-
const imageProps = fill
|
|
36
|
+
const imageProps = fill
|
|
37
|
+
? { fill: true }
|
|
38
|
+
: { width: sourceWidth, height: sourceHeight };
|
|
31
39
|
|
|
32
40
|
return (
|
|
33
41
|
<Image
|
|
34
|
-
src={urlForImage(image)
|
|
42
|
+
src={urlForImage(image)
|
|
43
|
+
.width(Math.min(maxWidth, sourceWidth))
|
|
44
|
+
.url()}
|
|
35
45
|
alt={alt || image.alt || ""}
|
|
36
|
-
aspectRatio={
|
|
46
|
+
aspectRatio={sourceWidth / sourceHeight}
|
|
37
47
|
{...imageProps}
|
|
38
48
|
{...props}
|
|
39
49
|
/>
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { EyeClosedIcon, EyeOpenIcon } from "@sanity/icons";
|
|
2
|
+
import { Badge, Box, Button, Flex, Text, Tooltip, useToast } from "@sanity/ui";
|
|
3
|
+
import { useState } from "react";
|
|
4
|
+
import { type ObjectItemProps, useClient, useFormValue } from "sanity";
|
|
5
|
+
import { apiVersion } from "@/lib/integrations/sanity/env";
|
|
6
|
+
|
|
7
|
+
const HIDDEN_KEYS_FIELD = "hiddenPageBuilderKeys";
|
|
8
|
+
|
|
9
|
+
type ReferenceItem = { _key: string; _ref: string; _type: string };
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Eye-icon toggle for a page builder placement's per-page visibility. Adds the
|
|
13
|
+
* placement's `_key` to the owning document's `hiddenPageBuilderKeys`; GROQ
|
|
14
|
+
* filters those keys out at query time. Standalone control because Sanity's
|
|
15
|
+
* built-in item menu isn't extensible via public props.
|
|
16
|
+
*/
|
|
17
|
+
export function BlockActionsItem(props: ObjectItemProps<ReferenceItem>) {
|
|
18
|
+
const { value, renderDefault } = props;
|
|
19
|
+
const client = useClient({ apiVersion });
|
|
20
|
+
const toast = useToast();
|
|
21
|
+
const [busy, setBusy] = useState(false);
|
|
22
|
+
|
|
23
|
+
// Open document id (draft while editing).
|
|
24
|
+
const documentId = useFormValue(["_id"]) as string | undefined;
|
|
25
|
+
const hiddenKeys =
|
|
26
|
+
(useFormValue([HIDDEN_KEYS_FIELD]) as string[] | undefined) ?? [];
|
|
27
|
+
const itemKey = value._key;
|
|
28
|
+
const hidden = hiddenKeys.includes(itemKey);
|
|
29
|
+
const label = hidden ? "Show on this page" : "Hide on this page";
|
|
30
|
+
|
|
31
|
+
async function handleToggleHidden() {
|
|
32
|
+
if (busy || !documentId) return;
|
|
33
|
+
setBusy(true);
|
|
34
|
+
try {
|
|
35
|
+
// Atomic array ops (not read-modify-write) so concurrent toggles
|
|
36
|
+
// of different blocks don't clobber each other.
|
|
37
|
+
const patch = client.patch(documentId);
|
|
38
|
+
if (hidden) {
|
|
39
|
+
patch.unset([`${HIDDEN_KEYS_FIELD}[@ == "${itemKey}"]`]);
|
|
40
|
+
} else {
|
|
41
|
+
patch
|
|
42
|
+
.setIfMissing({ [HIDDEN_KEYS_FIELD]: [] })
|
|
43
|
+
.insert("after", `${HIDDEN_KEYS_FIELD}[-1]`, [itemKey]);
|
|
44
|
+
}
|
|
45
|
+
await patch.commit();
|
|
46
|
+
toast.push({
|
|
47
|
+
status: "success",
|
|
48
|
+
title: hidden ? "Block shown" : "Block hidden",
|
|
49
|
+
});
|
|
50
|
+
} catch {
|
|
51
|
+
toast.push({ status: "error", title: "Failed to update visibility" });
|
|
52
|
+
} finally {
|
|
53
|
+
setBusy(false);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return (
|
|
58
|
+
<Flex align="center" gap={1} paddingLeft={1}>
|
|
59
|
+
<Tooltip content={<Text size={1}>{label}</Text>} placement="top" portal>
|
|
60
|
+
<Button
|
|
61
|
+
mode="bleed"
|
|
62
|
+
// Open eye = visible, closed eye = hidden.
|
|
63
|
+
icon={hidden ? EyeClosedIcon : EyeOpenIcon}
|
|
64
|
+
tone={hidden ? "caution" : "default"}
|
|
65
|
+
aria-label={label}
|
|
66
|
+
// Unsaved doc has no id to patch — disable, don't no-op.
|
|
67
|
+
disabled={busy || !documentId}
|
|
68
|
+
onClick={handleToggleHidden}
|
|
69
|
+
/>
|
|
70
|
+
</Tooltip>
|
|
71
|
+
{hidden && (
|
|
72
|
+
<Badge fontSize={0} mode="outline" tone="caution">
|
|
73
|
+
Hidden
|
|
74
|
+
</Badge>
|
|
75
|
+
)}
|
|
76
|
+
<Box flex={1} style={hidden ? { opacity: 0.5 } : undefined}>
|
|
77
|
+
{renderDefault(props)}
|
|
78
|
+
</Box>
|
|
79
|
+
</Flex>
|
|
80
|
+
);
|
|
81
|
+
}
|
|
@@ -1,18 +1,24 @@
|
|
|
1
|
+
import { Stack } from "@sanity/ui";
|
|
1
2
|
import { type FieldProps, useFormValue } from "sanity";
|
|
3
|
+
import { SlugRedirectNotice } from "@/lib/integrations/sanity/components/slug-notices";
|
|
2
4
|
import { getPinnedPageById } from "@/lib/integrations/sanity/pinned-pages";
|
|
3
5
|
|
|
4
6
|
const DEFAULT_DESCRIPTION = "URL path without leading slash.";
|
|
5
7
|
const PINNED_DESCRIPTION =
|
|
6
8
|
"URL path without leading slash. Disabled on 📌 pinned pages.";
|
|
7
9
|
|
|
8
|
-
// Swaps the slug description
|
|
9
|
-
// (Sanity descriptions are plain strings, hence the 📌 character.)
|
|
10
|
+
// Swaps the slug description on pinned pages (plain-string field, hence 📌).
|
|
10
11
|
export function SlugField(props: FieldProps) {
|
|
11
12
|
const documentId = useFormValue(["_id"]) as string | undefined;
|
|
12
|
-
const
|
|
13
|
+
const pinned = getPinnedPageById(documentId);
|
|
13
14
|
|
|
14
|
-
return
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
15
|
+
return (
|
|
16
|
+
<Stack space={3}>
|
|
17
|
+
{props.renderDefault({
|
|
18
|
+
...props,
|
|
19
|
+
description: pinned ? PINNED_DESCRIPTION : DEFAULT_DESCRIPTION,
|
|
20
|
+
})}
|
|
21
|
+
<SlugRedirectNotice pinnedPath={pinned?.path} />
|
|
22
|
+
</Stack>
|
|
23
|
+
);
|
|
18
24
|
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { Box, Button, Card, Stack, Text } from "@sanity/ui";
|
|
2
|
+
import { useEffect, useState } from "react";
|
|
3
|
+
import { useClient, useFormValue } from "sanity";
|
|
4
|
+
import { useRouter } from "sanity/router";
|
|
5
|
+
import { apiVersion } from "@/lib/integrations/sanity/env";
|
|
6
|
+
import { isEmptyPageSlug } from "@/lib/integrations/sanity/page-category";
|
|
7
|
+
|
|
8
|
+
// Mirrors `pageResolvedSlugExpression`'s chain; leaf joins in JS so this
|
|
9
|
+
// tracks the live slug, not the last saved draft.
|
|
10
|
+
const FOLDER_PATH_QUERY = `*[_id == $folderId][0]{
|
|
11
|
+
"path": array::join(array::compact([
|
|
12
|
+
parentFolder->parentFolder->parentFolder->parentFolder->parentFolder->slug.current,
|
|
13
|
+
parentFolder->parentFolder->parentFolder->parentFolder->slug.current,
|
|
14
|
+
parentFolder->parentFolder->parentFolder->slug.current,
|
|
15
|
+
parentFolder->parentFolder->slug.current,
|
|
16
|
+
parentFolder->slug.current,
|
|
17
|
+
slug.current
|
|
18
|
+
]), "/")
|
|
19
|
+
}.path`;
|
|
20
|
+
|
|
21
|
+
const REDIRECT_QUERY = `*[_type == "redirect" && source.current == $source][0]._id`;
|
|
22
|
+
|
|
23
|
+
// The slug field writes on every keystroke.
|
|
24
|
+
const LOOKUP_DEBOUNCE_MS = 400;
|
|
25
|
+
|
|
26
|
+
interface SlugRedirectNoticeProps {
|
|
27
|
+
/** Public path of a pinned page, whose URL is fixed rather than slug-derived. */
|
|
28
|
+
pinnedPath?: string | undefined;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Warns when a published redirect claims this page's URL — the catch-all
|
|
33
|
+
* route only runs `checkRedirect` after a page lookup misses, so the page
|
|
34
|
+
* wins and the redirect silently never fires.
|
|
35
|
+
*
|
|
36
|
+
* Reads the `page` form shape; don't reuse for other slug types.
|
|
37
|
+
*/
|
|
38
|
+
export function SlugRedirectNotice({ pinnedPath }: SlugRedirectNoticeProps) {
|
|
39
|
+
const client = useClient({ apiVersion });
|
|
40
|
+
const router = useRouter();
|
|
41
|
+
const slug = useFormValue(["slug", "current"]) as string | undefined;
|
|
42
|
+
const folderId = useFormValue(["pageFolder", "_ref"]) as string | undefined;
|
|
43
|
+
const [folderPath, setFolderPath] = useState("");
|
|
44
|
+
const [redirectId, setRedirectId] = useState<string | null>(null);
|
|
45
|
+
|
|
46
|
+
// Separate effect so the folder chain isn't re-resolved on every keystroke.
|
|
47
|
+
useEffect(() => {
|
|
48
|
+
if (!folderId) {
|
|
49
|
+
setFolderPath("");
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
let active = true;
|
|
54
|
+
const published = client.withConfig({ perspective: "published" });
|
|
55
|
+
|
|
56
|
+
published
|
|
57
|
+
.fetch<string | null>(FOLDER_PATH_QUERY, { folderId })
|
|
58
|
+
.then((path) => {
|
|
59
|
+
if (active) setFolderPath(path ?? "");
|
|
60
|
+
})
|
|
61
|
+
.catch(() => {
|
|
62
|
+
if (active) setFolderPath("");
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
return () => {
|
|
66
|
+
active = false;
|
|
67
|
+
};
|
|
68
|
+
}, [client, folderId]);
|
|
69
|
+
|
|
70
|
+
useEffect(() => {
|
|
71
|
+
const leaf = isEmptyPageSlug(slug) ? "" : slug;
|
|
72
|
+
const source = pinnedPath
|
|
73
|
+
? pinnedPath.replace(/^\/+/, "")
|
|
74
|
+
: [folderPath, leaf].filter(Boolean).join("/");
|
|
75
|
+
|
|
76
|
+
if (!source) {
|
|
77
|
+
setRedirectId(null);
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
let active = true;
|
|
82
|
+
// Drafts never reach visitors, so an unpublished redirect isn't a conflict.
|
|
83
|
+
const published = client.withConfig({ perspective: "published" });
|
|
84
|
+
|
|
85
|
+
const timer = setTimeout(async () => {
|
|
86
|
+
try {
|
|
87
|
+
const id = await published.fetch<string | null>(REDIRECT_QUERY, {
|
|
88
|
+
source,
|
|
89
|
+
});
|
|
90
|
+
if (active) setRedirectId(id);
|
|
91
|
+
} catch {
|
|
92
|
+
if (active) setRedirectId(null);
|
|
93
|
+
}
|
|
94
|
+
}, LOOKUP_DEBOUNCE_MS);
|
|
95
|
+
|
|
96
|
+
return () => {
|
|
97
|
+
active = false;
|
|
98
|
+
clearTimeout(timer);
|
|
99
|
+
};
|
|
100
|
+
}, [client, folderPath, pinnedPath, slug]);
|
|
101
|
+
|
|
102
|
+
if (!redirectId) return null;
|
|
103
|
+
|
|
104
|
+
return (
|
|
105
|
+
<Card border padding={3} radius={2} tone="caution">
|
|
106
|
+
<Stack space={3}>
|
|
107
|
+
<Stack space={2}>
|
|
108
|
+
<Text size={1} weight="semibold">
|
|
109
|
+
A redirect already claims this URL
|
|
110
|
+
</Text>
|
|
111
|
+
<Text muted size={1}>
|
|
112
|
+
Visitors reach this page instead — a redirect only runs when no page
|
|
113
|
+
matches its path. Remove the redirect, or move this page to another
|
|
114
|
+
slug.
|
|
115
|
+
</Text>
|
|
116
|
+
</Stack>
|
|
117
|
+
<Box>
|
|
118
|
+
<Button
|
|
119
|
+
fontSize={1}
|
|
120
|
+
mode="ghost"
|
|
121
|
+
onClick={() =>
|
|
122
|
+
router.navigateIntent("edit", {
|
|
123
|
+
id: redirectId,
|
|
124
|
+
type: "redirect",
|
|
125
|
+
})
|
|
126
|
+
}
|
|
127
|
+
padding={3}
|
|
128
|
+
text="Go to redirect"
|
|
129
|
+
tone="caution"
|
|
130
|
+
/>
|
|
131
|
+
</Box>
|
|
132
|
+
</Stack>
|
|
133
|
+
</Card>
|
|
134
|
+
);
|
|
135
|
+
}
|
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
LayoutIcon,
|
|
16
16
|
ListBulletsIcon,
|
|
17
17
|
ListIcon,
|
|
18
|
+
LockKeyIcon,
|
|
18
19
|
PackageIcon,
|
|
19
20
|
PushPinIcon,
|
|
20
21
|
QuestionIcon,
|
|
@@ -71,6 +72,7 @@ export const customJsonLdIcon = asDuotoneIcon(BracketsCurlyIcon);
|
|
|
71
72
|
export const heroIcon = asDuotoneIcon(RocketIcon);
|
|
72
73
|
export const descriptionIcon = asDuotoneIcon(TextAlignLeftIcon);
|
|
73
74
|
export const contentIcon = asDuotoneIcon(ArticleNyTimesIcon);
|
|
75
|
+
export const gatedContentIcon = asDuotoneIcon(LockKeyIcon);
|
|
74
76
|
|
|
75
77
|
export const pageIcon = asDuotoneIcon(FileTextIcon);
|
|
76
78
|
export const pinIcon = asDuotoneIcon(PushPinIcon);
|
|
@@ -22,7 +22,7 @@ export interface MarkdownRoute {
|
|
|
22
22
|
/** .md extension routes -- rewrite to API endpoints */
|
|
23
23
|
export const mdExtensionRoutes: MarkdownRoute[] = [
|
|
24
24
|
{
|
|
25
|
-
regex: /^\/(
|
|
25
|
+
regex: /^\/((?:[^/]+\/)*[^/]+)\.md$/,
|
|
26
26
|
apiPath: "/api/[slug].md",
|
|
27
27
|
publicPath: "/[slug].md",
|
|
28
28
|
},
|
|
@@ -4,6 +4,7 @@ import { singletonComponents } from "./singletons";
|
|
|
4
4
|
export type PageBuilderDocumentType =
|
|
5
5
|
| "hero"
|
|
6
6
|
| "description"
|
|
7
|
+
| "gatedContent"
|
|
7
8
|
| "blogContent"
|
|
8
9
|
| (typeof singletonComponents)[number]["schemaType"];
|
|
9
10
|
|
|
@@ -47,6 +48,12 @@ const reusablePageBuilderReferenceMembers: PageBuilderReferenceMember[] = [
|
|
|
47
48
|
title: "Description",
|
|
48
49
|
groups: ["generic"],
|
|
49
50
|
},
|
|
51
|
+
{
|
|
52
|
+
documentType: "gatedContent",
|
|
53
|
+
name: "gatedContentReference",
|
|
54
|
+
title: "Gated Content",
|
|
55
|
+
groups: ["generic"],
|
|
56
|
+
},
|
|
50
57
|
{
|
|
51
58
|
documentType: "blogContent",
|
|
52
59
|
name: "blogContentReference",
|