14 KiB
SEO Audit — Base Document
A running reference for auditing Civitai's SEO posture. This is the "what we know and where the gaps are" doc — findings and fixes should be appended as sections below.
Started: 2026-04-24 (Briant).
Context: .com vs .red
Civitai runs under two canonical hosts driven by the same codebase:
| Domain | Role | Content |
|---|---|---|
civitai.com (green) |
SFW site | Content whose nsfwLevel passes hasSafeBrowsingLevel |
civitai.red (red / blue) |
NSFW site | Everything, including mature content |
The domain is resolved server-side in _app.tsx:289
(canIndex = serverDomainMap values includes host) and surfaced to React via
useDomainColor() in src/hooks/useDomainColor.tsx.
Feature-flag style: isGreen = .com, isBlue / isRed = .red variants.
Crawler behavior at the domain boundary
When a user requests an NSFW resource on .com:
- SSR renders the page.
<Meta>is rendered outside<SensitiveShield>so meta tags are always present.- Inside the shield, a replacement card ("Mature content has a new home → civitai.red") is rendered instead of the real body.
- Googlebot (logged-out, safe-level only) receives: canonical title + description for the article, but a body that redirects off-site.
This creates a content/meta mismatch if the URL remains indexable on .com.
The fix pattern is to toggle deIndex based on both nsfwLevel and the domain:
when we're on green and content is NSFW, emit noindex,nofollow so the .red
URL wins the canonical fight.
Meta component safeguards
Meta already does the right thing for
images: line 75 only selects
getIsSafeBrowsingLevel images for og:image. The primary source is the
/api/og endpoint, which is expected to render a SFW preview card regardless of
content rating. If we ever allow raw-image fallbacks for NSFW content, this
guard is the last line of defense.
deIndex, canonical, and alternate are page-level controls. Each page is
responsible for passing the right values.
Global indexing gate
canIndex is true whenever the host matches a canonical server domain. It is
NOT sensitive to content rating. .red is currently in canIndex: true
territory; whether we want .red content indexed at all is a product question —
see Open Questions below.
Checklist: what a detail-page audit should cover
For every detail page (models, images, posts, articles, collections, user profiles, bounty, club, etc.):
<Meta>is outside any visibility gate (SensitiveShield, login gate, mature-content blur).descriptionis derived from content that is safe to show in a SERP snippet, or the page is de-indexed when the content is NSFW.og:imageuses either/api/ogor a SFW-filtered selector (never an unfilteredimage.url).deIndexis set when any of: - content is unpublished / draft, -availability === Unsearchable, - we're on.comand the content is NSFW.canonicalpoints at the production URL with slug (or the slug-less URL if that's the canonical choice — be consistent).alternateis set if slug-less and slug'd URLs both resolve.- JSON-LD schema is present where it adds value (Article, Product, Person, ImageObject, etc.) — optional, but worth noting.
Audit checklist
Tick a box once the page has been audited against the checklist above and an entry has been added to Findings below (even if no fix was needed — a "no issues" note counts).
P0 — Detail pages with NSFW content
Top priority: these are the pages most likely to produce the content/meta
mismatch described above. All have user-rated content; all need the NSFW-aware
deIndex guard.
/articles/:id/:slug?— articles/[id]/[[...slug]].tsx (fixed 2026-04-24)/models/:id/:slug?— models/[id]/[[...slug]].tsx/model-versions/:id— model-versions/[id].tsx/images/:imageId— images/[imageId].tsx/posts/:postId/:postSlug?— posts/[postId]/[[...postSlug]].tsx/bounties/:id/:slug?— bounties/[id]/[[...slug]].tsx/bounties/:id/entries/:entryId— bounties/[id]/entries/[entryId]/index.tsx/collections/:collectionId— collections/[collectionId]/index.tsx/comics/:id/:slug?— comics/[id]/[[...slug]].tsx
P1 — User-facing pages with mixed or derived content ratings
These aggregate user content (so they can surface NSFW previews in meta) and are SEO-visible.
/user/:username— user/[username]/index.tsx/user/:username/models— user/[username]/models.tsx/user/:username/images— user/[username]/images.tsx/user/:username/posts— user/[username]/posts.tsx/user/:username/videos— user/[username]/videos.tsx/user/:username/articles— user/[username]/articles.tsx/user/:username/collections— user/[username]/collections.tsx/user/:username/comics— user/[username]/comics.tsx/user/:username/:list— user/[username]/[list].tsx (catch-all list route)/user-id/:userId— user-id/[userId].tsx (confirm canonical points at username URL)/tag/:tagname— tag/[tagname].tsx (tag aggregation, can include NSFW)/reviews/:reviewId— reviews/[reviewId].tsx/comments/v2/:id— comments/v2/[id].tsx (likely should de-index)/challenges/:id/:slug?— challenges/[id]/[[...slug]].tsx/events/:slug— events/[slug].tsx/leaderboard/:id— leaderboard/[id].tsx/auctions/:slug?— auctions/[[...slug]].tsx/tools/:slug— tools/[slug].tsx
P2 — Index / feed pages
High traffic, but meta is mostly static per route. Confirm titles, descriptions, and canonicals are set (not inherited from defaults).
/— index.tsx/home— home/index.tsx/models— models/index.tsx/images— images/index.tsx/videos— videos/index.tsx/posts— posts/index.tsx/articles— articles/index.tsx/bounties— bounties/index.tsx/collections— collections/index.tsx/comics— comics/index.tsx/comics/browse— comics/browse.tsx/challenges— challenges/index.tsx/challenges/winners— challenges/winners.tsx/events— events/index.tsx/tools— tools/index.tsx/builds— builds/index.tsx/search/models— search/models.tsx/search/images— search/images.tsx/search/articles— search/articles.tsx/search/bounties— search/bounties.tsx/search/collections— search/collections.tsx/search/comics— search/comics.tsx/search/tools— search/tools.tsx/search/users— search/users.tsx
P3 — Marketing / evergreen / legal
Static (or near-static) pages that should be fully indexed on .com and may
benefit from richer meta/schema.
/content/:slug*— content/[[...slug]].tsx (TOS/Privacy/etc. catch-all)/pricing— pricing/index.tsx/safety— safety/index.tsx/newsroom— newsroom/index.tsx/support— support/index.tsx/creator-program— creator-program/index.tsx/changelog— changelog/index.tsx/product/vault— product/vault.tsx/product/link— product/link.tsx/product/odor— product/odor.tsx/shop— shop/index.tsx/gift-cards— gift-cards/index.tsx/buzz/marketplace— buzz/marketplace/index.tsx
P4 — Sitemaps
Must agree with per-page deIndex decisions (submitting a noindex URL is a
contradictory signal).
/sitemap-models.xml— sitemap-models.xml/index.tsx/sitemap-articles.xml— sitemap-articles.xml/index.tsx/sitemap-tools.xml— sitemap-tools.xml/index.tsx- Decide whether to add missing sitemaps: posts, images, bounties, collections, comics, users, events, challenges.
Should be de-indexed (sanity-check only)
These don't need SEO polish but do need to reliably send noindex so they
don't clutter SERPs. Spot-check one page per group; if all members of the
group render noindex, tick the group.
- Edit / create / wizard routes (
articles/[id]/edit,articles/create,models/[id]/edit,models/[id]/wizard,models/[id]/model-versions/…,models/create,models/train,posts/[postId]/edit,posts/create,bounties/[id]/edit,bounties/create,bounties/[id]/entries/create,bounties/[id]/entries/[entryId]/edit,comics/create,comics/project/[id]/*,train,training/[workflowId],generate) - Account / billing routes (
user/account,user/notifications,user/membership,user/buzz-dashboard,user/downloads,user/transactions,user/vault,user/referrals,user/earn-potential,user/pool-estimate,user/stripe-connect/onboard) - Auth / claim / redirect (
login,login/token,verify-email,redirect,region-blocked,preview-restricted,redeem-code,claim/buzz/[id],claim/cosmetic/[id],intent/avatar,intent/post,discord/link-role,studio/confirm,subscribe/[plan],purchase/buzz) - Payments (
payment/*,tipalti/setup) - Collections helpers (
collections/[collectionId]/join,collections/[collectionId]/review,collections/youtube/auth) - Games / one-offs (
games/chopped,games/knights-of-new-order,dev/onboarding,data-graph-v2,images/iterate) - Moderator / internal (
moderator/*,research/*,testing/*)
Findings
Articles — /articles/:id/:slug?
File: src/pages/articles/[id]/[[...slug]].tsx
Status: Fixed 2026-04-24.
- ✅
<Meta>outside<SensitiveShield>. - ✅
og:imageuses/api/og?type=article&id=…; image fallback is SFW-filtered byMeta. - ✅
deIndexnow also fires whendomain === 'green'and!hasSafeBrowsingLevel(article.nsfwLevel). - ⚠️
descriptionistruncate(removeTags(article.content), 150)— can leak explicit text into SERPs if an NSFW article ever slips through to the indexable set. Mitigated in practice by the green-domaindeIndexguard, but still a risk for.redif we decide to index it.
Open Questions
- Should
.redbe indexable at all? CurrentlycanIndexis true on.redbased purely on host match. Indexing NSFW URLs is legitimate (there's demand for mature search), but it has implications for ad networks, safe-search compliance, and backlink profile. Decide this explicitly rather than letting it be a side effect of thecanIndex= host check. - Canonical format consistency. Articles use
/articles/:id/:slugas canonical with/articles/:idasalternate. Other entity types should follow the same convention or we should document the divergence. /api/ogresilience. The OG endpoint is the primary social image for all shared links. Confirm it always renders a SFW card (including for NSFW-but-blurred-preview cases) and that it fails gracefully (we serve something, not a 500, if the entity is missing).- Sitemap coverage. Do our sitemaps respect the same green/NSFW de-index
rules as the pages themselves? A sitemapped URL that returns
noindexis a contradictory signal to Google.
Tooling notes
- Google Search Console is the source of truth for indexed-vs-submitted counts and coverage errors; pair any code finding with a GSC lookup to confirm real impact.
- For per-page verification,
curl -A "Googlebot" <url>against the deployed site is the fastest way to see exactly what the crawler receives (meta tags and body copy). - Dev-server rendering is sufficient for checking meta tags but not for
verifying full SSR output — use a production build (
pnpm run build+pnpm run start) when the server-render path matters.