Skip to content

Reference

The website — gurbanisoul.com

Updated

Page actions

ChatGPT and Claude are third-party services. The link gives them this page's public address and a short instruction. This site records nothing.

The public website gurbanisoul.com: DNS, SEO, Open Graph images, security headers, imagery policy, screenshots and its gates.

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, /learn and its articles, /privacy and /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 — uses Base.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

HostRoleServes
gurbanisoul.comcanonical apexthe site (200, no redirect)
www.gurbanisoul.comalias308 → apex
sggs-knowledge-base.vercel.applegacy Vercel alias308 → 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/www records 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.

Email

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"> and og:url built from SITE_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) gets id="kbThemeColor": the inline boot script in Base.astro sets it to the header’s colour for the stored theme before first paint, and scripts/core.ts keeps it equal to --chrome afterwards; the light-first marketing shell (themeColor="light") gets id="themeColorMeta", pinned to paper and rewritten by scripts/theme.ts on toggle.
  • apple-mobile-web-app-title, <link rel="manifest">, an apple-touch-icon, an SVG favicon, and — once the app is live (APP_STORE_LIVE) — the apple-itunes-app Smart 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.ts is the single manifest of public routes ({ path, lastmod, changefreq, priority, og }). It must list exactly the pages that build to HTML — the SitemapInvariants gate compares the sitemap’s <loc> set to the built HTML route set. lastmod is a fixed ISO date (never Date.now()) so the sitemap is byte-deterministic.
  • frontend/src/pages/sitemap.xml.ts emits /sitemap.xml over the manifest, each URL self-referencing its en + x-default hreflang alternates (no pa pages ship yet).
  • frontend/src/pages/rss.xml.ts emits /rss.xml (“Gurbani Soul — Learn”): every non-draft Learn article, newest first. RssInvariants checks it parses and links to the site.
  • The old frontend/public/sitemap.xml was 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.astro frontmatter): an Organization (“Algorythmos Pty Ltd”, logo /icons/icon-512.png, a customer-support contactPoint), a WebSite (“Gurbani Soul”) with a SearchAction targeting /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 no aggregateRating (we have no ratings). installUrl/url are added only once the app is live (APP_STORE_LIVE).
  • /support carries a FAQPage generated from the same faqs array that renders the visible <h4> question / <p> answer block, so the structured data can never drift from the page. The JsonLdInvariants gate parses every block, checks the landing’s three types (and the SearchAction host), and checks that every FAQPage name is 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-action add https://buttondown.com (the launch-notice sign-up), frame-ancestors 'none', base-uri 'self', object-src 'none'. The flip from -Report-Only to enforcing was gated on proof: frontend/e2e/csp.spec.ts loads every route in routes.ts (marketing AND Knowledge Base, /learn/* included) under the enforced header — injected via page.route — and asserts zero securitypolicyviolation events 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-run csp.spec.ts to zero, and only then ship.
  • Strict-Transport-Security (2-year, includeSubDomains; preload), Permissions-Policy (camera=(), microphone=(), geolocation=(self), payment=()), and Cross-Origin-Opener-Policy: same-origin.
  • Long-lived immutable Cache-Control for /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 in marketing.css (rule copied from styles/kb/tokens.css).
  • Source Serif 4 (SIL OFL 1.1) — a Latin subset for headings (Brand.heading), regenerated by frontend/scripts/subset-serif.sh into frontend/public/fonts/SourceSerif4-latin.woff2 (≤ 130 KB; licence at /fonts/OFL-SourceSerif4.txt). Declared in marketing.css and 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) and frontend/public/icons/ (192, 512, maskable-512, apple-touch), regenerated deterministically by frontend/scripts/gen-icons.mjs (a gold ੴ on warm ink).

The landing page (/)

  • Layout: frontend/src/layouts/Marketing.astro (renamed from Landing.astro in the PR1 web-foundations pass) — its own shell, brand tokens as CSS variables in frontend/src/styles/marketing.css (mirrored, typed, in frontend/src/theme.ts, which the WebThemeMatchesTokens gate keeps equal to docs/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 its system default), the shared MarketingFooter, <Seo>, <Analytics>, Sant Lipi + a serif heading stack, a skip link, lang="pa" on Gurmukhi, and prefers-reduced-motion respected.
  • 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; #FFBC0D only as a bordered fill in light mode (free in dark); gold text only via accentText (#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.py must exit 0 after any colour change.
  • Scripture is never typed by hand. The verse shown on / is generated from the verified DB by scripts/gen_landing_verse.py into frontend/src/generated/landing-verse.json, and the LandingPage gate compares the rendered lang="pa" line byte-for-byte to SELECT 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 and frontend/public/contributors.json, and read through frontend/src/facts.ts. The LandingPage gate 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), not site.ts (which every layout imports and the credit gate parses).
  • App screenshots: frontend/src/assets/app/<device>-<screen>[-variant]-<appearance>.png, recorded in frontend/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" }
  • url is the photographer’s profile on the bare host https://unsplash.com/@<handle> (the ExternalRequestAllowlist gate allows unsplash.com, never images.unsplash.com).
  • An entry without url is original artwork (“Artwork: …, made for Gurbani Soul.”).
  • MarketingFooter.astro renders 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, eager for the hero phone only).
  • Home: at most 2 loading="eager" images (hero artwork + hero phone) and exactly one fetchpriority="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

  1. Download the approved Unsplash original into frontend/src/assets/landing/ as unsplash-<handle>-<subject>.jpg; strip EXIF; resize to ≤ 2400 w.
  2. Add its IMAGE_CREDITS entry (who, what, url, file, source: "Unsplash") and the photographer to NOTICE.md.
  3. Use it through PhotoBand / <Picture> with an alt that names the place.
  4. cd frontend && npm run build:deploy twice (byte-identical), then python3 -m unittest discover -s webapp/tests and npx 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).

  1. Dedicated simulators (“SGGS iPhone 17 Pro”, “SGGS iPad Pro 13”), one booted at a time; a Debug build with -derivedDataPath in the scratchpad.
  2. The public DB swapped into a throwaway copy of the built .app — never into ios/Resources.
  3. 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.
  4. Deep links (sggs://ang/1, ang/712 on iPad landscape, search?q=…, nitnem, bani/japji, clock, theme/naam) — and sggs://shabad/<compId> for a deterministic Hukam, never the random sggs://hukam. Widgets come from the snapshot unit tests (SGGS_WIDGET_SNAPSHOT_DIR).
  5. Honest state only: a sparse reading-journey grid is fine; fabricated progress never is.
  6. 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 metathe Smart App Banner on every page (Seo.astro)
no installUrl in the SoftwareApplication JSON-LDurl + 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 required python check): LandingPage (verse verbatim, honest copy, alt text, SEO head, page-weight budget), SubmissionUrlsAreLive, and DocsHygiene (this doc set is the only place the legacy vercel.app alias may be named); plus the PR1 web-foundations gates SeoInvariants, SitemapInvariants, RssInvariants, ExternalRequestAllowlist, SmartBannerConsistency, NoSecretsInFrontend, and WebThemeMatchesTokens; plus the PR4 growth/polish gates JsonLdInvariants (structured data on / and /support) and NewsletterPrivacy (no Buttondown literal in src; no form/host in the env-less build). StaticRoutes in test_serve.py covers serve.py’s static resolver + content types.
  • @smoke Playwright (frontend/e2e/landing.spec.ts, run in the production deploy gate against https://gurbanisoul.com): landing renders, verse present, exactly one App-Store slot, canonical host, axe serious/critical = 0.
  • uptime.yml (every 15 minutes): /api/health all-true, / (“Gurbani Soul”), /privacy (“Data Not Collected”) and /support (a mailto: link) on the canonical host, plus the wiki.
  • sggs-verify-prod: after each release, proves the running commit and the no-redirect guard.