How Notes emits Schema.org JSON-LD. Source of truth: src/utils/structuredData.ts, rendered by StructuredData.astro from BaseLayout.astro.
Site constants (SITE_TITLE, AUTHOR, SEO_CONFIG, SOCIAL_LINKS) live in src/consts.ts. Image URLs go through generateImageUrl() (social-safe JPEG/PNG when a manifest variant exists). Canonical URLs never use trailing slashes (trailingSlash: 'never').
BaseLayout always calls generateStructuredData(...) and emits one <script type="application/ld+json"> per schema object.
| Layout / page prop | structuredDataType |
Extra schemas beyond base set |
|---|---|---|
| Default / most pages | website |
none |
BlogLayout (posts) |
article |
BlogPosting + BreadcrumbList (requires pubDate) |
category/[category] |
category |
CollectionPage when posts.length > 0 |
tag/[tag] |
tag |
CollectionPage when posts.length > 0 |
Helpers such as FAQ / HowTo / Review / generateEnhancedStructuredData exist in the module but are not wired into layouts today. Do not assume they appear in page HTML.
Always emitted first:
- WebSite —
name: Notes, site description/URL,inLanguage: en-US,publisheras Person. NoSearchAction(site search is client-only; there is no crawlable/search?q=endpoint). - Organization —
namefromSEO_CONFIG.organizationName(author name), logo viagenerateImageUrl,sameAs: Object.values(SOCIAL_LINKS), foundingDate2024, areaServed / serviceType strings. - Person (author) —
AUTHORfields, subsetsameAs(twitter, github, bluesky),knowsAbouttopic list, occupation metadata.
Requires pubDate. Emits:
Notable fields:
headline,description,image,datePublished,dateModified(falls back topubDate)author/publisher(Person / Organization with logo)keywords(comma-joined),timeRequiredasPTnMwhenminutesReadparsesurl(canonical),inLanguagefrom layout (post language →en-US/es-ES)wordCountwhen provided (> 0)mainEntityOfPage,isPartOf→ Blog namedNotesaboutfromcategory[]asThings when categories existarticleSection: primarycategory[0]if present; else first three tags joined; else"Personal Growth"featured→isAccessibleForFree: true;draft→isAccessibleForFree: false- TOC present →
hasPartWebPageElement named “Table of Contents”
Home → optional first category (/category/{id}) → post title. Positions adjust when no category.
Only when posts is non-empty. Emits CollectionPage with:
mainEntity→ItemListof compactBlogPostingitems (headline, description, url, dates, author, image, keywords, articleSection, timeRequired)- Nested
breadcrumb: Home → Categories|Tags index → current page - Category pages with
identifier:aboutThing - Tag pages with
identifier:keywords: identifier - Collection
inLanguageis hardcodeden-US(not post-language-aware)
Empty category/tag result sets fall back to the base three schemas only.
| Export | Intent | Wired to HTML? |
|---|---|---|
generateFAQSchema / autoDetectFAQSchema |
FAQPage from Q&A markdown heuristics | No |
generateEnhancedStructuredData |
Base schemas + auto FAQ for articles | No |
generateHowToSchema |
HowTo tutorials | No |
generateReviewSchema |
Review / rating | No |
generateArticleSchema |
Generic Article (vs BlogPosting) |
No |
generateContentTypeSpecificSchema |
Switch for how-to / review / faq | No |
validateStructuredData / generateStructuredDataSummary |
Dev/debug helpers | No (CI uses a separate smoke script) |
Wire these only with intentional layout changes and Rich Results expectations — auto-FAQ heuristics are noisy.
pnpm run validate-structured-dataSmoke-checks that structuredData.ts still exports generateStructuredData, generateArticleSchema, and validateStructuredData, and mentions core Schema.org types. It does not crawl live HTML or call Google’s Rich Results Test.
For live checks:
- Google Rich Results Test
- Search Console → Enhancements / Experience reports after deploy
Optional local helper:
import { validateStructuredData, generateStructuredDataSummary } from '../utils/structuredData';- Base WebSite / Organization / Person always use
inLanguage: en-USeven on Spanish posts (onlyBlogPosting.inLanguagefollows the post). - FAQ / HowTo / Review helpers are dead code unless product wants them on specific posts.
hasCommentsis accepted on options but unused in schema output.- Collection schemas list every post in the page’s
postsprop — keep that list bounded if indexes grow large.
- Meta / Open Graph / hreflang:
src/utils/seo.ts,BaseHead.astro - Multilingual listing vs URL policy:
docs/multilingual-setup.md - Technical audit:
docs/TECHNICAL-AUDIT.md