Skip to content
← All articles

Structured data, SEO, and the sitemap that builds itself

JSON-LD for Organisation and LocalBusiness. Article schema on every post. FAQPage from CMS content. A sitemap that queries Sanity at build time.

Structured data, SEO, and the sitemap that builds itself

Structured data is one of those things that takes an afternoon to implement and pays off for years. Search engines understand your content better. Rich results appear. Your site looks more authoritative in SERPs.

Here is everything we added to innatus.digital.

Organisation and ProfessionalService

Two schemas live in the site layout. They apply to every page.

const organizationSchema = {
  '@context': 'https://schema.org',
  '@type': 'Organization',
  name: 'Innatus Digital',
  legalName: 'Innatus Digital Ltd',
  url: siteUrl,
  logo: `${siteUrl}/images/logo.png`,
  email: '[email protected]',
  foundingDate: '2020-07',
  founder: {
    '@type': 'Person',
    name: 'Chris Ryan',
    jobTitle: 'Managing Director',
  },
  numberOfEmployees: {
    '@type': 'QuantitativeValue',
    value: 6,
  },
}

The `ProfessionalService` schema adds location, service area, and expertise signals:

const localBusinessSchema = {
  '@context': 'https://schema.org',
  '@type': 'ProfessionalService',
  name: 'Innatus Digital',
  url: siteUrl,
  description: 'Web development studio specialising in Next.js, Sanity, Laravel, and BigCommerce.',
  address: {
    '@type': 'PostalAddress',
    addressRegion: 'Somerset',
    addressCountry: 'GB',
  },
  areaServed: [
    { '@type': 'Country', name: 'United Kingdom' },
    { '@type': 'Continent', name: 'Europe' },
  ],
  knowsAbout: [
    'Web Development', 'Next.js', 'React', 'Sanity CMS',
    'Laravel', 'BigCommerce', 'WordPress', 'AWS',
    'Technical Consultancy', 'E-commerce Development', 'Headless CMS',
  ],
}

`knowsAbout` is underused. It tells Google exactly what topics your business is authoritative on. For a service business, this is free relevance signalling.

Article schema on every post

Each blog post gets Article structured data with author, publisher, and publication date:

const articleSchema = {
  '@context': 'https://schema.org',
  '@type': 'Article',
  headline: post.title,
  description: post.excerpt,
  datePublished: post.publishedAt,
  author: {
    '@type': 'Person',
    name: post.author?.name || 'Chris Ryan',
    url: `${siteUrl}/who-are-we`,
  },
  publisher: {
    '@type': 'Organization',
    name: 'Innatus Digital',
    url: siteUrl,
  },
  url: `${siteUrl}/digital-insights/${slug}`,
  ...(post.coverImage ? { image: post.coverImage } : {}),
}

This is the baseline for appearing in Google's article carousels and getting rich snippets with author information.

FAQPage from CMS content

Remember the FAQs array on the service schema? It generates FAQPage structured data automatically:

const faqSchema = service.faqs && service.faqs.length > 0 ? {
  '@context': 'https://schema.org',
  '@type': 'FAQPage',
  mainEntity: service.faqs.map((faq) => ({
    '@type': 'Question',
    name: faq.question,
    acceptedAnswer: {
      '@type': 'Answer',
      text: faq.answer,
    },
  })),
} : null

Editors add FAQs in Sanity. The frontend renders them as visible content and as structured data. Google can display them as rich results. No separate maintenance, no synchronisation problems.

BreadcrumbList

Both service pages and blog posts include breadcrumb schema:

const breadcrumbSchema = {
  '@context': 'https://schema.org',
  '@type': 'BreadcrumbList',
  itemListElement: [
    { '@type': 'ListItem', position: 1, name: 'Home', item: siteUrl },
    { '@type': 'ListItem', position: 2, name: 'Services', item: `${siteUrl}/#services` },
    { '@type': 'ListItem', position: 3, name: service.title, item: `${siteUrl}/services/${slug}` },
  ],
}

Breadcrumbs in SERPs make your listing wider and more clickable. Trivial to add, meaningful improvement.

The self-building sitemap

Next.js supports dynamic sitemaps via a `sitemap.ts` file. Ours queries Sanity for all published posts and services:

export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
  const staticPages: MetadataRoute.Sitemap = [
    { url: baseUrl, lastModified: new Date(), changeFrequency: 'weekly', priority: 1 },
    { url: `${baseUrl}/who-are-we`, lastModified: new Date(), changeFrequency: 'monthly', priority: 0.8 },
    { url: `${baseUrl}/digital-insights`, lastModified: new Date(), changeFrequency: 'weekly', priority: 0.8 },
  ]

const posts = await client.fetch(
    `*[_type == "post" && defined(slug.current)] | order(publishedAt desc) { "slug": slug.current, "date": publishedAt }`
  )

const postPages = posts.map((post) => ({
    url: `${baseUrl}/digital-insights/${post.slug}`,
    lastModified: post.date ? new Date(post.date) : new Date(),
    changeFrequency: 'monthly' as const,
    priority: 0.6,
  }))

const services = await client.fetch(
    `*[_type == "service" && defined(slug.current)] { "slug": slug.current }`
  )

const servicePages = services.map((service) => ({
    url: `${baseUrl}/services/${service.slug}`,
    changeFrequency: 'monthly' as const,
    priority: 0.7,
  }))

return [...staticPages, ...servicePages, ...postPages]
}

Publish a new post in Sanity, the sitemap includes it automatically. No manual XML editing. No forgotten pages.

All of this, structured data and sitemap combined, took about half a day. The SEO benefits compound over time.

Next in the series: [Visual editing and the Sanity presentation tool in Next.js 16](/digital-insights/visual-editing-and-the-sanity-presentation-tool-in-nextjs-16)

Chris Ryan

Chris Ryan

Managing Director

17+ years in full-stack web development, most of it leading teams agency-side across e-commerce, CMS platforms, and bespoke applications. Specialises in infrastructure, system integration, and data privacy, with hands-on experience as a Data Protection Officer. Founded Innatus Digital in 2020 to offer the kind of honest, technically-led partnership that he felt was missing from the agency world.