Skip to content

Commit 738e875

Browse files
committed
Sync starter from monorepo
1 parent af513ab commit 738e875

File tree

12 files changed

+7105
-40
lines changed

12 files changed

+7105
-40
lines changed

LICENSE

Lines changed: 338 additions & 0 deletions
Large diffs are not rendered by default.

app/[...slug]/page.tsx

Lines changed: 34 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@ import { EntityRenderer } from "@/components/entity"
44
import { ViewRenderer } from "@/components/view"
55

66
interface PageProps {
7-
params: Promise<{ slug: string[] }>
7+
params: { slug: string[] }
8+
searchParams?: Record<string, string | string[] | undefined>
89
}
910

1011
/**
@@ -39,11 +40,12 @@ const DEFAULT_INCLUDES = [
3940
* 4. Fetches the resource via JSON:API with media includes
4041
* 5. Renders with the appropriate component
4142
*/
42-
export default async function Page({ params }: PageProps) {
43-
const { slug } = await params
44-
const path = "/" + slug.join("/")
43+
export default async function Page({ params, searchParams }: PageProps) {
44+
const path = "/" + params.slug.join("/")
4545

4646
try {
47+
const query = searchParamsToString(searchParams)
48+
4749
// Resolve the path to a Drupal resource
4850
const resolved = await resolvePath(path)
4951

@@ -64,8 +66,9 @@ export default async function Page({ params }: PageProps) {
6466

6567
// Handle views (headless)
6668
if (resolved.kind === "view" && resolved.data_url) {
67-
const doc = await fetchView(resolved.data_url)
68-
return <ViewRenderer doc={doc} />
69+
const dataUrl = query ? `${resolved.data_url}?${query}` : resolved.data_url
70+
const doc = await fetchView(dataUrl)
71+
return <ViewRenderer doc={doc} currentPath={path} />
6972
}
7073

7174
// Handle entities
@@ -107,8 +110,7 @@ export default async function Page({ params }: PageProps) {
107110
* Generate metadata for SEO.
108111
*/
109112
export async function generateMetadata({ params }: PageProps) {
110-
const { slug } = await params
111-
const path = "/" + slug.join("/")
113+
const path = "/" + params.slug.join("/")
112114

113115
try {
114116
const resolved = await resolvePath(path)
@@ -135,3 +137,27 @@ export async function generateMetadata({ params }: PageProps) {
135137
return {}
136138
}
137139
}
140+
141+
function searchParamsToString(
142+
searchParams: Record<string, string | string[] | undefined> | undefined
143+
): string {
144+
if (!searchParams) {
145+
return ""
146+
}
147+
148+
const params = new URLSearchParams()
149+
150+
for (const [key, value] of Object.entries(searchParams)) {
151+
if (typeof value === "string") {
152+
params.set(key, value)
153+
continue
154+
}
155+
if (Array.isArray(value)) {
156+
for (const v of value) {
157+
params.append(key, v)
158+
}
159+
}
160+
}
161+
162+
return params.toString()
163+
}

app/api/revalidate/route.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -101,10 +101,12 @@ export async function POST(request: NextRequest) {
101101
}
102102
}
103103

104-
console.log(
105-
`[Revalidate] ${operation} - Revalidated ${revalidated.tags.length} tags, ${revalidated.paths.length} paths`,
106-
entity ? `(${entity.type}/${entity.bundle}/${entity.uuid})` : ""
107-
)
104+
if (process.env.NODE_ENV !== "production") {
105+
console.warn(
106+
`[Revalidate] ${operation} - Revalidated ${revalidated.tags.length} tags, ${revalidated.paths.length} paths`,
107+
entity ? `(${entity.type}/${entity.bundle}/${entity.uuid})` : ""
108+
)
109+
}
108110

109111
return NextResponse.json({
110112
revalidated: true,

app/not-found.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ export default function NotFound() {
66
<h1 className="text-6xl font-bold text-gray-300 mb-4">404</h1>
77
<h2 className="text-2xl font-semibold mb-4">Page Not Found</h2>
88
<p className="text-gray-600 mb-8">
9-
The page you're looking for doesn't exist or has been moved.
9+
The page you&apos;re looking for doesn&apos;t exist or has been moved.
1010
</p>
1111
<Link
1212
href="/"

app/page.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import { resolvePath, fetchJsonApi } from "@/lib/drupal"
22
import { EntityRenderer } from "@/components/entity"
3-
import { notFound } from "next/navigation"
43

54
/**
65
* Homepage - resolves the root path "/" from Drupal.

components/media/BodyContent.tsx

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ import { JsonApiResource } from "@/lib/drupal/types"
66
import {
77
extractMedia,
88
extractEmbeddedMediaUuids,
9-
findIncluded,
109
DrupalMediaData,
1110
} from "@/lib/drupal/media"
1211
import { resolveFileUrl } from "@/lib/drupal/url"
@@ -181,7 +180,7 @@ function renderWithEmbeddedMedia(
181180
imagePreset: "full" | "thumbnail" | "medium" | "large" | "hero"
182181
): React.ReactNode[] {
183182
// Split by drupal-media tags
184-
const parts = html.split(/(<drupal-media[^>]*>.*?<\/drupal-media>)/gs)
183+
const parts = html.split(/(<drupal-media[^>]*>[\s\S]*?<\/drupal-media>)/g)
185184

186185
return parts.map((part, index) => {
187186
// Check if this part is a drupal-media tag

components/view/ViewRenderer.tsx

Lines changed: 10 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -73,20 +73,14 @@ function ViewItem({ item }: { item: JsonApiResource }) {
7373
}
7474

7575
/**
76-
* Extract page number from JSON:API pagination URL.
77-
* JSON:API uses `page[offset]` parameter for pagination.
76+
* Extract query string from JSON:API pagination URL.
7877
*/
79-
function extractPageFromUrl(url: string): number | null {
78+
function extractSearchFromUrl(url: string): string {
8079
try {
8180
const urlObj = new URL(url, "http://dummy")
82-
const offset = urlObj.searchParams.get("page[offset]")
83-
if (offset) {
84-
// JSON:API offset / items per page (assuming 10 per page)
85-
return Math.floor(parseInt(offset, 10) / 10) + 1
86-
}
87-
return null
81+
return urlObj.search
8882
} catch {
89-
return null
83+
return ""
9084
}
9185
}
9286

@@ -104,13 +98,13 @@ function ViewPagination({
10498

10599
if (!nextUrl && !prevUrl) return null
106100

107-
const nextPage = nextUrl ? extractPageFromUrl(nextUrl) : null
108-
const prevPage = prevUrl ? extractPageFromUrl(prevUrl) : null
109-
110-
// Build frontend pagination URLs using query parameters
111101
const basePath = currentPath || ""
112-
const prevHref = prevPage ? `${basePath}?page=${prevPage}` : null
113-
const nextHref = nextPage ? `${basePath}?page=${nextPage}` : null
102+
103+
const prevSearch = prevUrl ? extractSearchFromUrl(prevUrl) : ""
104+
const nextSearch = nextUrl ? extractSearchFromUrl(nextUrl) : ""
105+
106+
const prevHref = prevSearch ? `${basePath}${prevSearch}` : null
107+
const nextHref = nextSearch ? `${basePath}${nextSearch}` : null
114108

115109
return (
116110
<nav

lib/drupal/fetch.ts

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,13 @@ export async function fetchJsonApi<T = JsonApiDocument>(
127127
export async function fetchView<T = JsonApiDocument>(
128128
dataUrl: string,
129129
options?: {
130-
page?: number
130+
/**
131+
* JSON:API pagination parameters.
132+
*
133+
* - number: sets page[offset]
134+
* - object: sets page[offset] and/or page[limit]
135+
*/
136+
page?: number | { offset?: number; limit?: number }
131137
revalidate?: number
132138
/** Additional cache tags to include */
133139
tags?: string[]
@@ -140,9 +146,18 @@ export async function fetchView<T = JsonApiDocument>(
140146

141147
const url = new URL(dataUrl, base)
142148

143-
// Add pagination if specified
149+
// Add pagination if specified.
144150
if (options?.page !== undefined) {
145-
url.searchParams.set("page", String(options.page))
151+
if (typeof options.page === "number") {
152+
url.searchParams.set("page[offset]", String(options.page))
153+
} else {
154+
if (options.page.offset !== undefined) {
155+
url.searchParams.set("page[offset]", String(options.page.offset))
156+
}
157+
if (options.page.limit !== undefined) {
158+
url.searchParams.set("page[limit]", String(options.page.limit))
159+
}
160+
}
146161
}
147162

148163
// Build cache tags

lib/drupal/media.ts

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
*/
44

55
import { JsonApiResource, JsonApiRelationship } from "./types"
6-
import { getFileUrl, resolveFileUrl } from "./url"
6+
import { getFileUrl } from "./url"
77

88
/**
99
* Extracted image data ready for rendering.
@@ -104,6 +104,21 @@ export function extractImageFromFile(
104104
}
105105
}
106106

107+
function getRelationshipMetaString(
108+
relationship: JsonApiRelationship | undefined,
109+
key: string
110+
): string | undefined {
111+
const data = relationship?.data
112+
if (!data || Array.isArray(data)) {
113+
return undefined
114+
}
115+
116+
const meta = (data as { meta?: Record<string, unknown> }).meta
117+
const value = meta?.[key]
118+
119+
return typeof value === "string" && value.trim() !== "" ? value : undefined
120+
}
121+
107122
/**
108123
* Extract media data from a media entity.
109124
*
@@ -154,9 +169,13 @@ export function extractMedia(
154169
const imageData = extractImageFromFile(file)
155170
if (imageData) {
156171
result.url = imageData.src
172+
const alt = getRelationshipMetaString(fileRelationship, "alt")
173+
const title = getRelationshipMetaString(fileRelationship, "title")
174+
157175
result.image = {
158176
...imageData,
159-
alt: (attrs?.field_media_image?.alt as string) || imageData.alt || name,
177+
alt: alt || imageData.alt || name,
178+
title: title || imageData.title,
160179
}
161180
}
162181
}

lib/drupal/types.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ export type ResolveResponse =
1111
id: string
1212
langcode: string
1313
}
14-
redirect: null
14+
redirect: { to: string; status?: number } | null
1515
jsonapi_url: string
1616
data_url: null
1717
/** Whether this entity type is enabled for headless rendering. */
@@ -24,7 +24,7 @@ export type ResolveResponse =
2424
kind: "view"
2525
canonical: string
2626
entity: null
27-
redirect: null
27+
redirect: { to: string; status?: number } | null
2828
jsonapi_url: null
2929
data_url: string
3030
/** Whether this View is enabled for headless rendering. */
@@ -37,7 +37,7 @@ export type ResolveResponse =
3737
kind: null
3838
canonical: null
3939
entity: null
40-
redirect: null
40+
redirect: { to: string; status?: number } | null
4141
jsonapi_url: null
4242
data_url: null
4343
headless: false

0 commit comments

Comments
 (0)