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
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"
}
};
}
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"
}
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"
}
};
}
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}`
}
};
}
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
};
}
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 };
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>`;
});
};
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:
- Google’s Rich Results Test – paste the live URL and confirm each block is detected.
-
Schema.org validator – run
npm run lint:schemawhich pipes the generated JSON throughjsonlintand 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)