How different tools handle pages: routing in plain HTML, PHP, React, Astro, and Next
Routing is how a website handles URLs. You type an address, something decides what you get back. Every tool in this post answers the same question, "someone asked for /bio, what do I give them?" The answer is always the same page. What changes is how each tool builds it.
Plain HTML: a folder of hand-written files
For example, a bio lives at /bio/anthony.html, a real file inside a bio folder. You wrote it by hand. The server finds it and sends it.
The URL is literally the folder path. site.com/bio/anthony.html is the file bio/anthony.html on the server's disk. This is file-based routing: the folder structure is the URL structure.
The problem shows up with more than one person. Five team members means five separate files in that folder: bio/anthony.html, bio/sarah.html, bio/mike.html, and so on. Each one is a full copy of the same layout with different words inside. Change the design, you edit five files. Add a person, you copy-paste a sixth. At fifty people this is unmanageable.
That copy-paste pain is exactly what PHP was built to kill: one template, one database, any number of bios.
PHP: one template, built on every visit
/bio is no longer a folder of duplicate hand-written files. It is a single script named bio.php, a file of code the server runs instead of just sending.
Someone visits site.com/bio.php?person=anthony. The ?person=anthony part is a query parameter: a variable passed in the URL. The script reads it, looks up that person's row in a database (or a JSON file, either works), fills in the variables in the template on the server, then sends the full finished page. Building the page on the server per request is server-side rendering (SSR). One script now serves all five people: ?person=anthony, ?person=sarah, ?person=mike.
A PHP file is HTML with holes in it:
<h1><?php echo $name; ?></h1>
<p><?php echo $job_title; ?></p>
Change a job title in the database, the page updates automatically. Add a person, add a database row. One template now serves any number of bios.
The cost: the server rebuilds the page for every single visitor, even when nothing changed. A thousand visits means building the same bio a thousand times.
The trade in plain terms: plain HTML is cheap to serve but expensive to maintain by hand. PHP flips it, expensive to serve but cheap to maintain. And PHP's real win is not just easier upkeep but capability: pages that change per visitor or per moment are impossible with static files at scale.
Dynamic content is the point; per-request server work is the accepted cost, managed with caching where possible.
React: routing moves into the browser
React broke the model every other tool in this post shares. A React single-page app has no folder of pages. The server sends the same nearly empty HTML file for every URL, plus the JavaScript bundle. Everything after that happens in the browser. This is client-side routing, and building the page in the browser is client-side rendering (CSR).
Routing is handled by a library, React Router, running client-side. You declare which component belongs to which path:
<Route path="/bio/:person" element={<BioPage />} />
The :person part is a dynamic segment: a variable inside the path itself, doing the same job as PHP's ?person=anthony query parameter.
The flow for our five bios, when you click a link to /bio/anthony:
- No request for a page goes to the server.
- React Router intercepts the click and updates the URL bar.
- React Router swaps in the BioPage component.
- The component reads anthony from the URL.
- The component fetches that person's data from an API (your Express backend in PERN) and fills itself in:
function BioPage() {
const { person } = useParams();
const [bio, setBio] = useState(null);
useEffect(() => {
fetch(`/api/people/${person}`).then(r => r.json()).then(setBio);
}, [person]);
if (!bio) return <p>Loading...</p>;
return <><h1>{bio.name}</h1><p>{bio.job_title}</p></>;
}
So the template-with-holes idea survived, but the filling moved: PHP fills holes on the server before sending; React sends empty holes and fills them in the browser after fetching.
The cost: the first visit downloads the whole app before anything shows, the visitor watches a loading state while data arrives, and search engines see an empty page. Those three problems are why the next two tools push the work back to the server.
Caching softens this for repeat visits: the browser stores the JavaScript bundle, and fetched data can be cached so returning visitors see content instantly. But first visits and search engines get no such help, since nothing is cached yet.
Astro: one template, built once at deploy
A bio lives at src/pages/bio/[person].astro, one template for all five people. The brackets are Astro's dynamic segment syntax: that part of the URL is a variable, so this single file handles /bio/anthony, /bio/sarah, /bio/mike. It is PHP's ?person=anthony idea moved into the path itself. Same shape as PHP too, a template with holes, but the logic sits in a fence at the top (Astro's frontmatter):
---
export async function getStaticPaths() {
const people = await getPeople();
return people.map(p => ({ params: { person: p.slug } }));
}
const person = await getPerson(Astro.params.person);
---
<h1>{person.name}</h1>
<p>{person.job_title}</p>
It also grabs info and drops it in, but only once, when you deploy the site. Building all pages at deploy is static site generation (SSG). The getStaticPaths part is Astro asking for the full list of people upfront, because it builds every bio page at deploy. Visitors just get finished files, like plain HTML. Change a job title, you redeploy.
Routing is file-based like plain HTML, and Astro combines the two older models: PHP's templates, plain HTML's finished files.
The cost: content is frozen at deploy. Pages that must differ per visitor need opt-in per-request rendering.
Next: you choose per page
A bio lives at app/bio/[person]/page.tsx, one React component in a folder. Same dynamic segment brackets as Astro: one file handles /bio/anthony, /bio/sarah, /bio/mike, any name. File-based routing like plain HTML and Astro.
The component grabs the person's info and fills in the variables, same as the others:
export default async function BioPage({ params }) {
const person = await getPerson(params.person);
return (
<>
<h1>{person.name}</h1>
<p>{person.job_title}</p>
</>
);
}
The new part: Next asks you, per page, when it should be built. All three earlier answers, all three build timings, are on the menu.
Choice Behaves like Use for the bios when Build once at deploy (SSG) Astro / plain HTML Bios rarely change Build per request (SSR) PHP Bios pull live data, like current status Build in the browser (CSR) React SPA The page is interactive, like an edit-your-own-bio form
Next's default is static: it builds pages at deploy unless the page uses something per-visitor, like cookies or live data, and then it switches that page to per-request automatically.
The cost: the choosing is the complexity. Plain HTML, PHP, and Astro each have one answer for when a page is built. Next has three, and knowing which one your page is using takes learning.
The through-line
/bio is Page built To update a bio Plain HTML A folder of hand-written files By hand Edit the file PHP A program On the spot, per request/visit Edit the database React A component, filled in the browser In the browser, after load Edit the API data Astro A template Once, at deploy Edit the data, redeploy Next A component, timing is your choice Your choice per page Depends on the timing chosen
The routing barely changed in thirty years: folder paths defined URLs in 1995, and folder paths define URLs in Astro and Next today. What changed is when and where the page gets built.
Terms to know
- File-based routing: the folder structure is the URL structure. Plain HTML, Astro, Next.
- Query parameter: a variable in the URL after ?, like ?person=anthony. PHP era, still used everywhere.
- Dynamic segment: a variable inside the path itself, like /bio/:person or [person]. React Router, Astro, Next.
- Client-side routing: a browser library intercepts clicks and swaps components instead of requesting pages. React SPA.
- Server-side rendering (SSR): the page is built on the server per request. PHP, opt-in for Astro and Next.
- Static site generation (SSG): all pages are built once at deploy. Astro's default, Next's default.
- Client-side rendering (CSR): the page is built in the browser after JavaScript loads. React SPA.
- Build timing: the umbrella question of when a page's HTML gets built: at deploy (SSG), per request (SSR), or in the browser (CSR).