bsmnt 0.4.0 → 0.4.1

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.
Files changed (26) hide show
  1. package/package.json +1 -1
  2. package/src/templates/next-pagebuilder/app/(content)/[[...slug]]/page.tsx +17 -8
  3. package/src/templates/next-pagebuilder/app/(content)/layout.tsx +19 -7
  4. package/src/templates/next-pagebuilder/app/actions/refresh.ts +5 -0
  5. package/src/templates/next-pagebuilder/components/layout/footer/index.tsx +15 -19
  6. package/src/templates/next-pagebuilder/components/layout/header/index.tsx +3 -5
  7. package/src/templates/next-pagebuilder/components/layout/json-ld/index.tsx +11 -10
  8. package/src/templates/next-pagebuilder/components/layout/wrapper/index.tsx +14 -4
  9. package/src/templates/next-pagebuilder/components/page-builder/components/{post-collection → content-collection}/content-card.tsx +3 -5
  10. package/src/templates/next-pagebuilder/components/page-builder/components/content-collection/content-filters.tsx +93 -0
  11. package/src/templates/next-pagebuilder/components/page-builder/components/{post-collection → content-collection}/content-grid.tsx +7 -9
  12. package/src/templates/next-pagebuilder/components/page-builder/components/content-collection/content-pagination-nav.tsx +71 -0
  13. package/src/templates/next-pagebuilder/components/page-builder/components/content-collection/index.tsx +212 -0
  14. package/src/templates/next-pagebuilder/components/page-builder/components/{post-collection → content-collection}/types.ts +5 -4
  15. package/src/templates/next-pagebuilder/components/page-builder/renderer.tsx +13 -5
  16. package/src/templates/next-pagebuilder/components/page-document/index.tsx +9 -4
  17. package/src/templates/next-pagebuilder/components/sanity/visual-editing.tsx +2 -1
  18. package/src/templates/next-pagebuilder/lib/integrations/sanity/constants.ts +1 -0
  19. package/src/templates/next-pagebuilder/lib/integrations/sanity/fetchers/layout.ts +17 -18
  20. package/src/templates/next-pagebuilder/lib/integrations/sanity/live/index.tsx +29 -2
  21. package/src/templates/next-pagebuilder/lib/integrations/sanity/presentation.ts +118 -0
  22. package/src/templates/next-pagebuilder/lib/integrations/sanity/queries.ts +144 -31
  23. package/src/templates/next-pagebuilder/lib/integrations/sanity/sanity.config.ts +5 -100
  24. package/src/templates/next-pagebuilder/next.config.ts +3 -0
  25. package/src/templates/next-pagebuilder/package.json +1 -2
  26. package/src/templates/next-pagebuilder/components/page-builder/components/post-collection/index.tsx +0 -28
@@ -0,0 +1,212 @@
1
+ import { fetchSanity } from "@/lib/integrations/sanity/live"
2
+ import {
3
+ ALL_BLOG_CATEGORIES_QUERY,
4
+ BLOG_ARTICLES_COUNT_QUERY,
5
+ BLOG_ARTICLES_PAGINATED_QUERY,
6
+ } from "@/lib/integrations/sanity/queries"
7
+ import { ContentFilters } from "./content-filters"
8
+ import { ContentGrid } from "./content-grid"
9
+ import { ContentPaginationNav } from "./content-pagination-nav"
10
+ import type {
11
+ ContentCollectionBlock,
12
+ ContentCollectionSearchParams,
13
+ ContentCategory,
14
+ ContentItemCard,
15
+ SearchParams,
16
+ } from "./types"
17
+
18
+ type ContentCollectionProps = {
19
+ block: ContentCollectionBlock
20
+ searchParams: ContentCollectionSearchParams
21
+ }
22
+
23
+ const FEATURED_COUNT = 2
24
+ const PAGE_SIZE = 6
25
+
26
+ const normalizePageValue = (value: string | string[] | undefined) => {
27
+ const page = Number(Array.isArray(value) ? value[0] : value)
28
+ return Number.isFinite(page) && page > 0 ? Math.floor(page) : 1
29
+ }
30
+
31
+ const getCollectionParams = (searchParams: SearchParams) => ({
32
+ currentPage: normalizePageValue(searchParams.page),
33
+ activeCategory:
34
+ typeof searchParams.category === "string" ? searchParams.category : "",
35
+ })
36
+
37
+ const getTotalPages = ({
38
+ activeCategory,
39
+ totalCount,
40
+ }: {
41
+ activeCategory: string
42
+ totalCount: number
43
+ }) => {
44
+ const regularTotal =
45
+ activeCategory === "" ? Math.max(0, totalCount - FEATURED_COUNT) : totalCount
46
+
47
+ return Math.max(1, Math.ceil(regularTotal / PAGE_SIZE))
48
+ }
49
+
50
+ const getCollectionWindow = ({
51
+ activeCategory,
52
+ currentPage,
53
+ }: {
54
+ activeCategory: string
55
+ currentPage: number
56
+ }) => {
57
+ const isFirstPageNoFilter = currentPage === 1 && activeCategory === ""
58
+
59
+ return {
60
+ isFirstPageNoFilter,
61
+ fetchLimit: isFirstPageNoFilter ? PAGE_SIZE + FEATURED_COUNT : PAGE_SIZE,
62
+ fetchOffset: isFirstPageNoFilter
63
+ ? 0
64
+ : (currentPage - 1) * PAGE_SIZE + (activeCategory === "" ? FEATURED_COUNT : 0),
65
+ }
66
+ }
67
+
68
+ function CollectionToolbarSection({
69
+ activeCategory,
70
+ categories,
71
+ totalCount,
72
+ }: {
73
+ activeCategory: string
74
+ categories: ContentCategory[]
75
+ totalCount: number
76
+ }) {
77
+ return (
78
+ <ContentFilters
79
+ categories={categories}
80
+ activeCategory={activeCategory}
81
+ totalCount={totalCount}
82
+ filteredCount={totalCount}
83
+ />
84
+ )
85
+ }
86
+
87
+ function CollectionResultsSection({
88
+ activeCategory,
89
+ items,
90
+ isFirstPageNoFilter,
91
+ subtitle,
92
+ }: {
93
+ activeCategory: string
94
+ items: ContentItemCard[]
95
+ isFirstPageNoFilter: boolean
96
+ subtitle: string | null | undefined
97
+ }) {
98
+ const featuredItems = isFirstPageNoFilter ? items.slice(0, FEATURED_COUNT) : []
99
+ const regularItems = isFirstPageNoFilter ? items.slice(FEATURED_COUNT) : items
100
+ const isEmpty = featuredItems.length === 0 && regularItems.length === 0
101
+
102
+ return (
103
+ <>
104
+ {featuredItems.length > 0 ? (
105
+ <ContentGrid
106
+ items={featuredItems}
107
+ className="grid grid-cols-1 gap-8 md:grid-cols-2"
108
+ />
109
+ ) : null}
110
+ {isFirstPageNoFilter && activeCategory === "" && subtitle ? (
111
+ <h2 className="font-medium text-2xl leading-tight">{subtitle}</h2>
112
+ ) : null}
113
+ {isEmpty ? (
114
+ <div className="rounded-[1.5rem] border border-black/15 border-dashed bg-black/[0.02] px-6 py-12 text-center">
115
+ <p className="font-medium text-lg">No content found</p>
116
+ <p className="mt-2 text-current/60 text-sm">
117
+ Try a different category to explore more content.
118
+ </p>
119
+ </div>
120
+ ) : (
121
+ <ContentGrid items={regularItems} />
122
+ )}
123
+ </>
124
+ )
125
+ }
126
+
127
+ function CollectionPaginationSection({
128
+ currentPage,
129
+ totalPages,
130
+ }: {
131
+ currentPage: number
132
+ totalPages: number
133
+ }) {
134
+ if (totalPages <= 1) return null
135
+
136
+ return (
137
+ <ContentPaginationNav currentPage={currentPage} totalPages={totalPages} />
138
+ )
139
+ }
140
+
141
+ export async function ContentCollection({
142
+ block,
143
+ searchParams,
144
+ }: ContentCollectionProps) {
145
+ const resolvedParams = (await searchParams) ?? {}
146
+ const { currentPage, activeCategory } = getCollectionParams(resolvedParams)
147
+
148
+ const { data: categoriesData } = await fetchSanity<ContentCategory[]>({
149
+ query: ALL_BLOG_CATEGORIES_QUERY,
150
+ tags: ["blog-categories"],
151
+ })
152
+
153
+ const categories = categoriesData ?? []
154
+ const validCategorySlugs = new Set(
155
+ categories
156
+ .map((category) => category.slug?.current)
157
+ .filter((slug): slug is string => Boolean(slug))
158
+ )
159
+
160
+ const canonicalCategory =
161
+ activeCategory && validCategorySlugs.has(activeCategory) ? activeCategory : ""
162
+
163
+ const { data: totalCountData } = await fetchSanity<number>({
164
+ query: BLOG_ARTICLES_COUNT_QUERY,
165
+ params: { category: canonicalCategory },
166
+ tags: ["blog-articles-count"],
167
+ })
168
+
169
+ const totalCount = totalCountData ?? 0
170
+ const totalPages = getTotalPages({
171
+ activeCategory: canonicalCategory,
172
+ totalCount,
173
+ })
174
+ const canonicalPage = Math.min(currentPage, totalPages)
175
+
176
+ const { isFirstPageNoFilter, fetchLimit, fetchOffset } = getCollectionWindow({
177
+ activeCategory: canonicalCategory,
178
+ currentPage: canonicalPage,
179
+ })
180
+
181
+ const { data: itemsData } = await fetchSanity<ContentItemCard[]>({
182
+ query: BLOG_ARTICLES_PAGINATED_QUERY,
183
+ params: {
184
+ category: canonicalCategory,
185
+ offset: fetchOffset,
186
+ limit: fetchLimit,
187
+ },
188
+ tags: ["blog-articles"],
189
+ })
190
+
191
+ const items = itemsData ?? []
192
+
193
+ return (
194
+ <section className="flex flex-col gap-8 py-8">
195
+ <CollectionToolbarSection
196
+ activeCategory={canonicalCategory}
197
+ categories={categories}
198
+ totalCount={totalCount}
199
+ />
200
+ <CollectionResultsSection
201
+ activeCategory={canonicalCategory}
202
+ items={items}
203
+ isFirstPageNoFilter={isFirstPageNoFilter}
204
+ subtitle={block.subtitle}
205
+ />
206
+ <CollectionPaginationSection
207
+ currentPage={canonicalPage}
208
+ totalPages={totalPages}
209
+ />
210
+ </section>
211
+ )
212
+ }
@@ -1,10 +1,9 @@
1
1
  import type { PageBuilderBlock } from "@/components/page-builder/types"
2
2
  import type { ALL_BLOG_ARTICLES_QUERY_RESULT } from "@/lib/integrations/sanity/sanity.types"
3
3
 
4
- export type BlogCollectionBlock = Extract<
5
- PageBuilderBlock,
6
- { _type: "blogCollection" }
7
- >
4
+ export type SearchParams = Record<string, string | string[] | undefined>
5
+
6
+ export type ContentCollectionSearchParams = Promise<SearchParams> | undefined
8
7
 
9
8
  export type ContentCollectionBlock = Extract<
10
9
  PageBuilderBlock,
@@ -14,3 +13,5 @@ export type ContentCollectionBlock = Extract<
14
13
  >
15
14
 
16
15
  export type BlogArticleCard = ALL_BLOG_ARTICLES_QUERY_RESULT[number]
16
+ export type ContentCategory = NonNullable<BlogArticleCard["blogContent"]>["categories"][number]
17
+ export type ContentItemCard = BlogArticleCard
@@ -1,29 +1,37 @@
1
1
  import { ArticleContent } from "./components/article-content"
2
+ import { ContentCollection } from "./components/content-collection"
2
3
  import { Description } from "./components/description"
3
4
  import { Hero } from "./components/hero"
4
- import { ContentCollection } from "./components/post-collection"
5
+ import type { ContentCollectionSearchParams } from "./components/content-collection/types"
5
6
  import type { PageBuilderBlock, PageBuilderBlocks } from "./types"
6
7
 
7
8
  type RendererProps = {
8
9
  blocks: PageBuilderBlocks
10
+ searchParams: ContentCollectionSearchParams
9
11
  }
10
12
 
11
- export function Renderer({ blocks }: RendererProps) {
13
+ export function Renderer({ blocks, searchParams }: RendererProps) {
12
14
  if (!blocks?.length) return null
13
15
 
14
16
  return (
15
17
  <div className="space-y-16">
16
18
  {blocks.map((block: PageBuilderBlock) => (
17
- <PageBuilderNode key={block._key} block={block} />
19
+ <PageBuilderNode key={block._key} block={block} searchParams={searchParams} />
18
20
  ))}
19
21
  </div>
20
22
  )
21
23
  }
22
24
 
23
- function PageBuilderNode({ block }: { block: PageBuilderBlock }) {
25
+ function PageBuilderNode({
26
+ block,
27
+ searchParams,
28
+ }: {
29
+ block: PageBuilderBlock
30
+ searchParams: ContentCollectionSearchParams
31
+ }) {
24
32
  switch (block._type) {
25
33
  case "blogCollection":
26
- return <ContentCollection block={block} />
34
+ return <ContentCollection block={block} searchParams={searchParams} />
27
35
  case "blogContent":
28
36
  return <ArticleContent block={block} />
29
37
  case "hero":
@@ -1,4 +1,5 @@
1
1
  import { stegaClean } from "@sanity/client/stega"
2
+ import type { ContentCollectionSearchParams } from "@/components/page-builder/components/content-collection/types"
2
3
  import { Renderer } from "@/components/page-builder/renderer"
3
4
  import type { PAGE_QUERY_RESULT } from "@/lib/integrations/sanity/sanity.types"
4
5
  import { urlForImage } from "@/lib/integrations/sanity/utils/image"
@@ -11,11 +12,15 @@ import {
11
12
  type PageDocumentProps = {
12
13
  page: NonNullable<PAGE_QUERY_RESULT>
13
14
  path: string | null
15
+ searchParams: ContentCollectionSearchParams
14
16
  }
15
17
 
16
- export const getPageSections = (page: NonNullable<PAGE_QUERY_RESULT>) =>
18
+ export const getPageSections = (
19
+ page: NonNullable<PAGE_QUERY_RESULT>,
20
+ searchParams: ContentCollectionSearchParams
21
+ ) =>
17
22
  page.pageBuilder?.length
18
- ? [<Renderer key="page-builder" blocks={page.pageBuilder} />]
23
+ ? [<Renderer key="page-builder" blocks={page.pageBuilder} searchParams={searchParams} />]
19
24
  : []
20
25
 
21
26
  const buildBreadcrumbs = (
@@ -47,9 +52,9 @@ const buildBreadcrumbs = (
47
52
  return items
48
53
  }
49
54
 
50
- export const PageDocument = ({ page, path }: PageDocumentProps) => {
55
+ export const PageDocument = ({ page, path, searchParams }: PageDocumentProps) => {
51
56
  const title = stegaClean(page.title) || page.title
52
- const sections = getPageSections(page)
57
+ const sections = getPageSections(page, searchParams)
53
58
 
54
59
  const cleanTitle = stegaClean(page.title) || "Basement"
55
60
  const cleanDescription = stegaClean(
@@ -1,6 +1,7 @@
1
1
  import { draftMode } from "next/headers"
2
2
  import { VisualEditing } from "next-sanity/visual-editing"
3
3
  import { Suspense } from "react"
4
+ import { refreshAction } from "@/app/actions/refresh"
4
5
  import { DraftModeToggle } from "@/components/sanity/draft-mode-toggle"
5
6
  import { isSanityConfigured } from "@/lib/integrations/check-integration"
6
7
  import { SanityLive } from "@/lib/integrations/sanity/live"
@@ -15,7 +16,7 @@ async function VisualEditingInner() {
15
16
  <>
16
17
  <DraftModeToggle />
17
18
  <VisualEditing />
18
- <SanityLive />
19
+ <SanityLive revalidateSyncTags={refreshAction} />
19
20
  </>
20
21
  )
21
22
  }
@@ -0,0 +1 @@
1
+ export const HOMEPAGE_DOCUMENT_ID = "page-homepage"
@@ -1,8 +1,6 @@
1
- import { sanityFetch } from "@/lib/integrations/sanity/live"
1
+ import { fetchSanity } from "@/lib/integrations/sanity/live"
2
2
  import {
3
- COMPANY_DATA_QUERY,
4
- FOOTER_QUERY,
5
- NAVBAR_QUERY,
3
+ SITE_LAYOUT_QUERY,
6
4
  } from "@/lib/integrations/sanity/queries"
7
5
  import type {
8
6
  COMPANY_DATA_QUERY_RESULT,
@@ -14,22 +12,23 @@ export type NavbarData = NonNullable<NAVBAR_QUERY_RESULT>
14
12
  export type FooterData = NonNullable<FOOTER_QUERY_RESULT>
15
13
  export type CompanyData = NonNullable<COMPANY_DATA_QUERY_RESULT>
16
14
 
17
- export const SANITY_LAYOUT_TAGS = ["navbar", "footer", "companyData"]
18
-
19
- export const getNavbar = async (): Promise<NavbarData | null> => {
20
- const { data } = await sanityFetch({ query: NAVBAR_QUERY, tags: ["navbar"] })
21
- return data
15
+ export type SiteLayoutData = {
16
+ navbar: NavbarData | null
17
+ footer: FooterData | null
18
+ companyData: CompanyData | null
22
19
  }
23
20
 
24
- export const getFooter = async (): Promise<FooterData | null> => {
25
- const { data } = await sanityFetch({ query: FOOTER_QUERY, tags: ["footer"] })
26
- return data
27
- }
21
+ export const SANITY_LAYOUT_TAGS = ["navbar", "footer", "companyData"]
28
22
 
29
- export const getCompanyData = async (): Promise<CompanyData | null> => {
30
- const { data } = await sanityFetch({
31
- query: COMPANY_DATA_QUERY,
32
- tags: ["companyData"],
23
+ export const getSiteLayoutData = async (): Promise<SiteLayoutData> => {
24
+ const { data } = await fetchSanity<SiteLayoutData>({
25
+ query: SITE_LAYOUT_QUERY,
26
+ tags: SANITY_LAYOUT_TAGS,
33
27
  })
34
- return data
28
+
29
+ return {
30
+ navbar: data?.navbar ?? null,
31
+ footer: data?.footer ?? null,
32
+ companyData: data?.companyData ?? null,
33
+ }
35
34
  }
@@ -1,4 +1,5 @@
1
1
  import { NextResponse } from "next/server"
2
+ import type { QueryParams } from "next-sanity"
2
3
  import { defineLive } from "next-sanity/live"
3
4
  import { isSanityConfigured } from "@/lib/integrations/check-integration"
4
5
  import { client } from "../client"
@@ -10,8 +11,8 @@ const liveExports =
10
11
  isConfigured && client
11
12
  ? defineLive({
12
13
  client,
13
- browserToken: sanityToken,
14
- serverToken: sanityToken,
14
+ browserToken: sanityToken ?? false,
15
+ serverToken: sanityToken ?? false,
15
16
  })
16
17
  : null
17
18
 
@@ -20,6 +21,32 @@ export const sanityFetch =
20
21
 
21
22
  export const SanityLive = liveExports?.SanityLive ?? (() => null)
22
23
 
24
+ type SanityFetchOptions = {
25
+ query: string
26
+ params?: QueryParams
27
+ tags?: string[]
28
+ stega?: boolean
29
+ perspective?: "published" | "drafts" | "previewDrafts"
30
+ }
31
+
32
+ export async function fetchSanity<T = unknown>({
33
+ query,
34
+ params = {},
35
+ tags = [],
36
+ stega,
37
+ perspective,
38
+ }: SanityFetchOptions): Promise<{ data: T | null }> {
39
+ const { data } = await sanityFetch({
40
+ query,
41
+ params,
42
+ tags,
43
+ ...(typeof stega === "boolean" ? { stega } : {}),
44
+ ...(perspective ? { perspective } : {}),
45
+ })
46
+
47
+ return { data: (data as T | null) ?? null }
48
+ }
49
+
23
50
  export const SanityFetch = async <T,>(
24
51
  slug: string,
25
52
  query: string,
@@ -0,0 +1,118 @@
1
+ import type { Observable } from "rxjs"
2
+ import { map } from "rxjs/operators"
3
+ import type { DocumentStore } from "sanity"
4
+ import {
5
+ type DocumentLocationsState,
6
+ defineDocuments,
7
+ presentationTool,
8
+ } from "sanity/presentation"
9
+ import { HOMEPAGE_DOCUMENT_ID } from "./constants"
10
+ import { previewURL } from "./env"
11
+
12
+ const MAX_FOLDER_DEPTH = 6
13
+
14
+ const buildNestedSlugExpression = (
15
+ parentReferenceField: string,
16
+ leafSlugField: string
17
+ ) => {
18
+ const cases = Array.from({ length: MAX_FOLDER_DEPTH }, (_, index) => {
19
+ const depth = MAX_FOLDER_DEPTH - index
20
+ const parentSlugRefs = Array.from({ length: depth }, (_, parentIndex) => {
21
+ const remainingDepth = depth - parentIndex - 1
22
+ return `${parentReferenceField}${"->parentFolder".repeat(remainingDepth)}->slug.current`
23
+ })
24
+
25
+ const pathDefinedChecks = parentSlugRefs
26
+ .map((field) => `defined(${field})`)
27
+ .join(" && ")
28
+
29
+ const childPath = [...parentSlugRefs, leafSlugField].join(' + "/" + ')
30
+ const folderRootPath = parentSlugRefs.join(' + "/" + ')
31
+
32
+ return [
33
+ `${pathDefinedChecks} && ${leafSlugField} == "/" => ${folderRootPath}`,
34
+ `${pathDefinedChecks} && defined(${leafSlugField}) => ${childPath}`,
35
+ ]
36
+ }).flat()
37
+
38
+ return `select(${cases.join(",\n ")},${leafSlugField})`
39
+ }
40
+
41
+ const pageResolvedSlugExpression = buildNestedSlugExpression(
42
+ "pageFolder",
43
+ "slug.current"
44
+ )
45
+
46
+ const CONTENT_TYPE_PREFIXES: Record<string, string> = {
47
+ blogContent: "blog",
48
+ }
49
+
50
+ const resolveSlugLocation = <T extends { title?: string }>(
51
+ documentStore: DocumentStore,
52
+ id: string,
53
+ projection: string,
54
+ toHref: (doc: T) => string | null,
55
+ fallbackTitle: string
56
+ ): Observable<DocumentLocationsState> =>
57
+ documentStore
58
+ .listenQuery(`*[_id == $id][0]{ ${projection} }`, { id }, {})
59
+ .pipe(
60
+ map((doc: T | null) => {
61
+ const href = doc ? toHref(doc) : null
62
+ if (!href) return { locations: [] }
63
+ return { locations: [{ title: doc?.title || fallbackTitle, href }] }
64
+ })
65
+ )
66
+
67
+ export const presentation = presentationTool({
68
+ name: "preview",
69
+ title: "Preview",
70
+ resolve: {
71
+ mainDocuments: defineDocuments([
72
+ {
73
+ route: "/",
74
+ filter: `_type == "page" && _id == "${HOMEPAGE_DOCUMENT_ID}"`,
75
+ },
76
+ ...Object.entries(CONTENT_TYPE_PREFIXES).map(([type, prefix]) => ({
77
+ route: `/${prefix}/:slug`,
78
+ filter: `_type == "${type}" && slug.current == $slug`,
79
+ })),
80
+ {
81
+ route: "/:slug+",
82
+ filter: `_type == "page" && ${pageResolvedSlugExpression} == $slug`,
83
+ },
84
+ ]),
85
+ locations: (params, { documentStore }) => {
86
+ if (params.type === "page") {
87
+ if (params.id === HOMEPAGE_DOCUMENT_ID) {
88
+ return { locations: [{ title: "Homepage", href: "/" }] }
89
+ }
90
+ return resolveSlugLocation<{ title?: string; resolvedSlug?: string }>(
91
+ documentStore,
92
+ params.id,
93
+ `title, "resolvedSlug": ${pageResolvedSlugExpression}`,
94
+ (doc) => (doc.resolvedSlug ? `/${doc.resolvedSlug}` : null),
95
+ "Page"
96
+ )
97
+ }
98
+
99
+ const prefix = CONTENT_TYPE_PREFIXES[params.type]
100
+ if (!prefix) return null
101
+
102
+ return resolveSlugLocation<{ title?: string; slug?: string }>(
103
+ documentStore,
104
+ params.id,
105
+ `title, "slug": slug.current`,
106
+ (doc) => (doc.slug ? `/${prefix}/${doc.slug}` : null),
107
+ "Untitled"
108
+ )
109
+ },
110
+ },
111
+ previewUrl: {
112
+ origin: previewURL,
113
+ draftMode: {
114
+ enable: "/api/draft-mode/enable",
115
+ disable: "/api/draft-mode/disable",
116
+ },
117
+ },
118
+ })