name: noir-seo-check
description: Verify SEO + social-sharing metadata on NOIR public-facing pages (blog posts, product pages, category pages, landing pages). Use when the user asks to audit SEO, add meta tags, improve search ranking, add Open Graph / Twitter Card / JSON-LD / Schema.org, check structured data, or ensure rich snippets work. Complements the seo-audit + schema-markup skills from marketing-skills plugin with NOIR-specific DTO knowledge and enforcement of docs/backend/research/seo-meta-and-hint-text-best-practices.md.
noir-seo-check — SEO + structured data for NOIR public pages
NOIR has significant public-facing surface area: blog posts, product detail pages, product category pages, public tenant landing. Each needs proper metadata to rank and share well. This skill walks a full audit + remediation for a given page type.
When to invoke
User says anything like:
- "audit SEO for the blog"
- "the product pages don't have Open Graph tags"
- "Google doesn't show rich snippets for our reviews"
- "generate structured data for X"
- "check meta descriptions"
- "fix sitemap"
Prerequisites
- The
marketing-skillsplugin (fromcoreyhaines31/marketingskills) is installed — it provides genericseo-audit+schema-markupworkflows. This skill layers NOIR-specific knowledge on top. - Reference doc:
docs/backend/research/seo-meta-and-hint-text-best-practices.md— the team's SEO principles. Read this before audit.
Page types + what to check
| Page type | Required meta | Required structured data | NOIR DTO source |
|---|---|---|---|
| Blog post | title ≤60, meta description 120–160, canonical URL, og:title, og:description, og:image, og:type=article, twitter:card | Article with author, datePublished, dateModified, image, headline |
BlogPostDto — title, excerpt, featuredImageUrl, publishedAt, author |
| Product detail | title, description, canonical, og:* | Product with offers, priceCurrency, price, availability, aggregateRating if reviews exist |
ProductDto + ProductVariantDto + aggregated review data |
| Product category | title, description, canonical | ItemList of product URLs + BreadcrumbList |
ProductCategoryDto + top N products |
| Blog category / tag | title, description, canonical | Blog + BreadcrumbList |
BlogCategoryDto / PostTagDto |
| Reviews (per product) | n/a (embedded) | Review aggregated into parent Product.aggregateRating |
ReviewDto |
| Tenant landing / marketing | title, description, canonical, og:*, twitter:card | Organization or WebSite with potentialAction (SearchAction) |
Tenant settings |
Workflow
Phase 1 — Audit (read-only)
- Run the generic audit first. If the
seo-auditskill frommarketing-skillsis available, invoke it to get the baseline (broken links, missing meta, duplicate titles). - Check the NOIR-specific dimensions:
- Every public page component has a
<Helmet>orreact-helmet-asyncblock OR equivalent (meta tags rendered server-side for bot visibility — if client-rendered only, Google + social crawlers often can't read them) - Check SSR strategy: grep for
react-helmet,Head, or<title>dynamic binding. If the frontend is pure CSR and there's no SSR/prerender layer, SEO is fundamentally broken — flag this as HIGH severity - Meta description in
BlogPost/Productentity has been populated (seeder, admin editor, or auto-generated) featuredImageUrl/thumbnailUrlis absolute (not relative) for og:imageslugcolumn has a unique-per-tenant index (canonical URL correctness)
- Every public page component has a
- Check structured data:
- Are JSON-LD
<script type="application/ld+json">blocks rendered? - Do they validate against schema.org? Test with the Rich Results Test tool
- For products with reviews: is
aggregateRatingaggregated fromReviewDto?
- Are JSON-LD
- Check sitemap:
GET /sitemap.xmlreturns valid XML (if NOIR has one)- Includes all public blog posts + products + categories
<lastmod>reflects entityModifiedAt- Robots.txt points at the sitemap
- Check Core Web Vitals impact:
- Image dimensions set (prevents CLS)
- Fonts preloaded
- Critical CSS inlined (unless Vite's default + Tailwind coverage is sufficient)
Phase 2 — Report
Output a findings table:
| Severity | Page type | Issue | Fix |
|---|---|---|---|
| HIGH | Blog post | No SSR — meta invisible to Googlebot | Add prerender or move blog to SSR route |
| HIGH | Product | Missing JSON-LD | Add Product structured data component |
| MEDIUM | Blog post | Meta description empty on 42 posts | Backfill via seeder or admin UI |
| LOW | Product | og:image uses relative URL | Prefix with absolute base URL |
Phase 3 — Remediation (with user consent)
For each HIGH/MEDIUM issue:
- Identify the affected files (grep for the page component)
- Show the diff before applying
- Update BOTH the page component AND any relevant DTO/entity (e.g. add
MetaTitle,MetaDescription,OgImageUrlcolumns toBlogPostvia migration if missing) - If backfilling DB data: write a one-time seeder or Hangfire job, don't update rows manually
- Add/update i18n keys if the meta is multi-locale (EN + VI)
Phase 4 — Verify
- View-source the page — all tags present
- Run the Rich Results Test on a representative URL
- Re-run
audit-website/seo-audit— deltas should show only fixes - Update
docs/backend/research/seo-meta-and-hint-text-best-practices.mdif a new pattern emerged
Anti-patterns this skill prevents
- Client-rendered meta tags — React setting
<title>only on mount; Googlebot sees the pre-render HTML which has the default title. If not SSR'd or prerendered, you must either migrate the public routes to SSR, add a prerender service (Prerender.io, Rendertron), or generate static meta at build time. - Relative og:image URLs — Facebook/Twitter/LinkedIn unfurl uses the origin from their crawler, not the page. Always absolute.
- Duplicate canonical URLs — tenant-slug pages that share a slug across tenants (e.g.
acme/products/widgetandglobex/products/widget) need tenant-scoped canonicals. Rule 18 (tenant-scoped unique indexes) supports this at the DB layer; the canonical URL must reflect the tenant subdomain or path. - Missing
aggregateRating— you haveReviewDtodata; aggregate it intoProductstructured data, don't leave it as separateReviewblocks - Over-stuffed keywords meta — Google ignores
<meta name="keywords">entirely; don't bother - Boilerplate meta descriptions — "Welcome to our site" pulls zero click-through. Generate per-entity or require editors to write one
- Empty
ModifiedAt— sitemap<lastmod>defaults toCreatedAt, signaling stale content. UseModifiedAtfrom audit columns (Rule 11 + audit columns standard)
Cross-references
marketing-skillsplugin →seo-audit,schema-markup,site-architecture,programmatic-seo,analytics-trackingaccessibility-complianceplugin → alt-text and ARIA issues overlap with SEO (image alt, heading hierarchy)ui-auditproject skill → catches accessibility issues that also hurt SEOdocs/backend/research/seo-meta-and-hint-text-best-practices.md— team's SEO principles (living doc)