with one click
seo-meta
// SEO meta tags, Open Graph, and structured data requirements for christophergarza.dev. Trigger when a PR adds or modifies pages, layouts, head metadata, sitemaps, robots configuration, or JSON-LD.
// SEO meta tags, Open Graph, and structured data requirements for christophergarza.dev. Trigger when a PR adds or modifies pages, layouts, head metadata, sitemaps, robots configuration, or JSON-LD.
[HINT] Download the complete skill directory including SKILL.md and all related files
| name | seo-meta |
| description | SEO meta tags, Open Graph, and structured data requirements for christophergarza.dev. Trigger when a PR adds or modifies pages, layouts, head metadata, sitemaps, robots configuration, or JSON-LD. |
Every public-facing page needs a complete metadata set. This skill defines the standard. It pairs with the buildMetadata helper at src/lib/seo.ts — pages should never assemble metadata by hand.
These live in src/lib/seo.ts and must never be hardcoded elsewhere. If you see a string literal for the domain, name, or socials anywhere outside this config file, that's a block.
export const siteConfig = {
name: "Christopher Garza",
domain: "https://www.christophergarza.dev",
description:
"Senior Software Engineer, Frontend at the seam between marketing and engineering. Building the technical systems behind pricing, conversion, and analytics for enterprise products. React • TypeScript • Next.js • Node.js",
jobTitle: "Senior Software Engineer, Frontend",
author: {
name: "Christopher Garza",
image: "https://www.christophergarza.dev/profile_triumph_pic.jpg",
sameAs: [
"https://github.com/cgarza1992",
"https://www.linkedin.com/in/christopher-garza-dev/",
],
},
locale: "en_US",
defaultOgImage: "https://www.christophergarza.dev/og/default.png",
} as const;
Pages call buildMetadata from src/lib/seo.ts, which returns a Next.js Metadata object. The helper is the single source of truth for tag shape — never bypass it.
export const metadata = buildMetadata({
title: "WP Engine Hosting Plans",
description: "Built the purchase funnel bridging WP Engine's marketing site and product portal. Redux-driven plan selection, Salesforce integration, Optimizely A/B testing.",
path: "/case-studies/wpe-plans",
ogImage: "/og/wpe-plans.png",
type: "article",
});
The helper handles: title suffix ( | Christopher Garza), absolute URLs, canonical, og:* tags, locale, and metadataBase. It does not set twitter fields explicitly — see the Twitter tags policy below for why.
After buildMetadata runs, the rendered HTML must contain:
<title>{Page-specific title} | Christopher Garza</title>
<meta name="description" content="{120-160 char page-specific summary}">
<link rel="canonical" href="https://www.christophergarza.dev{path}">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="robots" content="index, follow">
<meta property="og:type" content="website"> <!-- "article" for case studies -->
<meta property="og:url" content="https://www.christophergarza.dev{path}">
<meta property="og:title" content="{title}">
<meta property="og:description" content="{description}">
<meta property="og:image" content="https://www.christophergarza.dev{ogImage}">
<meta property="og:image:width" content="1200">
<meta property="og:image:height" content="630">
<meta property="og:image:alt" content="{describes the image}">
<meta property="og:site_name" content="Christopher Garza">
<meta property="og:locale" content="en_US">
| Christopher Garza appended by the helperSenior Software Engineer, Frontend | Christopher Garzawww. (the live site uses www.)LinkedIn matters most for this site — recruiters share links there constantly, and LinkedIn aggressively caches OG data. Make the OG image worth sharing.
og:image URLs are blocked by every platform/og/default.png for everything; case studies may add /og/{slug}.pngIf a per-route OG image is referenced but the file doesn't exist in /public/og/, that's a block.
Next.js 16 automatically derives twitter:* tags from the openGraph block in a Metadata object. These framework-emitted tags are acceptable — their content mirrors the OG block, which is the whole point of having OG metadata in the first place. Suppressing them is not possible without dropping the openGraph block entirely (verified empirically: setting twitter: null or casting individual fields to null does not prevent emission; Next.js 16 postProcessMetadata() and resolveTwitter() unconditionally backfill twitter title/description/image from openGraph).
What's forbidden is manually adding twitter:* fields. Specifically:
twitter.card, twitter.creator, twitter.site, or any twitter property in a Metadata export<meta name="twitter:*"> tags in JSXManual twitter metadata implies platform-specific tuning that the site doesn't need. Let the framework do it; don't fight it.
Inject via a <JsonLd> component using dangerouslySetInnerHTML with JSON.stringify. JSON-LD does not go in the metadata export — Next.js metadata API doesn't support it. If you see JSON-LD in generateMetadata, that's a block.
Validate every schema with Google's Rich Results Test before merging.
Person schemaRequired on /. Build from siteConfig — do not duplicate strings that already live in config.
{
"@context": "https://schema.org",
"@type": "Person",
"name": "Christopher Garza",
"url": "https://www.christophergarza.dev",
"image": "https://www.christophergarza.dev/profile_triumph_pic.jpg",
"jobTitle": "Senior Software Engineer, Frontend",
"description": "Senior Software Engineer, Frontend at the seam between marketing and engineering. Building the technical systems behind pricing, conversion, and analytics for enterprise products.",
"knowsAbout": [
"React",
"TypeScript",
"Next.js",
"Vue.js",
"Node.js",
"Conversion Rate Optimization",
"A/B Testing",
"Component Architecture",
"Analytics Infrastructure",
"Segment"
],
"sameAs": [
"https://github.com/cgarza1992",
"https://www.linkedin.com/in/christopher-garza-dev/"
]
}
Article schemaRequired on every /case-studies/{slug} route. Use real dates from git log when available; omit datePublished / dateModified rather than fabricating them.
{
"@context": "https://schema.org",
"@type": "Article",
"headline": "{Case study title}",
"datePublished": "{YYYY-MM-DD if known}",
"dateModified": "{YYYY-MM-DD if known}",
"author": {
"@type": "Person",
"name": "Christopher Garza",
"url": "https://www.christophergarza.dev"
},
"publisher": {
"@type": "Person",
"name": "Christopher Garza"
},
"image": "https://www.christophergarza.dev/og/{slug}.png",
"mainEntityOfPage": "https://www.christophergarza.dev/case-studies/{slug}",
"description": "{120-160 char summary, matches the page meta description}"
}
BreadcrumbListHelps Google show breadcrumb trails in search results. Acceptable on case studies once they're stable.
Organization schema — this is a personal portfolioProduct schema for the demosReview or Rating schema unless backed by real third-party reviewsBlogPosting on case studies — they're Article, not blog postsBoth are Next.js dynamic routes, not static files. If public/sitemap.xml or public/robots.txt exist as static files alongside the dynamic routes, that's a block — they conflict and cause confusion.
src/app/sitemap.ts must include every public route. New routes added in a PR must be added to the sitemap in the same PR.
src/app/robots.ts must:
/ for */api/The Storybook (storybook.christophergarza.dev) and demo subdomains (fastspring.christophergarza.dev, partners.christophergarza.dev) are out of scope for the main repo's metadata work, but if a PR touches their config:
noindex to avoid diluting the main domain in search resultsrobots.txt with Disallow: / or a site-wide <meta name="robots" content="noindex"><title> or meta descriptionbuildMetadatasrc/lib/seo.tstwitter:* field is introduced (Metadata.twitter export, hardcoded meta tag, etc.) — framework-emitted twitter tags from openGraph are fineog:image is missing, points to a non-existent file in /public/og/, or uses HTTPog:image dimensions don't match og:image:width / og:image:heightwww.metadata export instead of rendered as a <script> tagsitemap.tsnoindex is left on a public page by mistakepublic/sitemap.xml or public/robots.txt exist as static files alongside the dynamic routesPerson schema's sameAs includes anything other than the LinkedIn and GitHub URLs in siteConfig.author.sameAsBlogPosting instead of Articlehttps://search.google.com/test/rich-results?url=<preview-url>https://validator.schema.org/ for structured data not yet eligible for rich resultshttps://www.linkedin.com/post-inspector/ (the only way to force LinkedIn to refresh its OG cache)curl -sL <preview-url> | grep -iE '<(title|meta|link|script)' | head -40