DEV Community

the bomber
the bomber

Posted on

Adding JSON‑LD Structured Data to My Small‑Business Site: A Hands‑On Walkthrough

Why JSON‑LD matters for local SEO

When I built my own site for Dream Futures Tech, the first thing I wanted was to make sure Google could see exactly who I am, what I do, and where I’m located. JSON‑LD is the format Google recommends for structured data because it keeps the markup separate from the HTML, is easy to generate programmatically, and plays nicely with modern build pipelines.

Setting up the project

I keep my site in a simple static‑site generator (Eleventy) and run a Node.js script on my home automation server to inject the JSON‑LD at build time. The folder layout looks like this:

my-site/
├─ src/
│  ├─ pages/
│  └─ layouts/
├─ data/
│  └─ schema.js   ← our generator
└─ .eleventy.js
Enter fullscreen mode Exit fullscreen mode

The schema.js module exports a function that returns an object with the four schemas I need: Organization, WebSite, BlogPosting, and BreadcrumbList.

Defining the Organization schema

The Organization block is the cornerstone for any local business. I pull the address and contact info from a single JSON file so I only have to update it once.

// data/schema.js
const orgInfo = require('./orgInfo.json');

function organization() {
  return {
    "@context": "https://schema.org",
    "@type": "Organization",
    "name": orgInfo.name,
    "url": orgInfo.url,
    "logo": orgInfo.logo,
    "contactPoint": [{
      "@type": "ContactPoint",
      "telephone": orgInfo.phone,
      "contactType": "Customer Service",
      "areaServed": "US",
      "availableLanguage": ["English"]
    }],
    "address": {
      "@type": "PostalAddress",
      "streetAddress": orgInfo.street,
      "addressLocality": orgInfo.city,
      "addressRegion": orgInfo.state,
      "postalCode": orgInfo.zip,
      "addressCountry": "US"
    }
  };
}
Enter fullscreen mode Exit fullscreen mode

The JSON file (orgInfo.json) lives in the repo and looks like this:

{
  "name": "Dream Futures Tech",
  "url": "https://dreamfuturestech.com",
  "logo": "https://dreamfuturestech.com/assets/logo.png",
  "phone": "+1-555-123-4567",
  "street": "123 Ocean Blvd",
  "city": "Pompano Beach",
  "state": "FL",
  "zip": "33060"
}
Enter fullscreen mode Exit fullscreen mode

Whenever I add a new location or change a phone number, I edit this file and the schema updates automatically.

Adding WebSite and SearchAction

Google can surface a site‑wide search box in the results if you provide a WebSite schema with a potentialAction of type SearchAction.

function website() {
  return {
    "@context": "https://schema.org",
    "@type": "WebSite",
    "url": orgInfo.url,
    "name": orgInfo.name,
    "potentialAction": {
      "@type": "SearchAction",
      "target": `${orgInfo.url}/search?q={search_term_string}`,
      "query-input": "required name=search_term_string"
    }
  };
}
Enter fullscreen mode Exit fullscreen mode

I expose a very lightweight /search endpoint that returns results from my ElasticPress index. The schema tells Google the exact URL pattern to use.

BlogPosting for articles

My site includes a blog where I write about automation scripts, WordPress tips, and local SEO tricks. Each post gets its own BlogPosting block so Google can display rich results like article snippets.

function blogPosting(page) {
  return {
    "@context": "https://schema.org",
    "@type": "BlogPosting",
    "headline": page.title,
    "image": page.image || orgInfo.logo,
    "author": {
      "@type": "Person",
      "name": "Kaleb"
    },
    "publisher": {
      "@type": "Organization",
      "name": orgInfo.name,
      "logo": {
        "@type": "ImageObject",
        "url": orgInfo.logo
      }
    },
    "datePublished": page.date,
    "dateModified": page.modified || page.date,
    "mainEntityOfPage": {
      "@type": "WebPage",
      "@id": `${orgInfo.url}${page.url}`
    }
  };
}
Enter fullscreen mode Exit fullscreen mode

The Eleventy template calls blogPosting(page) for each markdown file in the posts/ folder, then injects the JSON‑LD into a <script type="application/ld+json"> tag.

BreadcrumbList for navigation

Breadcrumbs help Google understand the hierarchy of a page. I generate a BreadcrumbList based on the URL path segments.

function breadcrumbList(page) {
  const parts = page.url.split('/').filter(Boolean);
  const items = parts.map((part, i) => ({
    "@type": "ListItem",
    "position": i + 1,
    "name": part.charAt(0).toUpperCase() + part.slice(1),
    "item": `${orgInfo.url}/${parts.slice(0, i + 1).join('/')}/`
  }));

  // Add the homepage as the first item
  items.unshift({
    "@type": "ListItem",
    "position": 1,
    "name": "Home",
    "item": orgInfo.url
  });

  // Increment positions after unshift
  items.forEach((it, idx) => it.position = idx + 1);

  return {
    "@context": "https://schema.org",
    "@type": "BreadcrumbList",
    "itemListElement": items
  };
}
Enter fullscreen mode Exit fullscreen mode

Because the function works purely on the URL string, I can reuse it for any page without hard‑coding the hierarchy.

Automating schema generation with Node

All four functions live in schema.js. I expose a single helper that merges the needed blocks for a given page type.

function generateSchema(page) {
  const blocks = [organization(), website()];
  if (page.type === 'post') blocks.push(blogPosting(page));
  blocks.push(breadcrumbList(page));
  return JSON.stringify(blocks.length === 1 ? blocks[0] : blocks, null, 2);
}

module.exports = { generateSchema };
Enter fullscreen mode Exit fullscreen mode

In my Eleventy config I register a short‑code:

// .eleventy.js
const { generateSchema } = require('./data/schema');

module.exports = function(eleventyConfig) {
  eleventyConfig.addNunjucksShortcode('jsonld', page => {
    return `<script type="application/ld+json">${generateSchema(page)}</script>`;
  });
};
Enter fullscreen mode Exit fullscreen mode

Now any template can simply write {% jsonld page %} and the correct JSON‑LD appears.

Deploying and testing

I push the static site to Netlify from a GitHub repo. Netlify runs the Eleventy build, which calls the schema generator, and the final HTML contains the structured data.

To verify the output I use two tools:

  1. Google’s Rich Results Test – paste the live URL and confirm each block is detected.
  2. Schema.org validator – run npm run lint:schema which pipes the generated JSON through jsonlint and a custom rule set.

If a field is missing (for example, the logo URL), the validator throws a clear error, and I fix it before the next deploy.

Wrap‑up

Adding JSON‑LD to a small‑business site doesn’t have to be a one‑off copy‑paste job. By centralising the data in a JSON file and generating the markup with a few Node functions, I keep the schema accurate, version‑controlled, and easy to extend. The same approach works for any static or JAMstack site, and the performance impact is negligible because the script tag is just a few kilobytes of static JSON.

I build sites and automations for small businesses at dreamfuturestech.com.

Top comments (0)