The whole code structure is a bit complex because I do a whole bunch of other trickery this way during the build. I hope these few snippets can get you going (I’m omitting a few specifics, import statements, and type annotations etc.).
There are basically 4 files involved — the server-rendered page.tsx
that calls the builder API and extractData
; the client component that renders builder content inside the context provider; the context file (separated so it can be cleanly imported); and the extractData
function itself.
// [[...path]]/page.tsx
builder.init(process.env.NEXT_PUBLIC_BUILDER_API_KEY!)
export default async function Page ({ params: { path } }) {
const pageContent = await builder.get('page', {/* settings */})
const extractedData = pageContent ? await extractData(pageContent) : {}
/* ...more site-specific setup */
return (
<RenderBuilderContent content={pageContent}
extractedData={extractedData}
model="page"
/>
)
}
// RenderBuilderContent.ts
// in it's own file because the BuilderComponent needs 'use client'
'use client'
export function RenderBuilderContent ({ content, extractedData, model }) {
/* ...more site-specific setup */
return (
<ExtractedDataContext.Provider value={extractedData}>
<BuilderComponent
content={content}
model={model}
/>
</ExtractedDataContext.Provider>
)
}
// ExtractedDataContext.ts
import { createContext } from 'react'
export default createContext<Record<string, any>>({})
// extractData.ts
// get the value of a key from a JSON string
// this could theoretically also be achieved with a walker function, but since builder blocks have a well-known shape this is fine
const valueForKeyRegExp = (key: string) => new RegExp(`("${key}":\\s?")([^"]+)(")`, 'gm')
export default async function extractData (page) {
const extractedData = {
images: {},
priorityAssets: [],
/* other stuff we want to get */
}
async function getImageData ({ url }) {
try {
// pull in image data directly from the builder API endpoint
const result = await require('probe-image-size')(url, { open_timeout: 30000, follow_max: 10 })
// and store them using the URL as the key
extractedData.images[url] = { ...result }
} catch (e) {
console.warn(e)
}
}
const blocks = page?.data?.blocks
for (const [i, block] of (blocks ?? []).entries()) {
const text = JSON.stringify(block)
for (const key of ['image', 'poster', /* ...any other keys used for images */]) {
const matches = text.matchAll(valueForKeyRegExp(key))
for (const match of matches) {
const url = match[2]
if (url) {
await getImageData({ url })
// get assets from the first 2 blocks and mark them for fetchpriority
if (i <= 2) {
extractedData.priorityAssets.push(url)
}
}
}
}
}
return extractedData
}
I’ve been thinking that I could also just append the extractedData
to the API response itself (i.e. the content
object) instead of using a Context, but haven’t gotten around to testing this.
Hope this helps!