How to add structured data to a converted Framer site

Add JSON-LD structured data to a Framer site converted to Next.js or Astro, generated from your CMS JSON so every article, product and FAQ stays in sync.

Published
Reading time
7 min
Deframer cover: “Structured data, from your CMS.” Beside it, a list of CMS JSON, JSON-LD and Search engines with JSON-LD selected
On this page12 sections

To add structured data to a converted Framer site, render a <script type="application/ld+json"> element from your page or route component, with the JSON built from data the project already has: the page's metadata and the records in src/cms/<collection>.json. In a Next.js project that is a few lines in the route's page.tsx; in Astro it goes in the page's index.astro. Because the data comes from the same records that render the page, the markup can't drift from what visitors see.

Framer lets you paste JSON-LD into site or page custom code, but it can't build it from CMS fields per item. Once the site is code, it can. This guide shows where each kind of markup belongs, a reusable helper, examples for the common types, and how to check the result. Deframer is an independent tool and is not affiliated with, endorsed or sponsored by Framer.

What structured data does and doesn't do

Structured data is a machine-readable description of a page: this is an article, written by this person, published on this date; this is a product with this price; this is a list of questions and answers. Search engines read it to understand the page and, for some types, to show rich results.

Two honest caveats before you start:

  • It is not a ranking switch. Google describes structured data as a way to help it understand pages and to make them eligible for rich results, not as a guarantee of either.
  • Eligibility changes. Google has narrowed which sites get some rich results over the years (FAQ rich results, for example, are now shown for only a small set of authoritative sites). Markup that is accurate still helps machines read the page, so it is worth having, but check Google's current documentation for each type before promising anyone a visual result.

What carries over from Framer

If you added JSON-LD through Framer's site or page custom code, the conversion carries it over with that code: site-level custom code lands in the root layout, and page custom code goes with its route. Nothing is lost, but it stays static: the same block on every page that uses it.

Framer doesn't add structured data of its own, so a site with no custom JSON-LD has none after converting either. That is the starting point this guide assumes. Check what you have with:

sh
curl -s https://www.example.com/ | grep -c 'application/ld+json'

Where should each type of markup go?

Type

Where it goes

Built from

Organization or Person

Root layout, once per site

Constants you write once

WebSite

Root layout or home page

Site name and URL

Article / BlogPosting

CMS detail route for posts

The post's record in src/cms/

Product

CMS detail route for products

The product's record

BreadcrumbList

Detail routes

The route path and record title

FAQPage

The page with the questions

The questions on that page

Keep a rule in mind: markup describes what the page shows. Don't mark up reviews, prices or questions that aren't visible on the page.

A small helper for JSON-LD

Write one component and reuse it everywhere. In a Next.js project, add src/components/json-ld.tsx:

tsx
type JsonLdProps = { data: Record<string, unknown> | Record<string, unknown>[] };

export function JsonLd({ data }: JsonLdProps) {
  // Escape "<" so a value can never close the script tag.
  const json = JSON.stringify(data).replace(/</g, "\\u003c");
  return <script type="application/ld+json" dangerouslySetInnerHTML={{ __html: json }} />;
}

The escape matters: CMS text is content written by people, and a stray </script> in a field would otherwise break the page. The same component works in TanStack Start, React Router and Vite projects, since their pages are React too.

Step 1: site-wide Organization markup

Open src/app/layout.tsx and render the helper inside the body:

tsx
import { JsonLd } from "@/components/json-ld";

const organization = {
  "@context": "https://schema.org",
  "@type": "Organization",
  name: "Example Studio",
  url: "https://www.example.com",
  logo: "https://www.example.com/logo.png",
  sameAs: ["https://www.linkedin.com/company/example"],
};

// inside the returned <body>:
<JsonLd data={organization} />

Use your production domain, not the *.framer.website address. The root layout's metadataBase is set to the site's published URL when you convert; if you're moving to a new domain, change both at the same time. The SEO migration guide covers metadataBase in detail.

Step 2: Article markup from the CMS

CMS detail pages are where code pays off. Each collection is a typed schema in src/cms/<collection>.ts and its records in src/cms/<collection>.json, and the detail route (for example src/app/blog/[slug]/page.tsx) is prerendered for every item. The CMS to JSON guide walks through those files.

Field names follow your Framer CMS, so yours will differ. With a blog collection whose records have title, slug, publishedDate and thumbnail, add the markup in the route:

tsx
import posts from "@/cms/blog.json";
import { JsonLd } from "@/components/json-ld";

function articleData(slug: string) {
  const post = posts.find((item) => item.slug === slug);
  if (!post) return null;
  return {
    "@context": "https://schema.org",
    "@type": "BlogPosting",
    headline: post.title,
    datePublished: post.publishedDate,
    image: post.thumbnail?.src,
    url: `https://www.example.com/blog/${post.slug}`,
    author: { "@type": "Organization", name: "Example Studio" },
  };
}

Then render <JsonLd data={...} /> next to the page component the route already returns, using the slug from the route params. Keep the generated view (in src/views) as it is; adding markup in the route file means a future reconversion only touches files you haven't edited.

Three details worth getting right:

  1. Dates. Framer's date fields are ISO strings, which is what schema.org expects. If you add a dateModified, keep it honest.
  2. Images. By default image URLs point at Framer's CDN; with "Assets in the project" they are paths such as /images/…. Make relative paths absolute with your domain.
  3. Authors. If the collection has an author reference, use a Person with the author's name rather than the organization.

Step 3: breadcrumbs

A BreadcrumbList mirrors the page's place in the site. For a post:

ts
const breadcrumbs = {
  "@context": "https://schema.org",
  "@type": "BreadcrumbList",
  itemListElement: [
    { "@type": "ListItem", position: 1, name: "Blog", item: "https://www.example.com/blog" },
    { "@type": "ListItem", position: 2, name: post.title, item: `https://www.example.com/blog/${post.slug}` },
  ],
};

JsonLd takes an array, so you can pass the article and breadcrumbs together.

Step 4: FAQ markup that matches the page

If a page shows questions and answers, and they come from a CMS collection, build the FAQPage from the same records. If they are written into the page's design, copy them into a small array beside the route and keep the two in step. Mark up only the questions visitors can read on that page.

How does this work in Astro?

In an Astro project, each URL is a src/pages/**/index.astro file with its metadata in its head, beside a _route.tsx island. Put the script in the .astro file so it's part of the prerendered HTML:

Code
---
import posts from "../../../cms/blog.json";
const post = posts.find((item) => item.slug === Astro.params.slug);
const data = { "@context": "https://schema.org", "@type": "BlogPosting", headline: post?.title };
---
<script type="application/ld+json" set:html={JSON.stringify(data).replace(/</g, "\\u003c")} />

Paths depend on your project's folder depth, so adjust the import. The site's own head tags live in src/site/head.ts if you'd rather add a site-wide block there. For other frameworks, the per-page head lives in head() (TanStack Start), meta (React Router) or <PageHead> (Vite); a JSON-LD script can also be rendered in the page body in all of them, which Google reads just as well.

How to check your structured data

  1. Build and view source. Run npm run build && npm start, open a page and search the HTML for application/ld+json. It must be in the server-rendered HTML, not only after JavaScript runs.
  2. Validate the syntax. Paste the URL or the HTML into the Schema Markup Validator (validator.schema.org).
  3. Check rich result eligibility. Use Google's Rich Results Test for the types Google supports.
  4. Watch Search Console. After launch, the enhancement reports show detected items and errors. They take time to update.

A quick spot check across all posts:

sh
for slug in $(node -e 'console.log(require("./src/cms/blog.json").map(p => p.slug).join(" "))'); do
  curl -s "http://localhost:3000/blog/$slug" | grep -q 'ld+json' || echo "missing: $slug"
done

Common mistakes

  • Marking up the wrong domain. Using the framer.website address in url fields after moving.
  • Invisible content. Questions, ratings or prices in the markup that the page doesn't show.
  • Duplicates. Leaving an old static block from Framer custom code next to the new generated one. Search the layout and route files for ld+json and remove the old block once the new one works.
  • Unescaped text. Interpolating CMS text into a template string instead of JSON.stringify.

For more on what moving to code changes for search, read is Framer good for SEO, and for answer engines specifically, making a Framer site ready for AI search.

FAQ

Can I add structured data to a Framer site without converting it?

Yes. You can paste a JSON-LD block into Framer's site or page custom code. What you can't easily do in Framer is generate a different block for each CMS item from its fields; that is the part code makes simple.

Does the converted project include structured data automatically?

It includes whatever JSON-LD your site already published through custom code. The converter doesn't invent markup of its own, because it can't know what your pages should claim.

Should JSON-LD go in the head or the body?

Either works for Google, which reads JSON-LD anywhere in the HTML. What matters is that it is in the server-rendered HTML, so a crawler sees it without running JavaScript.

Which schema types matter most for a marketing site?

Usually Organization (or Person for a portfolio), WebSite, BlogPosting for articles and BreadcrumbList. Add Product, Event or others only when the page really shows that thing.

Will structured data get me rich results?

Not necessarily. Accurate markup makes a page eligible for the rich results Google supports for that type, and Google decides when to show them. Treat it as a way to describe your pages clearly, not as a guaranteed visual change.