← Home

यंग फाउंडेशन

The Yung Foundation website, end to end

Every page a visitor can reach, everything staff can edit, and the architecture underneath — on one page.

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.

yungfoundation.org Core Content Take action Legal /  — home /about /satpuda /programs /programs/ :slug /blog /blog/ :slug /gallery /events /team /resources /donate /contact /volunteer the last two accept a form /legal/privacy /legal/terms /legal/accessibility NOT LINKED FROM THE MENU /404 /robots.txt /sitemap.xml /admin  — staff only, behind a login one page per row in the database
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.

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.

STORED IN THE DATABASE RENDERED ON AN ENGLISH PAGE { mr: "आमची गोष्ट",   en: "Our story" } Our story tagged as English, set in the Latin font { mr: "आमची गोष्ट" } no English yet आमची गोष्ट (मराठी) tagged as Marathi — screen readers and the font follow the real text { } field not filled in at all nothing — the heading and its whitespace are gone too an empty state, not an empty box { mr: "आमची गोष्ट",   en: "" } a blank line, with the heading still above it an empty string is a present value, so the fallback never fires This is why the admin never saves an emptied field — it removes the key instead.
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.

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.

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.

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.

CLOUDFLARE WORKER SUPABASE Browser a phone, usually GET Middleware 1 · pick the language 2 · check the admin gate before any HTML exists public Astro page renders on the server no valid session 303 → /admin/login anon key Postgres the content Storage the files Row-level security unpublished rows never leave the database finished HTML — 0 bytes of JavaScript The gate runs before any markup is produced, and it fails closed: if the check itself errors, the answer is the login page, not a server error. It also revalidates the session against the auth server rather than trusting the cookie it was 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.

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.

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.

HOW AN IMAGE URL IS PRODUCED programs .cover_media_id FK media .storage_path publicUrl(   'media', path) a real URL the only way one exists THE TWO BUCKETS what publicUrl() will accept media public — photographs, covers, the donation QR files private — no public read policy of any kind outside the helper's accepted types, so linking one from a public page does not compile THE COLUMN THIS SCHEMA DOES NOT HAVE programs.cover_path A path can be typed wrongly, can point at a file nobody uploaded, and gives no way to ask which pages use a photograph before deleting it.
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.
A VISITOR READING A PAGE Browser not signed in Worker renders the page select AN EDITOR SAVING A CHANGE Browser signed in at /admin insert / update, under their own session no API layer in between — and therefore none to bypass Row-level security the same policy on both lanes Postgres + Storage Dashed means a signed-in session. The visitor's lane is anonymous throughout — the anonymous key is public by design and grants nothing on its own.
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.