Modular Schema.org Management in HubSpot CMS

Managing Schema.org markup page by page is one of the fastest ways to turn a good SEO idea into a maintenance nightmare.

I’ve written before about why structured data should be generated from templates rather than pasted into pages or bolted on with a plugin, and that principle applies to every CMS. HubSpot is worth its own walkthrough because it gives you genuinely good template-level tools for this, and because HubSpot’s own developer documentation endorses the modular approach. Most HubSpot sites I audit still do it the painful way.

Why Page-by-Page Schema Is a Terrible Approach

The manual pattern looks harmless at first: generate a JSON-LD block for a page, paste it into the page’s head HTML field, publish, repeat. Six months later you have a site where every page carries its own hand-maintained copy of markup, and three problems are now permanent features of your life.

Every change is a full-site project. Rebrand, move offices, change your logo file, update a phone number, and every page that embeds that property has to be found and edited individually. Nobody budgets time for that, so the markup drifts out of date and stays that way.

Search Console errors multiply by page count. This is the one that really hurts. When Search Console reports a structured data error, a manual setup means the same mistake was pasted into 40, 100, or 400 pages, and every single one has to be opened and fixed by hand. An error in a template is fixed once. An error in pasted blocks is fixed hundreds of times, usually across several tedious sessions, while the report keeps flagging stragglers you missed.

The markup lies. Pasted schema is a snapshot of the page at the moment someone generated it. The page keeps evolving, the JSON-LD doesn’t, and eventually your markup describes content that no longer exists. Inaccurate structured data is worse than none, because it teaches Google and AI systems that your markup can’t be trusted.

The fix is the same one I argue for everywhere: schema should be generated from the same data that renders the page, so it can never drift. HubSpot CMS gives you two solid ways to do that.

Method 1: Template-Level Schema with HubL

For markup that applies consistently across a content type, the right home is the template itself. HubSpot’s HubL templating language provides a require_head tag that injects code from anywhere in a template into the page’s <head>, which is exactly where JSON-LD belongs.

A blog post template can build complete Article markup from the same variables that render the visible page:

{% require_head %}
<script type="application/ld+json">
{
  "@context": "https://schema.org",
  "@type": "Article",
  "headline": "{{ content.title }}",
  "datePublished": "{{ content.publish_date|datetimeformat('%Y-%m-%dT%H:%M:%S') }}",
  "dateModified": "{{ content.updated|datetimeformat('%Y-%m-%dT%H:%M:%S') }}",
  {% if content.featured_image %}
  "image": "{{ content.featured_image }}",
  {% endif %}
  "author": {
    "@type": "Person",
    "name": "{{ content.blog_post_author.display_name }}"
  }
}
</script>
{% end_require_head %}

Every property comes from CMS data: content.title, content.publish_date, content.featured_image, the author object. Publish a post and the markup exists. Edit the title and the markup updates. Conditional HubL handles optional fields, like skipping the image property when no featured image is set, so you never emit empty or invalid values.

Write it once in the blog template and every post, past and future, carries correct markup. When Search Console flags a problem, you fix one file and the entire blog is corrected on the next render.

Method 2: Custom Modules for Editor-Controlled Schema

Template-level markup covers the consistent cases. The second method covers pages that need different schema types on the same template: a resources section where some posts are how-to guides, some announce events, and some are job postings.

HubSpot’s answer is a custom module with fields. Build a module, local or global, that exposes a schema type selector and the fields each type needs: event dates and venue for Event, salary range and location for JobPosting, steps for HowTo. The module’s HubL template renders the correct JSON-LD from whichever fields the editor filled in, again wrapped in require_head so it lands in the document head.

This is the part I want to emphasize, because it’s the difference between modular schema and merely relocated schema: the editor fills in structured fields, and the template owns the markup. Nobody pastes JSON. Nobody can introduce a syntax error. Marketing gets self-service schema on any page they build, and the markup structure stays centralized in one module you control. When the vocabulary changes or an error surfaces, you edit the module once and every page using it inherits the fix.

Global modules extend the same idea to site-wide markup: an Organization block with your logo, address and social profiles, maintained in exactly one place and emitted on every page.

The Same Rules Apply No Matter the Vocabulary

Modular delivery solves maintenance, but it doesn’t excuse you from knowing the spec. The markup is only worth shipping if it’s precise and truthful: real Organization, Article, Service and FAQPage types with verifiable properties, and none of the AggregateRating-without-reviews keyword stuffing that gets sites flagged. I went deeper on that standard in AI optimization that works, and it matters more every quarter as AI assistants lean on structured data to decide which brands they can state facts about.

The test for any CMS setup is the same question: if a property changed tomorrow, how many edits would it take to make every page truthful again? On a well-built HubSpot site the answer is one. If your answer is “one per page,” that’s the project to fix first, and it’s the kind of template architecture work I handle across HubSpot, WordPress and headless platforms as a CMS developer.