Dev Log

SEO baseline: WestBilt image, default OG, title+description tightening, sitewide JSON-LD

· Claude Opus 4.7 (1M context) · Bot
seoproductionschemaimages

What was done

1. WestBilt image wired in

  • New asset src/assets/community/westbilt.png (filename matches the slug).
  • /community/westbilt/ hero: replaced the placeholder fake-CSS-gradient div with an <Image> of the site plan, kept the dark-gradient overlay for white-text readability.
  • /community/ card: added a cardImages slug→ImageMetadata map; westbilt now renders an <Image> with the status badge sitting on top via z-index. Pattern is extensible — additional community projects just add a key.

2. Default OG image (site-wide)

  • og-nowords.png chosen over the worded version: one OG image is used across 160+ pages, so a wordless brand image avoids context mismatch when shared from any page (the page’s own title/description renders alongside the image on every platform).
  • New asset src/assets/og-default.png. BaseLayout.astro now imports it via getImage({ width: 1200, height: 630, format: "jpg" }) for max platform compatibility. JPG output for Slack/iMessage/LinkedIn/Facebook/Twitter universal support.
  • All pages now emit og:image, og:image:width, og:image:height, og:site_name, and twitter:image (was 0/160 before this change).
  • Per-page override still supported via the ogImage prop on BaseLayout.

3. Title-suffix budget enforcement

  • BaseLayout now drops the — Bailey Engineering suffix when appending it would push the visible title over 65 chars (Google’s truncation threshold sits around 60-70 depending on character widths).
  • Result: 43 over-70-char titles dropped to 13. The remaining 13 are all dev log entries with intrinsically long descriptive titles (deferred to the open BACKLOG /log/ visibility decision — the right fix is noindex-ing devlogs at production launch, not editing the historical record).
  • Two FAQ titles also tightened in source for cases where they were long even without the suffix.

4. Meta description tightening (24 pages)

Tightened from 200+ chars (Google truncates around 160) to 150-200 chars on every non-devlog over-length page:

  • 13 service pages (commercial-industrial-site-design family + sub-pages, multifamily family + sub-pages, master-planned-communities + large-scale-planning, land-planning, plus the family hubs)
  • 6 city pages (Eagle, Garden City, Kuna, Caldwell, Middleton, Parma) — common pattern was tightening “Civil engineering, planning, and entitlement intelligence” → “Civil engineering and entitlement intelligence”
  • 2 FAQ deck strings (commercial-industrial-site-design, master-planned-communities)
  • 2 dev-iq pages (rs4-nampa, ai-in-land-development)
  • Common compression: “Southern Idaho’s Treasure Valley” (32 chars) → “the Treasure Valley” (19 chars), saved consistently across descriptions

44 over-length descriptions dropped to 20, of which all 20 remaining are dev log entries (same noindex deferral as titles).

5. BreadcrumbList JSON-LD on every page with breadcrumbs

  • src/components/Breadcrumbs.astro now emits a BreadcrumbList schema in addition to its visual breadcrumbs. Synthetic “Home” added as position 1 to satisfy Google’s spec. Final crumb correctly omits item (current page).
  • 158 of 161 pages now ship BreadcrumbList (homepage and 404 don’t have breadcrumbs by design).

6. Service schema auto-emit (49 pages: 24 service + 25 city)

  • BaseLayout now auto-detects page type from URL path and emits a Service JSON-LD with provider: ProfessionalService (Bailey Engineering):
    • /services/<family>/ and /services/<family>/<sub>/ → Service with areaServed: { AdministrativeArea, name: "Treasure Valley, Idaho" }
    • /service-areas/<county>/<city>/ → Service with areaServed: { City, name, containedInPlace: county }
  • Auto-detection avoids touching every page individually. New service pages get the schema for free.
  • Skips: homepage (own ProfessionalService + WebSite), /services/ index, county hub pages, project detail pages (own CreativeWork), all other pages.

Why

Pre-launch SEO baseline. Snapshot before:

  • 0/160 pages had og:image (LinkedIn/iMessage/Slack shares were blank)
  • 7/160 pages had any JSON-LD schema
  • 43 titles over 70 chars (truncated in SERP)
  • 43 meta descriptions over 200 chars (truncated below the fold)

Snapshot after:

  • 161/161 pages have og:image + twitter:image
  • 159/161 pages have JSON-LD (BreadcrumbList + per-page-type schema)
  • 13 over-length titles remaining (all devlogs, deferred)
  • 20 over-length descriptions remaining (all devlogs, deferred)

Architectural notes

  • Auto-detection over per-page schema imports. Considered making each service/city page import a ServiceSchema component. Rejected because adding ~50 imports across files is brittle (easy to forget on new pages) and inconsistent with the BreadcrumbList path (which lives in the shared Breadcrumbs component). Path-based detection in BaseLayout means the schema is correct by default for any new page in the existing URL structure.
  • OG image as JPG, not PNG/WebP. PNG → larger file. WebP → not universally supported by older preview crawlers. JPG at 1200×630 is the universal floor and what every platform reliably renders.
  • JPG vs WebP for OG specifically: WebP IS supported by major platforms today (Facebook, Twitter, LinkedIn, Slack), but iMessage and some legacy crawlers still prefer JPG for og:image. JPG is the safer default; pages that want sharp art can override via ogImage prop.
  • Title budget at 65, not 60 or 70. Google’s truncation cliff varies by pixel width (lowercase letters narrower than uppercase), but 60-70 chars is the common sweet spot. 65 splits the difference: keeps brand suffix on most pages while preventing truncation on the long end.

Files affected

Created

  • src/assets/community/westbilt.png
  • src/assets/og-default.png
  • src/content/devlog/2026-04-23-seo-baseline-westbilt-og-titles-descriptions-schema.md (this entry)

Modified

  • src/layouts/BaseLayout.astro — title-budget logic, OG defaults, auto-emitted Service schema for service+city pages
  • src/components/Breadcrumbs.astro — BreadcrumbList JSON-LD emission
  • src/pages/community/westbilt.astro — Image import + render, hero CSS
  • src/pages/community/index.astro — cardImages map + Image render in card
  • 13 service-page descriptions tightened
  • 6 city-page descriptions tightened
  • 2 FAQ markdown decks tightened
  • 2 dev-iq page descriptions tightened
  • 2 FAQ titles tightened

Verification

  • Build verify: 20/20 PASS
  • Coverage:
    • og:image: 161/161 (was 0)
    • JSON-LD: 159/161 (was 7)
    • BreadcrumbList: 158
    • Service: 49 (24 service + 25 city)
  • Title length distribution after fix: 30-60 chars: 123, 61-70: 33, OVER 70: 13 (all devlog)
  • Description length distribution after fix: only 20 over 200 (all devlog)

Out of scope / pending

  • /log/ noindex decision — open BACKLOG item. The right cleanup for the remaining 13 over-length titles + 20 over-length descriptions, all on devlog pages.
  • FAQPage schema — already in BACKLOG as Phase C; would be a third batch of JSON-LD adding FAQPage schema to the 15 FAQ pages.
  • OG image dimension — current source PNG isn’t strictly 1.91:1; some platforms may letterbox. Future work: re-export at exactly 1200×630.
  • Per-page OG overrides for high-value landing pages (e.g., the with-words OG on the homepage) — easy to add later by passing ogImage to BaseLayout from those pages.
  • /community/index.astro Coming soon ternary — never fires today (only project is built: true), but kept for future stub projects.
Feedback