Every page a visitor can reach, everything staff can edit, and the architecture underneath
— on one page.
As built at v0.5.0 · 7 August 2026
0 bytesof JavaScript downloaded on the public sitemeasured on all 15 routes
15public pages, plus sitemap and robots10 in the main menu
2languages — Marathi is the defaultEnglish is the alternative
18database tables, each with its own access policyenforced in the database
44pxminimum size for anything a finger has to hitthe audience is on phones
Tier one
The site
What is on it, what it does, and who can change it. No technical background assumed.
Site map साइटचा नकाशा
Fifteen public pages in four groups, plus the three files search engines read.
Figure 1 — every URL a visitor can reach. The two gold, dashed routes are not
files; one page is generated for each published programme and each blog post, so adding
a programme in the admin adds a page to the site. /sitemap.xml lists 50
URLs — 25 pages in two languages each — and no admin path.
What each page does प्रत्येक पान
One line each, in the order they appear in the menu.
/Home. Hero with the Marathi tagline as the headline, the impact figures, the mission
statement, and the first six programmes as cards.
/aboutThe foundation's story in three parts, the mission, a list of key facts, and the
values.
/programsEvery published programme as a card with its cover photograph.
/programs/:slugOne programme in full: the write-up, the region, the focus area, the year it started,
and a donate prompt.
/satpuda"Come to Satpuda" — the three steps to arranging a visit, what to expect, and
testimonials from people who have been.
/eventsUpcoming, ongoing and past events. Which is which is worked out on every visit from
the current Indian date, so nothing is ever stale.
/galleryPhotographs in a masonry grid, filterable by category, with a tap-to-enlarge
viewer.
/blogPosts with a cover, a category, an excerpt, the author and the date.
/blog/:slugOne post in full, with prompts to donate or volunteer at the end.
/teamThree groups — advisory, core and fellows — each with its own empty state, and a
drawn silhouette where there is no photograph.
/resourcesDocuments to download. A resource with no file attached shows as pending rather than
as a broken link.
/donateSuggested amounts, how to give, the UPI QR code, bank details, and the registration
numbers donors need for tax. Each block appears only when its own details are
filled in.
/contactEmail, phone numbers and office addresses, plus a message form that also emails the
office.
/volunteerThe roles available, other ways to help, and a sign-up form asking for a name and at
least one way to reply.
/legal/…Privacy, terms and an accessibility statement, as three tabs.
A page never shows a blank where a fact is missing. Every block on
/donate, /about and /satpuda is tied to its own
data and disappears when that data is absent. An empty "80G —" line would read as a
claim the foundation cannot back, and donors rely on those numbers, so the whole line is
suppressed instead.
Two languages दोन भाषा
Marathi is the site's own language. English is the translation — not the other way round.
Switching is a link
The toggle in the header is an ordinary link to ?lang=en. It works with
JavaScript off, the choice is remembered for a year, and both language versions have
their own shareable, searchable address.
Everything is translatable
Every piece of text on the site — page copy, menu labels, dates, button words — exists
in both languages. Dates are formatted by hand rather than by the browser, so the
numbers stay legible in both.
Half-translated is a supported state
A programme can be written in Marathi and published before anyone has translated it.
An English reader sees the Marathi with a quiet (मराठी) marker
rather than a gap.
Figure 2 — one field, four outcomes. The first three are the design working. The
fourth is the failure it is built to avoid, and it is the reason the admin deletes a
translation rather than blanking it: a field containing an empty string looks filled in
to every check, so the Marathi never gets a chance to show.
What staff can edit संपादन
Sixteen sections in the admin, behind a login at /admin. Between them they
cover every word and every photograph on the public site — there is nothing an editor has
to ask a developer to change.
Single-record forms 3
One long form each, for settings that exist exactly once.
Site settings — titles, tagline, hero copy and its A/B variant, logo, contact email, phone numbers, offices, social links
About page — the story paragraphs, mission, and the story photograph
Donation info — UPI ID, QR code, bank details, 80G / 12A / PAN, the domestic-only notice
Ordered lists 9
Add, edit, reorder, publish or unpublish rows. Order on the page is the order here.
Key facts · Values · Impact figures
Programmes · Blog posts · Events
Team members · Testimonials · Resources
Gallery 1
Photograph, category and caption per tile. A tile is public only when both the tile
and the photograph itself are published.
Media library 1
Every image, video and document in one place: search, filter, bulk publish, alt text
and captions in both languages, and a "used in" line showing every page a file appears
on before you delete it.
Inboxes 2
Contact messages and volunteer sign-ups, read-only. The public can add to these and
nobody outside the login can read them back.
Both languages, side by side
Every translatable field shows a Marathi box and an English box together. There is no
separate "translate this page" mode to remember to visit.
A draft English translation on request
A button next to an English box drafts it from the Marathi. It fills the box for an
editor to correct — it never saves on its own.
Nothing is public until it is published
New rows start unpublished. A photograph is resized and re-encoded in the browser
before it uploads, so a 4 MB phone picture does not become a 4 MB download for every
visitor.
Built for phones on slow networks फोनसाठी
The audience is in the Satpuda hills — Dhadgaon and Shirpur, in Nandurbar and Dhule
districts. Most of them arrive on a phone, on a patchy connection.
No JavaScriptThe public pages download none. They arrive as finished HTML, so they render on a
slow connection and on an old phone, and they work if a script fails.
Split fontsThe Marathi typeface is 121 KB. English pages do not download it — including the two
Devanagari characters in the logo and the language toggle, which get their own
2.5 KB file for exactly that reason.
Capped imagesEvery uploaded photograph is resized to 1600px and squeezed under 250 KB before it is
stored, because how much a photo costs to serve is fixed at upload time, not at
display time.
44px targetsNothing tappable is smaller than that at any screen width. A layout that does not fit
gets changed; the button does not get smaller.
AccessibilityWCAG 2.1 AA is the target. One main heading per page, text tagged with the language
it is actually in, and an accessibility statement published at
/legal/accessibility.
Tier two
How it is built
The stack, the data model, and the three decisions that shape everything else. Written for
whoever maintains this next.
The stack तंत्रज्ञान
Read out of package.json and astro.config.mjs.
Astro 7server-rendered, no static build
React 19admin only, never on a public page
SupabasePostgres + object storage
Cloudflare Workersthe deploy runtime
TypeScriptstrict, with null checks
Server-rendered rather than statically built, on purpose. Every word of
page furniture — the menu labels, the site title, the footer — comes out of the CMS, so
a static build would have to be regenerated every time an editor changed a comma. The
cost is that every request hits the database, which is a known and recorded trade.
The development server and the production runtime are different engines.npm run dev runs on Node; production runs on Cloudflare's workerd. Code
that works on one can fail on the other, so any change to the server side is verified
with npm run build && npm run preview, which runs the real runtime
locally.
What happens on a request विनंतीचा प्रवास
One page view, from the tap to the HTML. Everything below happens inside a single
Cloudflare Worker before any markup is sent.
Figure 3 — one request, start to finish. The page never filters for "published"
itself. It asks for programmes and the database returns only the published ones, because
the policy says so. A redundant filter in the query would hide a broken policy — the app
would look correct while the database leaked.
The data model माहितीची रचना
Eighteen tables, grouped by what they are for.
Settings, exactly one row each 3
site_settings — titles, hero, contacts, socials
about_page — story and mission copy
donation_info — UPI, bank, registrations
Constrained to a single row in the schema, so "there is only one" is a fact about the
database rather than a convention.
Content lists 11
programs · blog_posts · events
gallery_items · team_members · testimonials
resources · key_facts · org_values
impact_stats · profiles
Media 1
media — one row per image, video or document, with its description in
both languages. Everything else points at it.
Inboxes 3
contact_messages
volunteer_submissions
event_registrations
The public can insert and cannot read — not even the row they just wrote. Consent is a
column the database refuses to accept as false.
Two rules the schema enforces rather than documents. Every content table
carries its own sort_order and an is_published flag that
defaults to off, so nothing is ever accidentally live. And no table has a path
column — there is no cover_path anywhere, because a path is something
you can type wrongly and a foreign key is not.
Derived facts are not stored. An event has a start date and an end date
and no status column; whether it is upcoming, ongoing or past is worked out on every
render from the Indian date. A stored status is a fact that goes wrong quietly at
midnight.
Who can read what प्रवेश नियंत्रण
Permission is enforced by the database on every statement, not by the code that asks.
SettingsReadable by anyone. There is nothing private in them.
ContentReadable by anyone if published. A draft is invisible to the public site and
404s by direct URL.
InboxesAnyone may add a row. Nobody outside the login may read one — there is no read policy
at all, which is stronger than a policy that returns nothing.
StaffA signed-in editor may read and write everything, checked against a role stored in
the database rather than a claim in the token.
Two exceptionsA gallery tile is public only if the photograph it points at is also published. And
staff accounts are self-read, admin-managed.
The all-powerful database key is used nowhere. It is declared to the
build as server-only and secret, so importing it into anything the browser sees is a
build error rather than a review comment. On top of that, continuous integration
searches the built output for a leaked key on every push, in both the old and current key
formats, and fails the build if it finds one.
Two decisions worth a diagram दोन निर्णय
The parts of this codebase that are least like the obvious version, and why.
Figure 4 — media is referenced by ID, never by path. Because every reference is a
foreign key, the admin can tell an editor exactly which pages use a photograph before
they delete it, correcting one description corrects every page it appears on, and the
same file used in four places is one upload and one row. The private bucket sits outside
the URL helper's accepted types on purpose: publishing a private document is a
compile error rather than something a reviewer has to catch.
Figure 5 — there is no endpoint between an editor and the database. The admin does
not post to an API that then talks to Postgres; the editor's browser writes directly,
carrying their own session, and the same policy that hides a draft from a visitor is what
decides whether the write is allowed. One check instead of two, and no second place for
the two to disagree.
JavaScript is spent where it earns its place. The public site is HTML —
the mobile menu is a native disclosure widget, the gallery viewer is a CSS selector
reacting to the address bar, the language toggle is a link. All three are keyboard- and
screen-reader-correct for free, and all three work with scripting disabled. The one
exception is the animated impact figures on the home page: 468 bytes written inline,
because the framework version of the same effect costs 184 KB.
The admin pays what the public site does not. It costs about 447 KB of
JavaScript on first load — a deliberate trade, since it sits behind a login, is used by a
handful of staff on desktops, and caches after the first visit. It is measured from the
build output on every change rather than estimated.