Reference
The website — gurbanisoul.com
Page actions
- View as Markdown (opens in a new tab)
- Ask ChatGPT (opens in a new tab)
- Ask Claude (opens in a new tab)
ChatGPT and Claude are third-party services. The link gives them this page's public address and a short instruction. This site records nothing.
Preview: the published page may be older or missing.
The public website gurbanisoul.com: DNS, SEO, Open Graph images, security headers, imagery policy, screenshots and its gates.
https://docs.gurbanisoul.com/website/
The public web property is one Astro static site (frontend/) served on the product domain
gurbanisoul.com. It carries two distinct things, deliberately kept separate (see
Brand & domains):
- The Gurbani Soul pages —
/,/features,/watch,/learnand its articles,/privacyand/support— use the marketing layout (Marketing.astro): the product’s home, its policy pages and the Learn articles. - The Knowledge Base —
/library(its home),/search,/reader,/nitnem,/browse,/themes,/lineage,/trail,/analytics(Insights),/constellation,/raag-clock,/divergence— usesBase.astro: the Sri Guru Granth Sahib Ji Knowledge Base, the scholarly study site, with its own name, header and theme.
Both are built by astro build into frontend/dist/, synced to webapp/static/, and served by
the stdlib Python server locally and by Vercel in production. There is no second site and no
second copy of the policy pages — /privacy and /support are Astro pages under
frontend/src/pages/, and they are the single source of truth for the App Store URLs.
Domains & DNS
| Host | Role | Serves |
|---|---|---|
gurbanisoul.com | canonical apex | the site (200, no redirect) |
www.gurbanisoul.com | alias | 308 → apex |
sggs-knowledge-base.vercel.app | legacy Vercel alias | 308 → apex |
- Registrar: Hostinger (2-year term, auto-renew on, WHOIS privacy on).
- DNS: delegated to Cloudflare (account label Company-Domains), nameservers
jamie.ns.cloudflare.com/luke.ns.cloudflare.com. The apex/wwwrecords are CNAME → Vercel, DNS-only (grey cloud, not proxied) — Vercel terminates TLS and issues the certificate, and HSTS is on. We do not proxy through Cloudflare (it would double-terminate TLS and hide Vercel’s own caching/headers). - Zone ID / Account ID / API tokens live in the Cloudflare dashboard only — never in this repo.
Canonical rule. The apex is canonical. <link rel="canonical">, og:url and the sitemap all
use bare https://gurbanisoul.com/…. www and the old *.vercel.app alias 308-redirect to it, so
there is exactly one indexable host. This is asserted at release time by
sggs-verify-prod (a no-redirect guard on /, /privacy, /support) and continuously by
uptime.yml.
Mail for gurbanisoul.com is hosted by Zoho Mail (since 2026-09-26). support@gurbanisoul.com is an
alias of the owner’s mailbox, and a catch-all delivers any other address at the domain to it. DNS (Cloudflare,
DNS only): MX mx/mx2/mx3.zoho.com.au, SPF v=spf1 include:zohomail.com.au ~all, DKIM selector zmail,
and DMARC v=DMARC1; p=none; rua=mailto:support@gurbanisoul.com on _dmarc (move to p=quarantine after two
weeks of clean reports). Do not add Cloudflare Email Routing’s SPF include: it would break delivery. The address
is the public contact in the App Store listing, the EU Digital Services Act trader declaration (verified
2026-09-26), SECURITY.md, and the privacy/support pages. See
runbook: support-inbox.
SEO
frontend/astro.config.mjs sets site: 'https://gurbanisoul.com'; frontend/src/site.ts holds
SITE_URL, SUPPORT_EMAIL, APP_STORE_URL, APP_STORE_ID, the APP_STORE_LIVE switch, and the
photo-credit list. The head
SEO/social/PWA block is a shared component, frontend/src/components/Seo.astro (used by both
Marketing.astro and the Knowledge Base’s Base.astro). Every page emits:
<link rel="canonical">andog:urlbuilt fromSITE_URL+ the path (bare apex host);description,og:title/description/type/image/site_name,twitter:card=summary_large_image;theme-color— always one meta that follows the page theme, never the OS. The Knowledge Base (themeColor="page", the default) getsid="kbThemeColor": the inline boot script inBase.astrosets it to the header’s colour for the stored theme before first paint, andscripts/core.tskeeps it equal to--chromeafterwards; the light-first marketing shell (themeColor="light") getsid="themeColorMeta", pinned to paper and rewritten byscripts/theme.tson toggle.apple-mobile-web-app-title,<link rel="manifest">, anapple-touch-icon, an SVG favicon, and — once the app is live (APP_STORE_LIVE) — theapple-itunes-appSmart App Banner.
og:image defaults to the page’s OG card (/og/<slug>.png, see OG images below).
frontend/public/robots.txt points crawlers at the canonical host and names /sitemap.xml, which
is now generated (see Routes, sitemap & RSS). Base.astro derives its canonical/OG from
Astro.url.pathname so every Knowledge Base page is self-consistent without per-page props.
The SeoInvariants gate checks every built page’s canonical/description/og:image(exists on disk)/
theme-color, and that any hreflang alternate is en/x-default and self-referential (no /pa/).
Routes, sitemap & RSS
frontend/src/routes.tsis the single manifest of public routes ({ path, lastmod, changefreq, priority, og }). It must list exactly the pages that build to HTML — theSitemapInvariantsgate compares the sitemap’s<loc>set to the built HTML route set.lastmodis a fixed ISO date (neverDate.now()) so the sitemap is byte-deterministic.frontend/src/pages/sitemap.xml.tsemits/sitemap.xmlover the manifest, each URL self-referencing itsen+x-defaulthreflang alternates (nopapages ship yet).frontend/src/pages/rss.xml.tsemits/rss.xml(“Gurbani Soul — Learn”): every non-draft Learn article, newest first.RssInvariantschecks it parses and links to the site.- The old
frontend/public/sitemap.xmlwas deleted (Astro forbids a public file colliding with a page route).
OG images
frontend/src/pages/og/[slug].png.ts renders each 1200×630 card at build with satori
(HTML/CSS → SVG) + @resvg/resvg-js (SVG → PNG): warm-ink background, a gold ੴ, the title in
Source Serif 4, the “Gurbani Soul” wordmark, a thin gold rule. getStaticPaths returns one card per
distinct og slug in routes.ts (at least home and knowledge-base). satori needs static
(non-variable) TTFs, so frontend/src/og/fonts/ holds pinned instances (*-og.ttf) of Source
Serif 4 and Sant Lipi with their OFL licences; only the single ੴ glyph is Gurmukhi (satori has no
Indic shaping). Cards are byte-deterministic and each ≤ 150 KB. public/og.jpg remains a hero crop
for any fixed-image use.
Analytics
frontend/src/components/Analytics.astro is an is:inline loader that injects
/_vercel/insights/script.js only when location.hostname matches /(^|\.)gurbanisoul\.com$/
— a true no-op on serve.py, localhost and staging. There is no @vercel/analytics import and no
third-party host. Vercel Web Analytics must also be enabled in the Vercel project (owner action)
for data to be collected; until then the loader simply fetches nothing. The ExternalRequestAllowlist
gate proves the marketing HTML/JS only references an approved host set and that the insights loader
is always behind the hostname guard.
Structured data (JSON-LD)
Emitted through frontend/src/components/JsonLd.astro, which serialises the object and escapes
< so the payload can never break out of its <script type="application/ld+json">.
- Landing (
/) carries three nodes (src/pages/index.astrofrontmatter): an Organization (“Algorythmos Pty Ltd”, logo/icons/icon-512.png, a customer-supportcontactPoint), a WebSite (“Gurbani Soul”) with aSearchActiontargeting/search?q={search_term_string}on the canonical host, and a SoftwareApplication (“Gurbani Soul”, iOS,ReferenceApplication,offers.price "0", publisher → the Organization by@id). There is deliberately noaggregateRating(we have no ratings).installUrl/urlare added only once the app is live (APP_STORE_LIVE). /supportcarries a FAQPage generated from the samefaqsarray that renders the visible<h4>question /<p>answer block, so the structured data can never drift from the page. TheJsonLdInvariantsgate parses every block, checks the landing’s three types (and the SearchAction host), and checks that every FAQPagenameis a visible<h4>.
Newsletter (launch notice) — env-gated
frontend/src/components/Newsletter.astro renders a quiet launch-notice sign-up only when the
build-time env var PUBLIC_NEWSLETTER_FORM_URL (the Buttondown embed endpoint) is set. It is unset
in CI, locally, and in the current build, so the section — and any Buttondown reference — is
absent from the built HTML. When set, it renders a single <input type="email" name="email" required>, a ghost-gold submit, an off-screen honeypot (hp_name), an honest one-line note (“One
short email when the app is on the App Store. No tracking, unsubscribe any time.”) and a
data-nl-status live region. Progressive enhancement lives in frontend/src/scripts/newsletter.ts
(wired by the idempotent landing.ts init(), view-transition safe): the honeypot short-circuits to
a no-op; otherwise it POSTs only the email field with mode:'no-cors' and shows an inline
confirmation, or an offline/error hint. With JS off, the native POST reaches Buttondown’s own
confirmation page. The URL is never a literal in src (NewsletterPrivacy gate); set it in the
Vercel Production + Preview envs — see runbook: newsletter.
Security headers
frontend/vercel.json adds, on top of the existing nosniff / X-Frame-Options: DENY /
Referrer-Policy:
Content-Security-Policy— enforcing as of PR4. Directive string:default-src 'self', inline script/style allowed,img-src 'self' data:,font-src 'self',connect-src/form-actionaddhttps://buttondown.com(the launch-notice sign-up),frame-ancestors 'none',base-uri 'self',object-src 'none'. The flip from-Report-Onlyto enforcing was gated on proof:frontend/e2e/csp.spec.tsloads every route inroutes.ts(marketing AND Knowledge Base,/learn/*included) under the enforced header — injected viapage.route— and asserts zerosecuritypolicyviolationevents on load, after toggling the theme, and after opening the mobile menu (46/46 green, desktop + mobile). The directive string is unchanged from the Report-Only version; only the header key changed. If a future page needs a new origin, add it to the directive, re-runcsp.spec.tsto zero, and only then ship.Strict-Transport-Security(2-year,includeSubDomains; preload),Permissions-Policy(camera=(), microphone=(), geolocation=(self), payment=()), andCross-Origin-Opener-Policy: same-origin.- Long-lived immutable
Cache-Controlfor/og/*,/icons/*,/_astro/*and/fonts/*.
Fonts & PWA
- Sant Lipi (SIL OFL 1.1) — the bundled Gurmukhi webfont,
unicode-range-scoped, shared with the Knowledge Base; declared inmarketing.css(rule copied fromstyles/kb/tokens.css). - Source Serif 4 (SIL OFL 1.1) — a Latin subset for headings (Brand.heading), regenerated by
frontend/scripts/subset-serif.shintofrontend/public/fonts/SourceSerif4-latin.woff2(≤ 130 KB; licence at/fonts/OFL-SourceSerif4.txt). Declared inmarketing.cssand wired into the heading stack in PR2 (this PR keeps the page pixel-identical). - PWA:
frontend/public/site.webmanifest(name/short_name, warm-ink theme/background,display: standalone) andfrontend/public/icons/(192, 512, maskable-512, apple-touch), regenerated deterministically byfrontend/scripts/gen-icons.mjs(a gold ੴ on warm ink).
The landing page (/)
- Layout:
frontend/src/layouts/Marketing.astro(renamed fromLanding.astroin the PR1 web-foundations pass) — its own shell, brand tokens as CSS variables infrontend/src/styles/marketing.css(mirrored, typed, infrontend/src/theme.ts, which theWebThemeMatchesTokensgate keeps equal todocs/brand/tokens.json), a light-first pre-paint theme script (no stored choice → light, whatever the OS; an explicit choice is shared with the Knowledge Base, which keeps itssystemdefault), the sharedMarketingFooter,<Seo>,<Analytics>, Sant Lipi + a serif heading stack, a skip link,lang="pa"on Gurmukhi, andprefers-reduced-motionrespected. - Page:
frontend/src/pages/index.astro— hero, the verse band, six feature cards, “Private by design”, a Knowledge Base card, and the App Store slot. - Brand rules are law (
docs/brand/gurbani-soul-brand-book.md): gold leads on warm paper/ink;#FFBC0Donly as a bordered fill in light mode (free in dark); gold text only viaaccentText(#8A6100); red never on the landing; Gurmukhi always ink-coloured Sant Lipi; never a gold mark on a red square.python3 scripts/brand/contrast_report.pymust exit 0 after any colour change. - Scripture is never typed by hand. The verse shown on
/is generated from the verified DB byscripts/gen_landing_verse.pyintofrontend/src/generated/landing-verse.json, and theLandingPagegate compares the renderedlang="pa"line byte-for-byte toSELECT gurmukhi FROM lines WHERE ang=1 ORDER BY id LIMIT 1. Citation is always “Sri Guru Granth Sahib Ji · Ang N”. - Counts are never typed by hand either. Every number the marketing pages quote (Angs, lines,
characters, raags, Vaars, pauris, saloks, themes, voices) comes from
frontend/src/generated/landing-facts.json, written by the same script from the DB andfrontend/public/contributors.json, and read throughfrontend/src/facts.ts. TheLandingPagegate checks the file against the pinned dataset and the roster. - Honest copy. The listing lint’s forbidden list applies here too: no “AI” except “never AI-generated”, no “beta”, nothing the app does not do.
Image policy
The v2 site (2026-09) is light-first with warm-ink bands and three kinds of imagery: the
owner’s original artwork (the hero), Unsplash photographs (photo bands, Learn covers), and
real app screenshots. Everything is self-hosted through astro:assets — the CSP is
img-src 'self' data:, so nothing is ever hot-linked from images.unsplash.com.
Unsplash photographs — licence facts
- Used under the Unsplash License: free for commercial and
non-commercial use, modification permitted, no permission needed, attribution appreciated but not
required — we credit anyway, on every marketing page and in
NOTICE.md. - The licence does not allow selling unaltered copies or compiling Unsplash photos into a competing service, and it carries no model or property releases — which is why faces and identifiable people are excluded below.
- Keep the photo’s Unsplash page URL in the PR description so the licence can be re-checked.
Respectful selection (owner approves every pick in the preview)
Briefs: Sri Harmandir Sahib at dawn / reflected at night · Gurdwara marble inlay detail · Nishan Sahib against the sky · Punjab wheat or mustard field · gold-leaf / brass texture · a single diya at dusk · an empty parikrama. Criteria:
- devotional light and architecture only; no saroop being handled, no identifiable faces, no crowds, no watermarks or text in frame;
- ≥ 2400 px wide originals, warm palette; treatment is a baked −10 % saturation + warm white balance only — on the page a warm-ink scrim, never a gold tint, grain or duotone;
- a photo never sits beside Gurmukhi (bands carry one serif statement ≤ 22 ch + one sub-line).
Where files live
- Photographs + artwork:
frontend/src/assets/landing/—unsplash-<handle>-<subject>.jpg(EXIF stripped, ≤ 2400 w). Every file here is counted by the credit gate. - Learn covers: mapped slug → import in
frontend/src/covers.ts(LEARN_COVERS), notsite.ts(which every layout imports and the credit gate parses). - App screenshots:
frontend/src/assets/app/<device>-<screen>[-variant]-<appearance>.png, recorded infrontend/src/assets/app/SHOTS.md(see Shot pipeline).
Credit shape
One IMAGE_CREDITS entry per file in frontend/src/site.ts (artwork entry first):
{ who: "Photographer Name", what: "Sri Harmandir Sahib reflected at dawn", url: "https://unsplash.com/@handle", file: "unsplash-handle-harmandir-dawn.jpg", source: "Unsplash" }urlis the photographer’s profile on the bare hosthttps://unsplash.com/@<handle>(theExternalRequestAllowlistgate allowsunsplash.com, neverimages.unsplash.com).- An entry without
urlis original artwork (“Artwork: …, made for Gurbani Soul.”). MarketingFooter.astrorenders the credits from this list on every marketing page: “Photographs: {who} / Unsplash, … (Unsplash License).” appears only when photos exist.- Add the same name + profile link to
NOTICE.md→ “Website imagery”.
The LandingPage.test_imagery_is_credited gate enforces all of it: exactly one credit per file in
assets/landing/, every who in the built index.html and in NOTICE.md, an “Artwork”
credit, and — once any photo has a url — “Unsplash” on the page and every url matching
https://unsplash.com/@<handle>. photo-bands.spec.ts checks each .photo-band img has alt text
and loads lazily, and that the footer names each linked photographer.
Rendering & budgets
- Components:
PhotoBand.astro(full-bleed<Picture>AVIF+WebP 640–1600 w,sizes="100vw", q55, lazy, warm-ink scrim,data-theme="dark");DeviceFrame.astro(kind="iphone"|"ipad",frame={false}for frame-less editorial shots,eagerfor the hero phone only). - Home: at most 2
loading="eager"images (hero artwork + hero phone) and exactly onefetchpriority="high"(the artwork, the LCP); any hero preload must reuse the rendered AVIF srcset. Every other page: nothing eager. Everything else lazy +decoding="async". - Budgets (gate-enforced): Home / Features / The watch / Privacy / Support HTML ≤ 60 KB each; each Learn page ≤ 48 KB; no served AVIF/WebP variant > 340 KB. Targets: hero ≤ 1920 w AVIF q55–60 ≤ 220 KB · photo bands ≤ 1600 w q55 ≤ 180 KB · iPhone frames WebP ≤ 80 KB · iPad AVIF+WebP ≤ 250 KB (drop to 1400 w rather than raise the gate).
Adding a photo
- Download the approved Unsplash original into
frontend/src/assets/landing/asunsplash-<handle>-<subject>.jpg; strip EXIF; resize to ≤ 2400 w. - Add its
IMAGE_CREDITSentry (who,what,url,file,source: "Unsplash") and the photographer toNOTICE.md. - Use it through
PhotoBand/<Picture>with an alt that names the place. cd frontend && npm run build:deploytwice (byte-identical), thenpython3 -m unittest discover -s webapp/testsandnpx playwright test photo-bands landing.
Shot pipeline (app screenshots)
Captured from the Simulator, never mocked; every file is recorded in SHOTS.md (raw SHA-256,
device, iOS version, commit, deep link, environment).
- Dedicated simulators (“SGGS iPhone 17 Pro”, “SGGS iPad Pro 13”), one booted at a time; a Debug
build with
-derivedDataPathin the scratchpad. - The public DB swapped into a throwaway copy of the built
.app— never intoios/Resources. xcrun simctl status_bar … override --time 9:41,xcrun simctl ui … appearance light|dark,SIMCTL_CHILD_SGGS_CLOCK_NOW=581 SGGS_CLOCK_MODE=fixed SGGS_CLOCK_NO_COORDS=1.- Deep links (
sggs://ang/1,ang/712on iPad landscape,search?q=…,nitnem,bani/japji,clock,theme/naam) — andsggs://shabad/<compId>for a deterministic Hukam, never the randomsggs://hukam. Widgets come from the snapshot unit tests (SGGS_WIDGET_SNAPSHOT_DIR). - Honest state only: a sparse reading-journey grid is fine; fabricated progress never is.
- Processing via
frontend/scripts/prep-shots.mjs(sharp, lanczos3, palette PNG, metadata stripped, idempotent): iPhone → 900 w, iPad landscape → 1600 w, widgets 2×, accent crops 900×420.
The App Store switch (launch day)
frontend/src/site.ts commits Apple’s real values — APP_STORE_ID = "6812982384" and
APP_STORE_URL = "https://apps.apple.com/app/id6812982384" — but whether the site presents the app
as available is a build-time switch: APP_STORE_LIVE is true only when the build env sets
PUBLIC_APP_STORE_LIVE=1. Every App Store render site keys on it:
| Off (default) | On (PUBLIC_APP_STORE_LIVE=1) |
|---|---|
hero chip data-app-store="coming-soon", download band coming-soon-foot, nav coming-soon-nav | “Download on the App Store” / “Get the app” links to the product page in the hero, the download band, the nav and the mobile menu |
no apple-itunes-app meta | the Smart App Banner on every page (Seo.astro) |
no installUrl in the SoftwareApplication JSON-LD | url + installUrl = the product page |
Why a switch and not a source edit: under the one-number policy the site must keep the version the
App Store build carries, so launch day cannot be a new release. The flip is a Vercel project env var
plus a redeploy of the same main commit — see docs/process/runbooks/deploy.md → “Launch-day
App Store switch”. Set it only after Apple’s product page answers 200.
Gates: AppStoreSwitch (the flag comes only from the env; no render site keys on the URL/ID),
test_cta_state_is_consistent (the built landing is wholly in one state) and the @smoke spec
(frontend/e2e/landing.spec.ts, same two states; SGGS_EXPECT_APP_STORE=live|soon pins the state
a deployment must serve). The CTAs are text links, not Apple’s badge artwork; if the badge is added
later, self-host the official SVG unmodified per Apple’s App Store marketing guidelines.
Adding a page
Create frontend/src/pages/<name>.astro using Base.astro (Knowledge Base) or Marketing.astro
(Gurbani Soul). Add its route to frontend/src/routes.ts (the sitemap and OG default derive from
it — the SitemapInvariants gate fails if the manifest and the built HTML routes disagree). If it
is a destination, add it to its place in PLACES in Base.astro (its path, and its entry in the place’s pages if it
should appear in the second row), and give it a state in frontend/e2e/kb-shell.spec.ts. Build + sync.
What protects the site
- Repo gates (
webapp/tests/test_repo_gates.py, run by the requiredpythoncheck):LandingPage(verse verbatim, honest copy, alt text, SEO head, page-weight budget),SubmissionUrlsAreLive, andDocsHygiene(this doc set is the only place the legacyvercel.appalias may be named); plus the PR1 web-foundations gatesSeoInvariants,SitemapInvariants,RssInvariants,ExternalRequestAllowlist,SmartBannerConsistency,NoSecretsInFrontend, andWebThemeMatchesTokens; plus the PR4 growth/polish gatesJsonLdInvariants(structured data on/and/support) andNewsletterPrivacy(no Buttondown literal insrc; no form/host in the env-less build).StaticRoutesintest_serve.pycovers serve.py’s static resolver + content types. @smokePlaywright (frontend/e2e/landing.spec.ts, run in the production deploy gate againsthttps://gurbanisoul.com): landing renders, verse present, exactly one App-Store slot, canonical host, axe serious/critical = 0.uptime.yml(every 15 minutes):/api/healthall-true,/(“Gurbani Soul”),/privacy(“Data Not Collected”) and/support(amailto:link) on the canonical host, plus the wiki.sggs-verify-prod: after each release, proves the running commit and the no-redirect guard.
Sri Guru Granth Sahib Ji — Knowledge Base wiki. Generated from commit c3d9360. Updated 26 October 2026. This copy may be out of date; the page is at https://docs.gurbanisoul.com/website/