By default, Builder pages offer a graphical UI for Search Engine Optimization (SEO). To take the best possible advantage of SEO with Builder.io content, make sure you provide metadata for search engines.
Page metadata is the data about your page that search engines use to display information in search results. For any page you publish, make sure that you include, at the very least, a title and description for your page.
The built-in Page model in Builder includes title and description fields by default. To specify a title and description on a page content entry made using the default Builder Page model:
Go to Content . Click on the page to open it in the Visual Editor. In the Options tab, expand the section titled Docs Content Fields . Enter the title and description for your page. The following video demonstrates filling out the title and description metadata in a page created with the default page model in Builder:
Framework
React Vue Qwik (Beta) REST API Angular
Meta-Framework
Next.js (Pages Router) Gatsby
Gen 1 (Recommended) // Next.js example with SEO custom fields
import Head from "next/head";
import { builder, BuilderComponent } from "@builder.io/react";
export default class extends React.Component {
static async getInitialProps({ res, req, asPath }) {
const page = await builder.get("page", { req, res }).promise();
return { builderPage: page };
}
render() {
const page = this.props.builderPage;
return (
<>
<Head>
<title>{page.data.title}</title>
</Head>
<BuilderComponent name="page" content={page} />
</>
);
}
}// src/pages/[...index].vue
<script setup>
import { ref, onMounted, watch, computed } from 'vue';
import { useRoute, useHead } from 'vue-router';
import { Content, fetchOneEntry, isPreviewing } from '@builder.io/sdk-vue';
const BUILDER_PUBLIC_API_KEY = 'YOUR_API_KEY';
const BUILDER_MODEL = 'page';
const route = useRoute();
const content = ref(null);
// Fetch builder content
const fetchContent = async () => {
const builderContent = await fetchOneEntry({
model: BUILDER_MODEL,
apiKey: BUILDER_PUBLIC_API_KEY,
userAttributes: {
urlPath: route.path,
},
});
content.value = builderContent;
};
// Determine if it should show Builder content
const shouldRenderBuilder = computed(() => {
return content.value || isPreviewing();
});
// Set up SEO metadata
useHead(() => {
if (!content.value?.data) {
return {
title: 'Default Title',
meta: [
{ name: 'description', content: 'Default Description' }
]
};
}
return {
title: content.value.data.title || 'Default Title',
meta: [
{ name: 'description', content: content.value.data.description || 'Default Description' }
]
};
});
onMounted(fetchContent);
// Refetch content when route changes
watch(() => route.path, fetchContent);
</script>
<template>
<div>
<template v-if="shouldRenderBuilder">
<Content
:model="BUILDER_MODEL"
:content="content"
:apiKey="BUILDER_PUBLIC_API_KEY"
/>
</template>
<template v-else>
<p>Not Found</p>
</template>
</div>
</template>// pages/[...slug].vue
<script setup>
import { ref, computed, useHead, useRoute } from 'nuxt/app';
import { Content, fetchOneEntry, isPreviewing } from '@builder.io/sdk-vue';
const BUILDER_PUBLIC_API_KEY = 'YOUR_API_KEY';
const BUILDER_MODEL = 'page';
const route = useRoute();
const content = ref(null);
// Fetch builder content
const fetchContent = async () => {
try {
const builderContent = await fetchOneEntry({
model: BUILDER_MODEL,
apiKey: BUILDER_PUBLIC_API_KEY,
userAttributes: {
urlPath: route.path,
},
});
content.value = builderContent;
} catch (error) {
console.error('Error fetching Builder content:', error);
}
};
// Determine if we should show Builder content
const shouldRenderBuilder = computed(() => {
return content.value || isPreviewing();
});
// Set up SEO metadata
useHead(() => {
if (!content.value?.data) {
return {
title: 'Default Title',
meta: [
{ name: 'description', content: 'Default Description' }
]
};
}
return {
title: content.value.data.title || 'Default Title',
meta: [
{ name: 'description', content: content.value.data.description || 'Default Description' }
]
};
});
// Fetch content on component mount
onMounted(fetchContent);
// Watch for route changes to refetch content
watch(() => route.path, fetchContent);
</script>
<template>
<div>
<template v-if="shouldRenderBuilder">
<Content
:model="BUILDER_MODEL"
:content="content"
:apiKey="BUILDER_PUBLIC_API_KEY"
/>
</template>
<template v-else>
<p>Not Found</p>
</template>
</div>
</template>// src/routes/[...index]/index.tsx
import { component$ } from '@builder.io/qwik';
import { routeLoader$ } from '@builder.io/qwik-city';
import { useDocumentHead, useLocation } from '@builder.io/qwik-city';
import {
Content,
fetchOneEntry,
getBuilderSearchParams,
} from '@builder.io/sdk-qwik';
export const BUILDER_PUBLIC_API_KEY = 'YOUR_API_KEY';
export const BUILDER_MODEL = 'page';
// Define a loader for Builder content with metadata
export const useBuilderContent = routeLoader$(async ({ url }) => {
const builderContent = await fetchOneEntry({
model: BUILDER_MODEL,
apiKey: BUILDER_PUBLIC_API_KEY,
options: getBuilderSearchParams(url.searchParams),
userAttributes: {
urlPath: url.pathname,
},
});
return builderContent;
});
export default component$(() => {
const content = useBuilderContent();
const head = useDocumentHead();
const loc = useLocation();
// Set SEO metadata if content exists
if (content.value?.data) {
head.title = content.value.data.title || 'Default Title';
head.meta.push({
name: 'description',
content: content.value.data.description || 'Default Description'
});
}
return (
<Content
model={BUILDER_MODEL}
content={content.value}
apiKey={BUILDER_PUBLIC_API_KEY}
/>
);
});// landing-page.component.ts
import { Component } from '@angular/core';
import { Content, BuilderContent } from '@builder.io/sdk-angular';
import { Meta, Title } from '@angular/platform-browser';
@Component({
selector: 'app-landing-page',
standalone: true,
imports: [Content],
template: `
<builder-content
*ngIf="content"
[content]="content"
[customComponents]="customComponents"
/>
`
})
export class LandingPageComponent {
content: BuilderContent | null = null;
customComponents = []; // Your custom components
constructor(
private meta: Meta,
private title: Title
) {}
ngOnInit() {
if (this.content?.data) {
// Set basic metadata from Builder content
this.title.setTitle(this.content.data.title || 'Default Title');
this.meta.updateTag({
name: 'description',
content: this.content.data.description || 'Default Description'
});
}
}
}// page.resolver.ts
import { ResolveFn } from '@angular/router';
import { BuilderContent } from '@builder.io/sdk-angular-v2';
import { inject } from '@angular/core';
import { BuilderService } from './builder.service';
export const pageResolver: ResolveFn<BuilderContent | null> = () => {
const builderService = inject(BuilderService);
return builderService.getContent('page');
};
// landing-page.component.ts
import { Component, inject } from '@angular/core';
import { BuilderContent } from '@builder.io/sdk-angular-v2';
import { Meta, Title } from '@angular/platform-browser';
import { ActivatedRoute } from '@angular/router';
import { firstValueFrom } from 'rxjs';
@Component({
selector: 'app-landing-page',
standalone: true,
template: `
<builder-component
*ngIf="content"
[content]="content"
model="page">
</builder-component>
`
})
export class LandingPageComponent {
content?: BuilderContent;
private route = inject(ActivatedRoute);
private meta = inject(Meta);
private title = inject(Title);
async ngOnInit() {
// Get resolved content from route
this.content = await firstValueFrom(this.route.data)
.then(data => data['content']);
if (this.content?.data) {
// Set metadata during SSR
this.title.setTitle(this.content.data.title || 'Default Title');
this.meta.updateTag({
name: 'description',
content: this.content.data.description || 'Default Description'
});
}
}
}// Gatsby example with SEO custom fields
import { BuilderComponent, builder } from '@builder.io/react';
import { Helmet } from 'react-helmet';
export default ({ data }) => {
const content = data.allBuilderModels.page[0].content;
return (
<>
<Helmet>
<title>{content.data.title}</title>
</Helmet>
<BuilderComponent content={content} />
</>
);
};
export const query = graphql`
query($path: String!) {
allBuilderModels {
page(target: { urlPath: $path }, limit: 1) {
content
}
}
}
`;// Node.js example
// Run when your code doesn't match a page to check Builder for one
let page = await request(
'https://cdn.builder.io/api/v1/html/page?url=' + encodeURIComponent(request.url) + '&apiKey=' + apiKey
)
if (page) {
// Use any templating language, just put the html between your header and footer
res.send(`
<head>
<title>${page.data.title}</title>
</head>
<body>
/* Your header */
${page.data.html}
/* Your footer */
</body>
`);
} else {
// Send 404 page
}// landing-page.component.ts
import { Component } from '@angular/core';
import { BuilderComponent, BuilderContent } from '@builder.io/angular';
import { Meta, Title } from '@angular/platform-browser';
@Component({
selector: 'app-landing-page',
template: `
<builder-component
*ngIf="content"
[model]="'page'"
[content]="content">
</builder-component>
`
})
export class LandingPageComponent {
content: BuilderContent | null = null;
constructor(
private meta: Meta,
private title: Title
) {}
ngOnInit() {
if (this.content?.data) {
// Set basic metadata from Builder content
this.title.setTitle(this.content.data.title || 'Default Title');
this.meta.updateTag({
name: 'description',
content: this.content.data.description || 'Default Description'
});
}
}
}By creating custom fields , you can render these fields into your Pages as needed. Consider the examples below for implementations of a title field:
To reference dynamic values in your SEO tags, use custom fields on the content. As an example, consider dynamically referencing product prices.
By referencing your external product through custom fields instead of hardcoding product prices, price updates propagate throughout your Builder content without the need for manually bulk editing content.
For more information, see the Write API documentation and Intro to Plugins .
Framework
React Vue Qwik (Beta) REST API Angular
Meta-Framework
Next.js (Pages Router) Gatsby
Gen 1 (Recommended) export function getStaticProps() {
const builderContent = await builder.get("page" /*...*/).promise();
// assumes there is a custom field called "productReference" that lets the user pick a product ID
const productReference = await getProductInfo(builderContent.data.productReference);
return {
props: {
productReference,
builderContent,
},
};
}
export default function Mypage(props) {
return (
<>
<Head>
<title>
{props.builderContent.data.title} - ${props.productReference.price}
</title>
</Head>
<BuilderComponent /*...*/ />
</>
);
}// src/pages/product/[...slug].vue
<script setup>
import { Content, fetchOneEntry, isPreviewing } from '@builder.io/sdk-vue';
const BUILDER_PUBLIC_API_KEY = 'YOUR_API_KEY_HERE';
const BUILDER_MODEL = 'page';
// Get route and params
const route = useRoute();
const slug = Array.isArray(route.params.slug)
? route.params.slug.join('/')
: route.params.slug || '';
// Fetch both Builder content and product data
const { data: pageData } = await useAsyncData(
`product-${slug}`,
async () => {
// Fetch Builder content
const builderContent = await fetchOneEntry({
model: BUILDER_MODEL,
apiKey: BUILDER_PUBLIC_API_KEY,
userAttributes: {
urlPath: route.path,
},
});
// Fetch product data if there's a product reference
let productData = null;
if (builderContent?.data?.productReference) {
// Example product fetch - replace with your actual product fetching logic
const response = await fetch(`/api/products/${builderContent.data.productReference}`);
productData = await response.json();
}
return {
content: builderContent,
product: productData
};
}
);
// Determine if we should show Builder content
const shouldRenderBuilder = computed(() => {
return pageData.value?.content || isPreviewing();
});
// Set SEO metadata if content exists
useHead(() => {
if (!pageData.value?.content?.data) {
return {
title: 'Product Page',
meta: [
{ name: 'description', content: 'Product description' }
]
};
}
const { content, product } = pageData.value;
// Create meta tag array
const metaTags = [
{ name: 'description', content: content.data.description },
{ property: 'og:title', content: product
? `${content.data.title} - $${product.price}`
: content.data.title },
{ property: 'og:description', content: content.data.description }
];
// Add product-specific meta tags if available
if (product) {
metaTags.push(
{ property: 'og:image', content: product.imageUrl },
{ property: 'product:price:amount', content: product.price.toString() }
);
}
return {
// Set title with product price if available
title: product
? `${content.data.title} - $${product.price}`
: content.data.title,
meta: metaTags
};
});
</script>
<template>
<div>
<template v-if="shouldRenderBuilder">
<Content
:model="BUILDER_MODEL"
:content="pageData?.content"
:apiKey="BUILDER_PUBLIC_API_KEY"
/>
</template>
<template v-else>
<p>Product not found</p>
</template>
</div>
</template>// pages/product/[...slug].vue
<script setup>
import { Content, fetchOneEntry, isPreviewing } from '@builder.io/sdk-vue';
const BUILDER_PUBLIC_API_KEY = 'YOUR_API_KEY_HERE';
const BUILDER_MODEL = 'page';
const route = useRoute();
// Using Nuxt's data fetching for SSR
const { data: pageData } = await useAsyncData(
'builder-product-content',
async () => {
// Fetch Builder content
const builderContent = await fetchOneEntry({
model: BUILDER_MODEL,
apiKey: BUILDER_PUBLIC_API_KEY,
userAttributes: {
urlPath: route.path,
},
});
// Fetch product data if there's a product reference
let productData = null;
if (builderContent?.data?.productReference) {
// Example product fetch - replace with your actual product fetching logic
productData = await $fetch(`/api/products/${builderContent.data.productReference}`);
}
return {
content: builderContent,
product: productData
};
}
);
// Determine if we should show Builder content
const shouldRenderBuilder = computed(() => {
return pageData.value?.content || isPreviewing();
});
// Set SEO metadata
useHead(() => {
if (!pageData.value?.content?.data) {
return {
title: 'Product',
meta: [
{ name: 'description', content: 'Product description' }
]
};
}
const { content, product } = pageData.value;
// Set title with product price if available
const pageTitle = product
? `${content.data.title} - $${product.price}`
: content.data.title;
// Create meta tags
const metaTags = [
{ name: 'description', content: content.data.description },
{ property: 'og:title', content: pageTitle },
{ property: 'og:description', content: content.data.description }
];
// Add product-specific meta tags if available
if (product) {
metaTags.push(
{ property: 'og:image', content: product.imageUrl },
{ property: 'product:price:amount', content: product.price.toString() }
);
}
return {
title: pageTitle,
meta: metaTags
};
});
</script>
<template>
<div>
<template v-if="shouldRenderBuilder">
<Content
:model="BUILDER_MODEL"
:content="pageData?.content"
:apiKey="BUILDER_PUBLIC_API_KEY"
/>
</template>
<template v-else>
<p>Product not found</p>
</template>
</div>
</template>// src/routes/product/[...slug]/index.tsx
import { component$ } from '@builder.io/qwik';
import { routeLoader$ } from '@builder.io/qwik-city';
import { useDocumentHead } from '@builder.io/qwik-city';
import {
Content,
fetchOneEntry,
getBuilderSearchParams,
} from '@builder.io/sdk-qwik';
export const BUILDER_PUBLIC_API_KEY = 'YOUR_API_KEY';
export const BUILDER_MODEL = 'page';
// Loader for both Builder content and product data
export const useBuilderAndProductContent = routeLoader$(async ({ url, params }) => {
// Fetch Builder content
const builderContent = await fetchOneEntry({
model: BUILDER_MODEL,
apiKey: BUILDER_PUBLIC_API_KEY,
options: getBuilderSearchParams(url.searchParams),
userAttributes: {
urlPath: url.pathname,
},
});
// Fetch product data if there's a product reference
let productData = null;
if (builderContent?.data?.productReference) {
// Example product fetch - replace with your actual product fetching logic
productData = await fetch(`/api/products/${builderContent.data.productReference}`)
.then(res => res.json());
}
return {
content: builderContent,
product: productData
};
});
export default component$(() => {
const pageData = useBuilderAndProductContent();
const head = useDocumentHead();
// Set SEO metadata if content exists
if (pageData.value.content?.data) {
const { content, product } = pageData.value;
// Set title with product price if available
head.title = product
? `${content.data.title} - $${product.price}`
: content.data.title;
// Set description
head.meta.push({
name: 'description',
content: content.data.description
});
// Add Open Graph tags for social sharing
head.meta.push(
{
property: 'og:title',
content: head.title
},
{
property: 'og:description',
content: content.data.description
}
);
// Add product-specific meta tags if available
if (product) {
head.meta.push(
{
property: 'og:image',
content: product.imageUrl
},
{
property: 'product:price:amount',
content: product.price.toString()
}
);
}
}
return (
<Content
model={BUILDER_MODEL}
content={pageData.value.content}
apiKey={BUILDER_PUBLIC_API_KEY}
/>
);
});// product-page.component.ts
import { Component } from '@angular/core';
import { Content, BuilderContent, fetchOneEntry } from '@builder.io/sdk-angular';
import { Meta, Title } from '@angular/platform-browser';
import { environment } from '../environments/environment';
@Component({
selector: 'app-product-page',
standalone: true,
imports: [Content],
template: `
<builder-content
*ngIf="content"
[content]="content"
[customComponents]="customComponents"
/>
`
})
export class ProductPageComponent {
apiKey = environment.builderApiKey;
content: BuilderContent | null = null;
customComponents = []; // Your custom components
constructor(
private meta: Meta,
private title: Title
) {}
async ngOnInit() {
if (this.content?.data) {
// Fetch product data if it exists
if (this.content.data.productReference) {
const productData = await fetchOneEntry({
model: 'products',
apiKey: this.apiKey,
query: { id: this.content.data.productReference }
});
// Set dynamic title with product price
if (productData?.data) {
this.title.setTitle(
`${this.content.data.title} - $${productData.data.price}`
);
}
}
// Set basic meta tags
this.meta.updateTag({
name: 'description',
content: this.content.data.description
});
}
}
}// product.resolver.ts
import { ResolveFn } from '@angular/router';
import { BuilderContent } from '@builder.io/sdk-angular-v2';
import { inject } from '@angular/core';
import { BuilderService } from './builder.service';
import { ProductService } from './product.service';
import { forkJoin } from 'rxjs';
interface ProductPageData {
content: BuilderContent | null;
productData: any;
}
export const productResolver: ResolveFn<ProductPageData> = (route) => {
const builderService = inject(BuilderService);
const productService = inject(ProductService);
return forkJoin({
content: builderService.getContent('page'),
productData: productService.getProduct(route.params['id'])
});
};
// product-page.component.ts
import { Component, inject } from '@angular/core';
import { BuilderContent } from '@builder.io/sdk-angular-v2';
import { Meta, Title } from '@angular/platform-browser';
import { ActivatedRoute } from '@angular/router';
import { firstValueFrom } from 'rxjs';
@Component({
selector: 'app-product-page',
standalone: true,
template: `
<builder-component
*ngIf="content && productData"
[content]="content"
model="page">
</builder-component>
`
})
export class ProductPageComponent {
content?: BuilderContent;
productData?: any;
private route = inject(ActivatedRoute);
private meta = inject(Meta);
private title = inject(Title);
async ngOnInit() {
// Get both content and product data resolved during SSR
const data = await firstValueFrom(this.route.data);
this.content = data['pageData'].content;
this.productData = data['pageData'].productData;
if (this.content?.data && this.productData) {
// Set metadata during SSR
this.title.setTitle(
`${this.content.data.title} - $${this.productData.price}`
);
this.meta.updateTag({
name: 'description',
content: this.content.data.description
});
// Add Open Graph tags for social sharing
this.meta.updateTag({
property: 'og:title',
content: this.title.getTitle()
});
this.meta.updateTag({
property: 'og:description',
content: this.content.data.description
});
this.meta.updateTag({
property: 'og:image',
content: this.productData.imageUrl
});
}
}
}
// app.routes.ts
import { Routes } from '@angular/router';
import { pageResolver } from './page.resolver';
import { productResolver } from './product.resolver';
export const routes: Routes = [
{
path: '',
component: LandingPageComponent,
resolve: {
content: pageResolver
}
},
{
path: 'product/:id',
component: ProductPageComponent,
resolve: {
pageData: productResolver
}
}
];// Gatsby example with Dynamic SEO custom fields
import { BuilderComponent, builder } from '@builder.io/react';
import { Helmet } from 'react-helmet';
export default ({ data }) => {
const content = data.allBuilderModels.page[0].content;
// build a custom transformer that populate the content with product data from your external resources
const productPrice = data.allBuilderModels.page[0].myCustomProudctTransformer.price;
return (
<>
<Helmet>
<title>{content.data.title} - {productPrice} </title>
</Helmet>
<BuilderComponent content={content} />
</>
);
};
export const query = graphql`
query($path: String!) {
allBuilderModels {
page(target: { urlPath: $path }, limit: 1) {
content
myCustomProudctTransformer {
price
}
}
}
}
`;// Node.js example
// Run when your code doesn't match a page to check Builder for one
let page = await request(
'https://cdn.builder.io/api/v1/html/page?url=' + encodeURIComponent(request.url) + '&apiKey=' + apiKey
)
if (page) {
const productReference = await getProductInfo(page.data.productReference);
// Use any templating language, just put the html between your header and footer
res.send(`
<head>
<title>${page.data.title} - ${productReference.price}</title>
</head>
<body>
/* Your header */
${page.data.html}
/* Your footer */
</body>
`);
} else {
// Send 404 page
}// product-page.component.ts
import { Component } from '@angular/core';
import { builder, BuilderComponent, BuilderContent } from '@builder.io/angular';
import { Meta, Title } from '@angular/platform-browser';
@Component({
selector: 'app-product-page',
template: `
<builder-component
*ngIf="content"
[model]="'page'"
[content]="content">
</builder-component>
`
})
export class ProductPageComponent {
content: BuilderContent | null = null;
constructor(
private meta: Meta,
private title: Title
) {}
async ngOnInit() {
if (this.content?.data?.productReference) {
// Fetch product data using Builder's get helper
const productData = await builder
.get('products', {
query: {
id: this.content.data.productReference
}
})
.promise();
if (productData?.data) {
// Set dynamic title with product price
this.title.setTitle(
`${this.content.data.title} - $${productData.data.price}`
);
}
}
// Set basic meta tags
if (this.content?.data?.description) {
this.meta.updateTag({
name: 'description',
content: this.content.data.description
});
}
}
}Tip: Other possibilities for custom fields for SEO include making file fields for meta tags such as og:image or boolean fields for flagging which pages should be crawled or not crawled using the meta robots tag.
Sometimes you want to link to or maintain content that doesn't need to inform your online presence. With Builder custom fields , you can customize your Page model, so non-coding teammates can specify settings such as nofollow and noindex, which tells search engines not to convey reputation or index your page, respectively.
For credibility, link to reputable and relevant sources outside your website. When you link to another site, you share some of your site's reputation with it.
The nofollow attribute prevents reputation transfer, which is useful when linking to user-added links in comments or mentioning sites negatively. It's also handy for third-party widgets that might add unintended links to your site, ensuring control over your site's reputation.
In code, you can include the <meta name="robots" content="nofollow"> tag in the head section of a page to apply nofollow to all its links.
The noindex rule, implemented through either a meta tag or an HTTP response header, prevents search engines like Google from indexing specific content. When Googlebot encounters this rule, it excludes the page from Google Search results, even if other sites link to it. It's vital that the page remain unblocked by a robots.txt file and remain accessible to the crawler to ensure the effectiveness of the noindex rule.
For more detail, read Google's Block Search indexing with noindex and Using the robots meta tag .
In your Page model, you can add as many custom fields as you like. Custom fields could include any meta robots tags of your choice such as the noindex or nofollow.
With Builder's custom fields , you can select the input type so a noindex or nofollow setting can be a toggle. This way, non-dev teammates can quickly adjust this setting on their own. With almost 30 available custom input types, you can add as many inputs (such as for metadata or directives) to your Page model as needed, along with default values, requirements, localization as well as helper text so you users have context.
The image below shows a custom input for a noindex toggle that Builder uses in its documentation when, for example, there's a little-used page that is being phased out but the content is still occasionally needed.
The first image is of the custom field in the Page model:
This second image is how the noindex toggle appears in the Visual Editor.
Tip: The index and follow directives are search engine defaults, so you don't need to specify them . You'd only specify noindex and nofollow if you wanted to toggle these settings off.
To create a sitemap based on your content, you first need to obtain the relevant JSON data. Here's an example of how to retrieve the JSON data for a Space we use for documentation demos:
To create a sitemap based on your content, you first need to obtain the relevant JSON data. Here's an example of how to retrieve the JSON data for a Space we use for documentation demos:
https://cdn.builder.io/api/v2/content/page?apiKey=2b5ffc858d74425485135b88d2fc307a&fields=data.url&query.data.includeInSitemap.$ne=falseFor your own JSON, replace the Public API Key with your Public API Key . With your JSON, you can now create your sitemap.
Things to keep in mind:
If your sitemap is bigger than 50MB, create multiple sitemaps. To impact all files on your site, place the sitemap at the root. To focus on a particular area, place it in specific directories. Use complete URLs, not slugs. Use canonical URLs. Now that you have your JSON, which includes all the Page slugs, create a script to write out all the URLs:
const pageUrls = pages
.map((page) => page.data.url)This snippet:
takes an array of page objects extracts the URLs of those pages using the map() function creates a new array containing only the URLs The next part depends on your framework. You can create your sitemap manually or use a sitemap generator tool.
Here's an example of an XML sitemap based on the documentation demo query:
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<url>
<loc>https://www.example.com/</loc>
</url>
<url>
<loc>https://www.example.com/nextjs-test</loc>
</url>
<url>
<loc>https://www.example.com/remix-test</loc>
</url>
<url>
<loc>https://www.example.com/ai-demo</loc>
</url>
</urlset>Builder doesn't use iframes or canvases when rendering content with the Builder SDK on your site. In this way, what you create in Builder is as optimized as the code your developers write.
Was this article helpful?