| name | google-list-pro |
| description | Inject schema.org LocalBusiness JSON-LD structured data into an Astro + TypeScript project
to improve Google Business Profile ranking via Semantic SEO. Astro-native equivalent of the
Google List Pro WordPress plugin. Use when: adding a local business to an Astro site, improving
GBP listing rank for a client, or onboarding a new Astro project for a local business.
|
| trigger | User asks to "add schema markup", "improve local SEO", "inject structured data", or "set up
Google Business SEO" on an Astro project. Also fires when the google-list-money-pro knowledge
pack is cited and the target stack is Astro.
|
| source | knowledge/marketing/courses/google-list-money-pro.md |
Google List Pro — Astro Implementation
Semantic SEO for local businesses on Astro. Config-driven, type-safe, zero runtime JS. Maps 1:1 to
the WordPress plugin's field set and schema output.
When to fire
- An Astro project represents a local business with a GBP listing.
- User wants to rank higher on Google Maps / GBP without content-heavy SEO.
- Adding a new client site where
@type is one of the supported LocalBusiness subtypes.
Procedure
Step 1 — Audit the project layout
Before creating files, verify the target project structure:
ls src/config src/lib src/components src/layouts 2>/dev/null
Adapt output paths to match whatever layout the project already uses (some Astro projects use
src/data/ instead of src/config/, etc.). The file names below are canonical; the folders
are not.
Step 2 — Create the business config
Create src/config/schema.ts. This is the single source of truth — the "settings form"
equivalent in the WP plugin. Fill it out based on the client's information.
export type LocalBusinessType =
| 'LocalBusiness'
| 'Attorney' | 'LegalService' | 'Notary'
| 'AccountingService' | 'FinancialService' | 'InsuranceAgency' | 'BankOrCreditUnion'
| 'MedicalBusiness' | 'Dentist' | 'Hospital'
| 'Restaurant' | 'Bakery' | 'BarOrPub' | 'CafeOrCoffeeShop' | 'FastFoodRestaurant'
| 'BeautySalon' | 'HairSalon' | 'DaySpa' | 'HealthClub' | 'NailSalon'
| 'Electrician' | 'Plumber' | 'GeneralContractor' | 'HVACBusiness' | 'RoofingContractor'
| 'AutoRepair' | 'AutoDealer' | 'AutoBodyShop' | 'GasStation'
| 'RealEstateAgent' | 'TravelAgency' | 'Hotel' | 'Motel' | 'Gym'
| 'Store' | 'ClothingStore' | 'FurnitureStore' | 'ElectronicsStore' | 'Florist'
| 'ChildCare' | 'EmploymentAgency' | 'Locksmith' | 'MovingCompany'
export type DayOfWeek =
| 'Monday' | 'Tuesday' | 'Wednesday' | 'Thursday'
| 'Friday' | 'Saturday' | 'Sunday'
export interface OpeningHours {
days: DayOfWeek[]
opens: string
closes: string
}
export interface LocalBusinessConfig {
type: LocalBusinessType
name: string
url: string
description?: string
logo?: string
image?: string
telephone?: string
fax?: string
email?: string
priceRange?: '$' | '$$' | '$$$' | '$$$$' | '$$$$$'
address: {
street: string
city: string
state: string
zip: string
country?: string
}
geo?: {
latitude: number
longitude: number
}
openingHours?: OpeningHours[]
contactPoint?: {
telephone: string
contactType:
| 'customer service' | 'technical support' | 'billing support'
| 'sales' | 'reservations' | 'emergency'
}
social?: {
facebook?: string
instagram?: string
twitter?: string
youtube?: string
linkedin?: string
pinterest?: string
whatsapp?: string
}
}
export const businessConfig: LocalBusinessConfig = {
type: 'LocalBusiness',
name: '',
url: '',
description: '',
logo: '',
image: '',
telephone: '',
email: '',
priceRange: '$$',
address: {
street: '',
city: '',
state: '',
zip: '',
country: 'BR',
},
geo: {
latitude: 0,
longitude: 0,
},
openingHours: [
{ days: ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday'], opens: '09:00', closes: '18:00' },
],
social: {
instagram: '',
facebook: '',
},
}
Geo tip: Get lat/long from latlong.net — paste the address, copy the values.
Step 3 — Create the schema builder
Create src/lib/buildLocalBusinessSchema.ts. This is pure TypeScript — no Astro-specific APIs.
import type { LocalBusinessConfig } from '../config/schema'
export function buildLocalBusinessSchema(config: LocalBusinessConfig): Record<string, unknown> {
const schema: Record<string, unknown> = {
'@context': 'https://schema.org',
'@type': config.type,
name: config.name,
url: config.url,
address: {
'@type': 'PostalAddress',
streetAddress: config.address.street,
addressLocality: config.address.city,
addressRegion: config.address.state,
postalCode: config.address.zip,
addressCountry: config.address.country ?? 'BR',
},
}
if (config.description) schema.description = config.description
if (config.logo) schema.logo = config.logo
if (config.image) schema.image = config.image
if (config.telephone) schema.telephone = config.telephone
if (config.fax) schema.faxNumber = config.fax
if (config.email) schema.email = config.email
if (config.priceRange) schema.priceRange = config.priceRange
if (config.geo?.latitude && config.geo?.longitude) {
schema.geo = {
'@type': 'GeoCoordinates',
latitude: config.geo.latitude,
longitude: config.geo.longitude,
}
}
if (config.openingHours?.length) {
schema.openingHoursSpecification = config.openingHours.flatMap(({ days, opens, closes }) =>
days.map(day => ({
'@type': 'OpeningHoursSpecification',
dayOfWeek: `https://schema.org/${day}`,
opens,
closes,
}))
)
}
const socialUrls = Object.values(config.social ?? {}).filter(Boolean) as string[]
if (socialUrls.length) schema.sameAs = socialUrls
if (config.contactPoint) {
schema.contactPoint = {
'@type': 'ContactPoint',
telephone: config.contactPoint.telephone,
contactType: config.contactPoint.contactType,
}
}
return schema
}
Step 4 — Create the Astro component
Create src/components/SchemaOrg.astro. Renders a single <script type="application/ld+json">
tag — no client-side JS, no runtime cost.
---
// src/components/SchemaOrg.astro
import { buildLocalBusinessSchema } from '../lib/buildLocalBusinessSchema'
import type { LocalBusinessConfig } from '../config/schema'
interface Props {
config: LocalBusinessConfig
}
const schema = buildLocalBusinessSchema(Astro.props.config)
---
<script type="application/ld+json" set:html={JSON.stringify(schema, null, 0)} />
Step 5 — Integrate into the base layout
In the project's base layout (usually src/layouts/BaseLayout.astro or Layout.astro), import
SchemaOrg and place it inside <head>. Import businessConfig from the config file.
---
import SchemaOrg from '../components/SchemaOrg.astro'
import { businessConfig } from '../config/schema'
---
<html lang="pt-BR">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width" />
<!-- ... other head tags ... -->
<SchemaOrg config={businessConfig} />
</head>
<body>
<slot />
</body>
</html>
Step 6 — Verify the output
After astro dev or astro build, inspect the page source and confirm:
grep -r 'application/ld+json' dist/index.html
Paste the URL (or the raw JSON) into Google's Rich Results Test to validate the schema
before submitting to Search Console.
Step 7 — Submit to Google Search Console
After deploy, trigger re-indexing:
- Open Google Search Console for the domain.
- Paste the homepage URL into the top search bar → Request Indexing.
- Repeat for any inner pages that carry the same schema.
- Wait 1–4 weeks, then compare GBP listing rank before and after.
If the client doesn't have Search Console set up, add it as a property using their domain
and verify via DNS TXT record or HTML meta tag.
Output shape
Four files, all created or modified by this skill:
| File | Role |
|---|
src/config/schema.ts | Editable config — one object per client site |
src/lib/buildLocalBusinessSchema.ts | Pure serializer — never needs to change |
src/components/SchemaOrg.astro | Astro component — renders the JSON-LD tag |
src/layouts/BaseLayout.astro | Existing file — add two lines to <head> |
Hard rules
- One
SchemaOrg per <head>. Duplicate JSON-LD tags for the same @type confuse crawlers.
set:html, not {JSON.stringify(...)} — Astro escapes dynamic content inside <script>; set:html bypasses that for JSON-LD.
- No
<script> in <body>. The tag must live in <head> to be processed by crawlers.
- Fill geo coordinates. The original plugin required lat/long; Google uses it to confirm location match with the GBP listing. Never leave both at
0.
sameAs must be profile URLs, not post or feed URLs (e.g., https://instagram.com/handle, not https://instagram.com/p/abc).
opens/closes in 24h HH:MM format. Schema.org rejects AM/PM strings.
addressCountry as ISO 3166-1 alpha-2 (e.g., 'BR', 'US', 'PT'), not full country names.
Variations
Multiple locations: Wrap businessConfig in an array, pass each to a separate <SchemaOrg> in the page that corresponds to that location. Do not merge multiple locations into one schema object.
Per-page schema override: Pass a different config object as a prop from the page's frontmatter when a specific inner page (e.g., a branch office page) needs its own schema.
Service business without a storefront: Use 'areaServed' instead of address — add it manually to the schema object returned by the builder; no UI change needed in the config file.
See also