bsmnt 0.3.3 → 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 (70) hide show
  1. package/dist/helpers/integrate/merge-config.js +1 -1
  2. package/dist/helpers/integrate/merge-config.js.map +1 -1
  3. package/dist/helpers/integrate/merge-orchestrator.d.ts.map +1 -1
  4. package/dist/helpers/integrate/merge-orchestrator.js +5 -5
  5. package/dist/helpers/integrate/merge-orchestrator.js.map +1 -1
  6. package/dist/helpers/integrate/sanity/config.d.ts.map +1 -1
  7. package/dist/helpers/integrate/sanity/config.js +2 -1
  8. package/dist/helpers/integrate/sanity/config.js.map +1 -1
  9. package/dist/helpers/integrate/sanity/mergers/sitemap-merger.d.ts +1 -3
  10. package/dist/helpers/integrate/sanity/mergers/sitemap-merger.d.ts.map +1 -1
  11. package/dist/helpers/integrate/sanity/mergers/sitemap-merger.js +117 -76
  12. package/dist/helpers/integrate/sanity/mergers/sitemap-merger.js.map +1 -1
  13. package/package.json +1 -1
  14. package/src/helpers/integrate/sanity/files/app/api/blog/[slug]/route.ts +2 -1
  15. package/src/helpers/integrate/sanity/files/app/api/revalidate/route.ts +4 -1
  16. package/src/helpers/integrate/sanity/files/app/blog/[slug]/page.tsx +3 -1
  17. package/src/helpers/integrate/sanity/files/app/layout.tsx +2 -2
  18. package/src/helpers/integrate/sanity/files/app/sitemap.md/route.ts +29 -18
  19. package/src/helpers/integrate/sanity/files/lib/integrations/sanity/WEBHOOK-SETUP.md +74 -0
  20. package/src/helpers/integrate/sanity/files/lib/integrations/sanity/env.ts +4 -2
  21. package/src/helpers/integrate/sanity/files/lib/integrations/sanity/queries.ts +42 -0
  22. package/src/helpers/integrate/sanity/files/lib/integrations/sanity/sitemap.ts +90 -0
  23. package/src/helpers/integrate/sanity/files/lib/utils/metadata.ts +2 -2
  24. package/src/helpers/integrate/sanity/files/lib/utils/url.ts +23 -0
  25. package/src/templates/next-default/.env.example +3 -3
  26. package/src/templates/next-default/app/layout.tsx +2 -2
  27. package/src/templates/next-default/app/robots.ts +2 -2
  28. package/src/templates/next-default/app/sitemap.xml/route.ts +51 -0
  29. package/src/templates/next-default/lib/utils/metadata.ts +2 -2
  30. package/src/templates/next-default/lib/utils/url.ts +16 -0
  31. package/src/templates/next-experiments/.env.example +3 -3
  32. package/src/templates/next-experiments/app/layout.tsx +2 -2
  33. package/src/templates/next-experiments/app/robots.ts +0 -4
  34. package/src/templates/next-experiments/lib/utils/metadata.ts +2 -2
  35. package/src/templates/next-experiments/lib/utils/url.ts +16 -0
  36. package/src/templates/next-pagebuilder/app/(content)/[[...slug]]/page.tsx +17 -8
  37. package/src/templates/next-pagebuilder/app/(content)/layout.tsx +19 -7
  38. package/src/templates/next-pagebuilder/app/actions/refresh.ts +5 -0
  39. package/src/templates/next-pagebuilder/components/layout/footer/index.tsx +15 -19
  40. package/src/templates/next-pagebuilder/components/layout/header/index.tsx +3 -5
  41. package/src/templates/next-pagebuilder/components/layout/json-ld/index.tsx +11 -10
  42. package/src/templates/next-pagebuilder/components/layout/wrapper/index.tsx +14 -4
  43. package/src/templates/next-pagebuilder/components/page-builder/components/{post-collection → content-collection}/content-card.tsx +3 -5
  44. package/src/templates/next-pagebuilder/components/page-builder/components/content-collection/content-filters.tsx +93 -0
  45. package/src/templates/next-pagebuilder/components/page-builder/components/{post-collection → content-collection}/content-grid.tsx +7 -9
  46. package/src/templates/next-pagebuilder/components/page-builder/components/content-collection/content-pagination-nav.tsx +71 -0
  47. package/src/templates/next-pagebuilder/components/page-builder/components/content-collection/index.tsx +212 -0
  48. package/src/templates/next-pagebuilder/components/page-builder/components/{post-collection → content-collection}/types.ts +5 -4
  49. package/src/templates/next-pagebuilder/components/page-builder/renderer.tsx +13 -5
  50. package/src/templates/next-pagebuilder/components/page-document/index.tsx +9 -4
  51. package/src/templates/next-pagebuilder/components/sanity/visual-editing.tsx +2 -1
  52. package/src/templates/next-pagebuilder/lib/integrations/sanity/constants.ts +1 -0
  53. package/src/templates/next-pagebuilder/lib/integrations/sanity/fetchers/layout.ts +17 -18
  54. package/src/templates/next-pagebuilder/lib/integrations/sanity/live/index.tsx +29 -2
  55. package/src/templates/next-pagebuilder/lib/integrations/sanity/presentation.ts +118 -0
  56. package/src/templates/next-pagebuilder/lib/integrations/sanity/queries.ts +144 -31
  57. package/src/templates/next-pagebuilder/lib/integrations/sanity/sanity.config.ts +5 -100
  58. package/src/templates/next-pagebuilder/next.config.ts +3 -0
  59. package/src/templates/next-pagebuilder/package.json +1 -2
  60. package/src/templates/next-webgl/.env.example +3 -3
  61. package/src/templates/next-webgl/app/layout.tsx +2 -2
  62. package/src/templates/next-webgl/app/robots.ts +2 -2
  63. package/src/templates/next-webgl/app/sitemap.xml/route.ts +51 -0
  64. package/src/templates/next-webgl/lib/utils/metadata.ts +2 -2
  65. package/src/templates/next-webgl/lib/utils/url.ts +16 -0
  66. package/src/helpers/integrate/sanity/files/app/sitemap.ts +0 -61
  67. package/src/templates/next-default/app/sitemap.ts +0 -16
  68. package/src/templates/next-experiments/app/sitemap.ts +0 -16
  69. package/src/templates/next-pagebuilder/components/page-builder/components/post-collection/index.tsx +0 -28
  70. package/src/templates/next-webgl/app/sitemap.ts +0 -16
@@ -0,0 +1,93 @@
1
+ "use client"
2
+
3
+ import { usePathname, useRouter, useSearchParams } from "next/navigation"
4
+ import { cn } from "@/lib/styles/cn"
5
+ import type { ContentCategory } from "./types"
6
+
7
+ type ContentFiltersProps = {
8
+ categories: ContentCategory[]
9
+ activeCategory: string
10
+ totalCount: number
11
+ filteredCount: number
12
+ }
13
+
14
+ function FilterChip({
15
+ active,
16
+ label,
17
+ onClick,
18
+ }: {
19
+ active: boolean
20
+ label: string
21
+ onClick: () => void
22
+ }) {
23
+ return (
24
+ <button
25
+ type="button"
26
+ onClick={onClick}
27
+ className={cn(
28
+ "rounded-full border px-4 py-2 text-sm transition-colors",
29
+ active
30
+ ? "border-black bg-black text-white"
31
+ : "border-black/10 bg-white hover:border-black/30"
32
+ )}
33
+ >
34
+ {label}
35
+ </button>
36
+ )
37
+ }
38
+
39
+ export function ContentFilters({
40
+ categories,
41
+ activeCategory,
42
+ totalCount,
43
+ filteredCount,
44
+ }: ContentFiltersProps) {
45
+ const pathname = usePathname()
46
+ const router = useRouter()
47
+ const searchParams = useSearchParams()
48
+
49
+ const navigate = (category: string) => {
50
+ const nextParams = new URLSearchParams(searchParams.toString())
51
+
52
+ if (category) nextParams.set("category", category)
53
+ else nextParams.delete("category")
54
+
55
+ nextParams.delete("page")
56
+
57
+ const href = nextParams.toString()
58
+ ? `${pathname}?${nextParams.toString()}`
59
+ : pathname
60
+
61
+ // biome-ignore lint/suspicious/noExplicitAny: typed routes do not accept dynamic query string helpers cleanly
62
+ router.push(href as any, { scroll: false })
63
+ }
64
+
65
+ return (
66
+ <div className="flex flex-col gap-4 rounded-[1.5rem] border border-black/10 bg-black/[0.02] p-4 sm:p-5">
67
+ <div className="flex flex-wrap gap-2">
68
+ <FilterChip
69
+ active={activeCategory === ""}
70
+ label="All content"
71
+ onClick={() => navigate("")}
72
+ />
73
+ {categories.map((category) => {
74
+ const slug = category.slug?.current
75
+ if (!slug) return null
76
+
77
+ return (
78
+ <FilterChip
79
+ key={category._id}
80
+ active={activeCategory === slug}
81
+ label={category.title || "Untitled"}
82
+ onClick={() => navigate(slug)}
83
+ />
84
+ )
85
+ })}
86
+ </div>
87
+ <p className="text-current/60 text-sm">
88
+ Showing {filteredCount} {filteredCount === 1 ? "item" : "items"}
89
+ {filteredCount !== totalCount ? ` out of ${totalCount}` : ""}
90
+ </p>
91
+ </div>
92
+ )
93
+ }
@@ -1,20 +1,20 @@
1
- import type { BlogArticleCard } from "./types"
2
1
  import { ContentCard } from "./content-card"
2
+ import type { ContentItemCard } from "./types"
3
3
 
4
4
  type ContentGridProps = {
5
- articles: BlogArticleCard[]
5
+ items: ContentItemCard[]
6
6
  start?: number
7
7
  end?: number
8
8
  className?: string
9
9
  }
10
10
 
11
11
  export function ContentGrid({
12
- articles,
12
+ items,
13
13
  start = 0,
14
14
  end,
15
15
  className,
16
16
  }: ContentGridProps) {
17
- const slice = articles.slice(start, end)
17
+ const slice = items.slice(start, end)
18
18
 
19
19
  if (slice.length === 0) {
20
20
  if (start === 0) {
@@ -29,13 +29,11 @@ export function ContentGrid({
29
29
  className ?? "grid grid-cols-2 gap-8 md:grid-cols-2 lg:grid-cols-3"
30
30
  }
31
31
  >
32
- {slice.map((article) => {
33
- const slug = article.resolvedSlug
32
+ {slice.map((item) => {
33
+ const slug = item.resolvedSlug
34
34
  if (!slug) return null
35
35
 
36
- return (
37
- <ContentCard key={article._id} article={article} />
38
- )
36
+ return <ContentCard key={item._id} article={item} />
39
37
  })}
40
38
  </ul>
41
39
  )
@@ -0,0 +1,71 @@
1
+ "use client"
2
+
3
+ import { usePathname, useSearchParams } from "next/navigation"
4
+ import { Link } from "@/components/ui/link"
5
+
6
+ type ContentPaginationNavProps = {
7
+ currentPage: number
8
+ totalPages: number
9
+ }
10
+
11
+ function buildPaginationHref({
12
+ pathname,
13
+ page,
14
+ searchParams,
15
+ }: {
16
+ pathname: string
17
+ page: number
18
+ searchParams: ReturnType<typeof useSearchParams>
19
+ }) {
20
+ const nextParams = new URLSearchParams(searchParams.toString())
21
+
22
+ if (page > 1) nextParams.set("page", String(page))
23
+ else nextParams.delete("page")
24
+
25
+ const query = nextParams.toString()
26
+ return query ? `${pathname}?${query}` : pathname
27
+ }
28
+
29
+ export function ContentPaginationNav({
30
+ currentPage,
31
+ totalPages,
32
+ }: ContentPaginationNavProps) {
33
+ const pathname = usePathname()
34
+ const searchParams = useSearchParams()
35
+
36
+ if (!pathname || totalPages <= 1) return null
37
+
38
+ return (
39
+ <nav className="flex flex-wrap items-center gap-2" aria-label="Pagination">
40
+ {currentPage > 1 ? (
41
+ <Link
42
+ href={buildPaginationHref({
43
+ pathname,
44
+ page: currentPage - 1,
45
+ searchParams,
46
+ })}
47
+ className="rounded-full border border-black/10 px-4 py-2 text-sm transition-colors hover:border-black/30"
48
+ >
49
+ Previous
50
+ </Link>
51
+ ) : null}
52
+
53
+ <span className="text-current/60 text-sm">
54
+ Page {currentPage} of {totalPages}
55
+ </span>
56
+
57
+ {currentPage < totalPages ? (
58
+ <Link
59
+ href={buildPaginationHref({
60
+ pathname,
61
+ page: currentPage + 1,
62
+ searchParams,
63
+ })}
64
+ className="rounded-full border border-black/10 px-4 py-2 text-sm transition-colors hover:border-black/30"
65
+ >
66
+ Next
67
+ </Link>
68
+ ) : null}
69
+ </nav>
70
+ )
71
+ }
@@ -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,