Skip to content

SEO Programmatic

github-actions[bot] edited this page Aug 1, 2026 · 4 revisions

SEO & Programmatic Pages

360Ghar's growth engine is a programmatic SEO (pSEO) system that generates hundreds of city/intent/type landing pages, faceted BHK/budget/amenity pages, locality pages, sitemaps, RSS feeds, and AI-discovery artifacts at build time. Pages are prerendered with Puppeteer so crawlers receive fully rendered HTML with structured data, hreflang alternates, and internal links.

Key Files

File Role
src/pages/landing/Landing.jsx Dynamic landing page /:citySlug/:intent/:type
src/pages/landing/FacetLanding.jsx Faceted landing pages (BHK / budget / amenity)
src/pages/localities/LocalityTemplate.jsx Locality template page
src/seo/indexationPolicy.js Approved cities, static routes, budget facets, prerender seeds
src/seo/structuredData.js JSON-LD generators (Breadcrumb, FAQ, HowTo, etc.)
src/seo/aiDiscovery.js llms.txt, ai.txt, API catalog, LLM feed builder
src/seo/siteMetadata.js Default title, description, OG image
src/seo/toolSchemas.js Tool-page structured data
src/utils/internalLinks.js City normalization, BHK/budget/locality link builders
src/utils/landingKeywords.js Landing/facet keyword builders
src/utils/propertyTaxonomy.js Property type label/slug normalization
scripts/generate-sitemaps.mjs Static + landing sitemaps
scripts/generate-dynamic-sitemaps.mjs Property + locality sitemaps
scripts/generate-locality-sitemap.mjs Locality sitemap
scripts/generate-datahub-sitemap.mjs Data hub sitemap
scripts/ingest-gurgaon-entities.mjs Gurgaon entity ingestion
scripts/merge-entities.mjs Merge raw entities
scripts/build-localities-json.mjs Build localities.json
scripts/build-localities-index.mjs Build localities-index.json
scripts/write-ai-discovery.mjs Write llms.txt / ai.txt / llm-feed.json
scripts/generate-rss.mjs RSS feed generation
scripts/generate-prerender-routes.mjs Build prerender route manifest
scripts/prerender-pages.mjs Puppeteer prerendering
scripts/indexnow-submit.mjs Submit same-host URLs from generated sitemaps to IndexNow
public/ba96fa507cb7447aa74f5ddd2f516a6d.txt IndexNow verification key file served at root path

Dynamic Landing Routes

Defined in src/App.jsx and governed by indexationPolicy.js:

Route Page Example
/:citySlug/:intent/:type Landing.jsx /gurgaon/buy/flats
/:citySlug/:intent/:type/:bhk FacetLanding.jsx /gurgaon/rent/flats/2-bhk
/:citySlug/:intent/:type/budget/:budget FacetLanding.jsx /gurgaon/buy/flats/budget/under-50-lakhs
/:citySlug/:intent/:type/amenity/:amenity FacetLanding.jsx /gurgaon/rent/flats/amenity/swimming-pool
/locality/:slug LocalityTemplate.jsx /locality/dlf-phase-1-gurgaon

Landing.jsx

Validates citySlug against approvedIndexableCitySlugs, normalizes via normalizeCitySlug(), and checks isIndexableCitySlug() for the noindex decision. intent is constrained to buy | rent | pg; type is normalized through normalizePropertyTypeToken() and getPropertyRouteSlug(). The page:

  • Builds a property search query (buildPropertySearchQuery) and embeds live results.
  • Generates FAQ structured data via buildLandingFaqs() (8 Q&As, i18n-aware with Hindi verb variants).
  • Emits Breadcrumb + FAQ JSON-LD.
  • Renders related links (other intents, BHK facets, budget facets, localities) via getRelatedLandingLinks().
  • Includes AiFactSheet for AI crawlers.

FacetLanding.jsx

Handles three facet types in one component:

  • BHK - VALID_BHKS = ['1-bhk','2-bhk','3-bhk','4-bhk','5-bhk']
  • Budget - rent (under-10k, under-15k, under-20k) and buy (under-50-lakhs, under-80-lakhs, under-1-crore) from indexableBudgetFacets
  • Amenity - arbitrary amenity slugs

isIndexableFacetLanding() decides noindex. FAQs are built by buildFacetFaqs() with Hindi action variants. Invalid combinations redirect via <Navigate>.

Entity Ingestion Pipeline

scripts/ingest-gurgaon-entities.mjs fetches sitemaps from Magicbricks, SquareYards, NoBroker, and CommonFloor, extracts <loc> URLs matching gurgaon|gurugram, and decodes locality/society/project names from the URL slugs. Output: scripts/reports/entity-raw.json.

The pipeline (run by npm run build:entities):

  1. ingest-gurgaon-entities - fetch external sitemaps -> entity-raw.json
  2. merge-entities - dedupe + merge raw entities
  3. build-localities-json - src/data/localities.json
  4. build-localities-index - src/data/localities-index.json (sorted, slugged)
  5. generate-locality-sitemap - public/sitemap-localities.xml

isPlaceholderName() filters out junk tokens like povp abc123. Each entity carries confidence and sourceCoverage for quality scoring.

Sitemap Generation

npm run build:sitemaps runs four scripts:

Script Output
generate-sitemaps.mjs sitemap.xml (index), sitemap-static.xml, sitemap-landing.xml
generate-locality-sitemap.mjs sitemap-localities.xml
generate-datahub-sitemap.mjs sitemap-datahub.xml
generate-dynamic-sitemaps.mjs sitemap-properties.xml + any dynamic batches

hreflang Alternates

buildAlternates(enPath) emits en, hi, and x-default for every URL. Each English URL is paired with its /hi/ Hindi twin in the same <url> block, so Google can serve the correct locale.

Pruning + Batching

  • src/data/pseo-prune-list.json - patterns (path, path/*, or regex) to exclude.
  • SITEMAP_MAX_LANDING_PER_CITY / SITEMAP_BATCH env vars cap the count per city for phased releases.
  • validateSitemapUrl() skips redirected patterns (/gurugram/, /apartments).

Search Index Notification

On production builds, after sitemaps are generated and prerendering is complete, the new build:indexnow step calls scripts/indexnow-submit.mjs to notify Bing IndexNow of publish-ready URLs.

The script:

  • reads all generated sitemap*.xml files (dist/ then public/ fallback),
  • extracts <loc> URLs, filters same-host links,
  • submits deduplicated URLs in batches to api.indexnow.org.

Configuration is through optional environment variables:

Variable Default Effect
INDEXNOW_API_KEY ba96fa507cb7447aa74f5ddd2f516a6d API key in notification payload
INDEXNOW_ENDPOINT https://api.indexnow.org/IndexNow Submission endpoint
INDEXNOW_KEY_LOCATION https://360ghar.com/ba96fa507cb7447aa74f5ddd2f516a6d.txt Public key file URL
INDEXNOW_BATCH_SIZE 1000 Maximum URLs per request
INDEXNOW_DRY_RUN 0 1 to disable network calls for testing
INDEXNOW_ENFORCE_SUCCESS 0 1 to fail the full build if any batch fails

AI Discovery (write-ai-discovery.mjs)

buildAiDiscoveryArtifacts() from src/seo/aiDiscovery.js produces:

Artifact Path Purpose
llms.txt public/llms.txt Concise LLM-readable site summary
llms-full.txt public/llms-full.txt Expanded version
ai.txt public/.well-known/ai.txt AI crawler policy
api-catalog public/.well-known/api-catalog application/linkset+json API catalog
llm-feed.json public/data/llm-feed.json Structured feed enriched with top 50 localities + 20 societies

The script reads localities-index.json to enrich feed.top_localities and feed.top_societies and set metadata.total_localities / total_societies.

netlify.toml advertises these via Link headers on / (RFC 8288): api-catalog, service-doc, service-meta, mcp-server, agent-skills, llms-txt, openid-configuration.

RSS Feed (generate-rss.mjs)

Fetches blog posts and properties via fetchPaginatedCollectionParallel from scripts/lib/paginatedApi.mjs, plus localities from localities-index.json, and builds:

  • public/rss.xml - main feed (blog + properties)
  • public/rss/localities.xml - locality feed

Each item is XML-escaped and dated with toRfc2822(). Property titles synthesize a description from type, purpose, and locality.

Structured Data / JSON-LD

src/seo/structuredData.js generates:

  • generateBreadcrumbStructuredData
  • generateFaqStructuredData
  • generateHowToStructuredData
  • generatePropertyProductStructuredDataProduct with offers (a priceless "Contact for price" Offer when price data is missing, since price:0 and offerless Products are both rejected), aggregateRating, and review[].
  • generateJobPostingStructuredData (library helper; careers pages build JobPosting inline in Careers.jsx / CareerDetails.jsx with title, datePosted/validThrough sourced from src/data/careers.js postedDate, baseSalary, and a complete PostalAddress incl. streetAddress + postalCode). The helper defaults emit the same complete PostalAddress (Sector 50, Gurugram / 122001); validateStructuredData.js enforces Google's JobPosting required set (title, description, datePosted, validThrough, employmentType, hiringOrganization, jobLocation, baseSalary) and warns on missing streetAddress/postalCode — the exact "Missing field" error/warnings seen in the GSC Job Postings report.
  • Review blocks (CustomerReviews.jsx, ReviewDisplay.jsx) emit a typed parent (@type: RealEstateAgent, a LocalBusiness subtype) wrapping aggregateRating and review[] together — a standalone AggregateRating with itemReviewed is rejected by Google.
  • Tool-page schemas via src/seo/toolSchemas.js

These are emitted through <Helmet> in each page's SEO component (src/common/SEO.jsx).

Field-level validation (CI)

npm run validate:schemas (scripts/validate-structured-data.mjs) now imports the real validators and runs field-level checks — not just regex presence checks: every realEstateStructuredData entry plus representative generator outputs (JobPosting, Article, VideoObject) are run through validateSchema and any error fails the build. Rules in src/seo/validateStructuredData.js track Google's rich-result required fields (Product offers/aggregateRating/review, Article image/author/publisher, Event endDate/eventStatus/eventAttendanceMode/image, VideoObject uploadDate, JobPosting full set incl. streetAddress/postalCode warnings, validThrough date check); required props use a == null check so x || undefined emissions are caught; nested errors propagate into isValid. Site-wide 2026-08 audit fixed: broken logo.png/office.jpg URLs → assets/images/logo/logo.png; missing streetAddress on the sitewide Organization block; fabricated numberOfItems: 1000; fabricated aggregateRating/review fallbacks (gated on real data); Event schema on /bank-auctions/:id; Article image/publisher.logo on guide pages; new Date()-derived dates (static now); fabricated verifiedCount: 500 ClaimReview on locality pages; /data-hub/* soft-404 301s in public/_redirects + netlify.toml.

Canonical, Meta & hreflang

  • Canonical is deterministic from the URL path (pathLocale), never the i18n store, so it cannot flip between / and /hi/ across renders. The canonical prop override still wins when passed. There is no static <link rel="canonical"> in index.html (see the head-tag dedup gotcha in Build Pipeline).
  • Meta description is owned per-page by the SEO component (description || siteMetadata.defaultDescription); index.html ships no static name="description" so the per-page value is the sole one Google sees.
  • hreflang alternates are built per-page by buildHreflangs(canonicalUrl) in SEO.jsx; index.html ships no static hreflang to avoid conflicting alternates (e.g. en pointing to both / and the current page).

Prerendering

scripts/generate-prerender-routes.mjs builds scripts/prerender-routes.json from indexableStaticRoutes, seedLandingPrerenderRoutes, seedLocalityPrerenderRoutes, and locality data. Each route has a waitForSelector / waitForText / waitForTitle so Puppeteer knows when the page is ready.

scripts/prerender-pages.mjs:

  1. Spawns vite preview on 127.0.0.1:4317.
  2. Launches headless Puppeteer (--no-sandbox on Linux for Netlify).
  3. For each route, navigates, waits for the configured signal (60s timeout), sets window.__PRERENDER_INJECTED = { isPrerendering: true }, and serializes page.content() to dist/<route>.html.
  4. Rewrites stylesheets to be non-blocking (strips duplicate <noscript> copies, converts media="print" preloads).

The isPrerendering flag is checked by authStore.initializeAuth, locationStore.initializeLocation, posthogService.init, and main.jsx to skip network calls during prerender.

SEO Pipeline

flowchart TD
    Ext[External sitemaps] --> Ingest[ingest-gurgaon-entities]
    Ingest --> Raw[entity-raw.json]
    Raw --> Merge[merge-entities]
    Merge --> LJ[localities.json]
    LJ --> LIdx[localities-index.json]
    LIdx --> LocTmpl[LocalityTemplate pages]
    LIdx --> AI[write-ai-discovery]
    LIdx --> RSS[generate-rss]
    Pol[indexationPolicy] --> Land[Landing + Facet pages]
    Land --> SM[generate-sitemaps]
    LIdx --> SM
    SM --> Sitemaps[sitemap-*.xml]
    Sitemaps --> IDX[submit to IndexNow]
    IDX --> Dist[dist/*.html]
    Pol --> PR[generate-prerender-routes]
    Land --> PR
    LIdx --> PR
    PR --> PP[prerender-pages Puppeteer]
    PP --> Dist
Loading

Cross-References

Clone this wiki locally