-
Notifications
You must be signed in to change notification settings - Fork 2
SEO Programmatic
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.
| 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 |
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 |
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
AiFactSheetfor AI crawlers.
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) fromindexableBudgetFacets - Amenity - arbitrary amenity slugs
isIndexableFacetLanding() decides noindex. FAQs are built by buildFacetFaqs() with Hindi action variants. Invalid combinations redirect via <Navigate>.
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):
-
ingest-gurgaon-entities - fetch external sitemaps ->
entity-raw.json - merge-entities - dedupe + merge raw entities
-
build-localities-json -
src/data/localities.json -
build-localities-index -
src/data/localities-index.json(sorted, slugged) -
generate-locality-sitemap -
public/sitemap-localities.xml
isPlaceholderName() filters out junk tokens like povp abc123. Each entity carries confidence and sourceCoverage for quality scoring.
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 |
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.
-
src/data/pseo-prune-list.json- patterns (path,path/*, or regex) to exclude. -
SITEMAP_MAX_LANDING_PER_CITY/SITEMAP_BATCHenv vars cap the count per city for phased releases. -
validateSitemapUrl()skips redirected patterns (/gurugram/,/apartments).
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.
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.
src/seo/structuredData.js generates:
generateBreadcrumbStructuredDatagenerateFaqStructuredDatagenerateHowToStructuredData- Tool-page schemas via
src/seo/toolSchemas.js
These are emitted through <Helmet> in each page's SEO component (src/common/SEO.jsx).
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:
- Spawns
vite previewon127.0.0.1:4317. - Launches headless Puppeteer (
--no-sandboxon Linux for Netlify). - For each route, navigates, waits for the configured signal (60s timeout), sets
window.__PRERENDER_INJECTED = { isPrerendering: true }, and serializespage.content()todist/<route>.html. - Rewrites stylesheets to be non-blocking (strips duplicate
<noscript>copies, convertsmedia="print"preloads).
The isPrerendering flag is checked by authStore.initializeAuth, locationStore.initializeLocation, posthogService.init, and main.jsx to skip network calls during prerender.
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]
Pol --> PR[generate-prerender-routes]
Land --> PR
LIdx --> PR
PR --> PP[prerender-pages Puppeteer]
PP --> Dist[dist/*.html]
-
Build Pipeline - how these scripts chain in
npm run build - Internationalization - Hindi hreflang twins
- Authentication - prerender skips auth init