Most frontend frameworks start with JavaScript.
Astro starts with HTML.
That one decision changes everything.
In React, Vue, or Svelte apps, the browser often receives a JavaScript application and then uses that JavaScript to build or hydrate the page. Astro flips the default. It renders your site to HTML first, ships almost no JavaScript by default, and only adds client-side JavaScript where you explicitly ask for interactivity.
That is why Astro is popular for blogs, portfolios, documentation sites, landing pages, marketing pages, content hubs, and e-commerce pages where speed and SEO matter.
It is not “React but different.” It is a content-first web framework with a very specific performance model.
Let’s break it down.
What Is Astro?
Astro is a web framework for content-driven websites.
It gives you routing, layouts, components, Markdown support, content collections, image handling, integrations, server rendering, API endpoints, middleware, and more. But its core idea is simple:
Render HTML on the server. Send less JavaScript to the browser.
Traditional SPA mental model
┌──────────────┐ ┌─────────────────────┐ ┌───────────────┐
│ Browser gets │ ──▶ │ Downloads JS bundle │ ──▶ │ JS builds UI │
└──────────────┘ └─────────────────────┘ └───────────────┘
Astro mental model
┌──────────────┐ ┌─────────────────────┐ ┌───────────────┐
│ Server/build │ ──▶ │ Browser gets HTML │ ──▶ │ JS only where │
│ renders HTML │ │ already rendered │ │ needed │
└──────────────┘ └─────────────────────┘ └───────────────┘
This is the phrase you need for interviews:
Astro is server-first and zero-JavaScript by default.
Not zero JavaScript forever. Not no interactivity. Just zero JavaScript unless you opt in.
That is the whole deal.
Why Astro Exists
Modern frontend development got powerful. It also got heavy.
For a dashboard or Figma-like app, a big client-side JavaScript application makes sense. The user is doing complex interactive work. State changes constantly. The page behaves more like software than a document.
But what about a blog post?
What about a docs page?
What about a product landing page with one newsletter form and one carousel?
Shipping an entire SPA for mostly static content is overkill. The browser downloads JavaScript, parses it, executes it, hydrates components, attaches event listeners, and finally becomes interactive.
Astro asks a better question:
Which parts of this page actually need JavaScript?
Usually, the answer is: not much.
So Astro renders the static parts as HTML and lets you hydrate only the interactive pieces.
Astro vs SPA Frameworks
Astro is a Multi-Page App framework by default. Traditional React apps are often Single-Page Apps.
Here is the difference:
SPA
┌──────────────┐
│ One HTML app │
└──────┬───────┘
▼
┌──────────────────────────────┐
│ JavaScript handles routing, │
│ rendering, data, state, UI │
└──────────────────────────────┘
Astro MPA
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ /about HTML │ │ /blog HTML │ │ /shop HTML │
└──────┬──────┘ └──────┬──────┘ └──────┬──────┘
▼ ▼ ▼
only needed JS only needed JS only needed JS
In an SPA, JavaScript owns the application shell.
In Astro, the page is already useful as HTML.
That improves:
- First load performance because there is less JavaScript to download and run
- SEO because crawlers get real HTML
- Accessibility because HTML is the foundation
- Resilience because the page is not useless while JavaScript is loading
Interview answer:
Astro is best when the page is mostly content with islands of interactivity. A full SPA is better when the whole screen is constantly interactive application state.
The .astro Component File
Astro components use the .astro extension.
They look like HTML with a server-side script section at the top.
---
const name = 'Dhaivick';
const skills = ['JavaScript', 'React', 'Astro'];
---
<section>
<h1>Hello, {name}</h1>
<ul>
{skills.map((skill) => <li>{skill}</li>)}
</ul>
</section>
The top part between --- fences is the component script. It runs on the server at build time or request time.
The bottom part is the component template. It describes the HTML output.
┌──────────────────────────────┐
│ --- │
│ server-side JavaScript │
│ imports, data fetching, vars │
│ --- │
├──────────────────────────────┤
│ HTML template │
│ expressions with { } │
│ components and slots │
└──────────────────────────────┘
Important point:
JavaScript in Astro frontmatter does not automatically ship to the browser.
It runs before the HTML is sent. That means you can fetch data, read files, import content, build arrays, and prepare props without adding client-side JavaScript.
This is one of Astro’s biggest differences from React components.
In React, component code usually becomes part of the client bundle.
In Astro, component script code is server-side by default.
Props And Components
Astro components can accept props through Astro.props.
---
const { title, description } = Astro.props;
---
<article>
<h2>{title}</h2>
<p>{description}</p>
</article>
Then use it like this:
---
import Card from '../components/Card.astro';
---
<Card
title="Astro is fast"
description="Because it ships less JavaScript by default."
/>
This should feel familiar if you know component-based UI. But the runtime model is different.
Astro components are not reactive in the browser. You cannot use useState inside an .astro component. You cannot attach onClick the React way. You cannot expect the component to re-render on the client.
Why?
Because an Astro component is rendered to HTML and then it is done.
If you need browser interactivity, you use a client island.
Slots
Slots let a parent component pass HTML content into a child component.
---
// src/layouts/BaseLayout.astro
const { title } = Astro.props;
---
<html lang="en">
<head>
<title>{title}</title>
</head>
<body>
<slot />
</body>
</html>
Use it:
---
import BaseLayout from '../layouts/BaseLayout.astro';
---
<BaseLayout title="Home">
<main>
<h1>Welcome</h1>
<p>This content is injected into the slot.</p>
</main>
</BaseLayout>
The <slot /> is the placeholder.
Named slots work when a layout has multiple insertion points:
---
// ArticleLayout.astro
---
<article>
<header>
<slot name="header" />
</header>
<main>
<slot />
</main>
</article>
<ArticleLayout>
<h1 slot="header">Astro Islands</h1>
<p>Main article content goes here.</p>
</ArticleLayout>
Interview translation:
Props pass data. Slots pass markup.
File-Based Routing
Astro uses file-based routing.
Files inside src/pages become routes.
src/pages/
├── index.astro → /
├── about.astro → /about
├── blog/
│ ├── index.astro → /blog
│ └── [slug].astro → /blog/:slug
└── api/
└── contact.ts → /api/contact
No route config. No router object. No giant route array.
Create a page file, get a route.
Dynamic routes use bracket syntax:
---
// src/pages/blog/[slug].astro
export async function getStaticPaths() {
const posts = [
{ slug: 'astro-intro', title: 'Astro Intro' },
{ slug: 'islands', title: 'Islands Architecture' },
];
return posts.map((post) => ({
params: { slug: post.slug },
props: { post },
}));
}
const { post } = Astro.props;
---
<h1>{post.title}</h1>
For static builds, getStaticPaths() tells Astro which dynamic pages to generate.
If your route is /blog/[slug].astro, Astro needs to know every slug at build time. getStaticPaths() provides that list.
Static Site Generation
By default, Astro pre-renders pages to static HTML at build time.
That means this:
source files + data
│
▼
┌──────────────────┐
│ astro build │
└────────┬─────────┘
▼
┌──────────────────┐
│ static HTML/CSS │
│ minimal JS │
└────────┬─────────┘
▼
CDN/browser
Static generation is excellent for content that does not change per request:
- Blog posts
- Portfolios
- Documentation
- Marketing pages
- Product pages updated by rebuilds
- Public content from Markdown or a CMS
Static HTML is cheap to host, easy to cache, and fast to deliver.
This is why Astro feels so good for content-heavy sites.
SSR And On-Demand Rendering
Static generation is the default. But Astro can also render pages on demand.
That means the page is generated when a request comes in.
Use this when the page depends on request-time data:
- Cookies
- Logged-in user information
- Fresh database reads
- Personalized content
- Request headers
- Data that must not wait for a rebuild
---
export const prerender = false;
const user = await getUserFromSession(Astro.cookies);
---
<h1>Welcome back, {user.name}</h1>
In static mode, most pages are pre-rendered. You opt a specific page out with:
export const prerender = false;
In server mode, the opposite is possible: render everything on demand by default, then opt specific pages into static output.
Interview answer:
SSG generates HTML at build time. SSR/on-demand rendering generates HTML at request time. Astro supports both, and the best Astro apps often mix them.
Islands Architecture
This is the Astro concept everyone asks about.
An island is an interactive component inside an otherwise static HTML page.
The page is the ocean. The interactive widgets are islands.
┌──────────────────────────────────────────────┐
│ Static HTML page │
│ │
│ Header │
│ Article content │
│ │
│ ┌──────────────────────┐ │
│ │ Interactive Search │ ← island │
│ └──────────────────────┘ │
│ │
│ Static footer │
└──────────────────────────────────────────────┘
Only the island gets hydrated.
Not the whole page.
That is the difference.
In a typical SPA, hydration happens across the app tree. Astro uses partial hydration or selective hydration. It lets you decide which components need JavaScript and when that JavaScript should load.
That means you can write a mostly static article page with a React search box, a Svelte pricing calculator, or a Vue cart button.
Astro does not care which UI framework the island uses.
Client Directives
Astro will not hydrate a framework component unless you tell it to.
This is intentional.
---
import Counter from '../components/Counter.jsx';
---
<Counter />
This renders the component’s initial HTML, but it is not interactive in the browser.
To make it interactive, add a client directive:
<Counter client:load />
Common directives:
client:load hydrate immediately when the page loads
client:idle hydrate when the browser is idle
client:visible hydrate when the component enters the viewport
client:media hydrate when a media query matches
client:only render only on the client, skip server HTML
Example:
---
import HeroCarousel from '../components/HeroCarousel.jsx';
import NewsletterForm from '../components/NewsletterForm.jsx';
import MobileMenu from '../components/MobileMenu.jsx';
---
<HeroCarousel client:visible />
<NewsletterForm client:idle />
<MobileMenu client:media="(max-width: 768px)" />
This is not just syntax. This is performance control.
You are telling the browser:
- Load the carousel only when someone scrolls to it
- Hydrate the newsletter form when the browser has spare time
- Send the mobile menu only when the screen is mobile-sized
Interview answer:
Client directives control when a client island hydrates. They are how Astro turns interactivity into an explicit opt-in.
Astro Can Use React, Vue, Svelte, Solid, And More
Astro is UI-framework agnostic.
You can use Astro components for static structure and bring in another framework for interactive pieces.
---
import ProductGallery from '../components/ProductGallery.jsx';
import PriceCalculator from '../components/PriceCalculator.svelte';
---
<main>
<h1>Product Page</h1>
<ProductGallery client:visible />
<PriceCalculator client:load />
</main>
This is powerful because you are not forced to choose one framework for the whole site.
But do not abuse it.
If every section becomes a hydrated React component, you have rebuilt an SPA inside Astro and lost the main benefit.
Astro works best when the default stays HTML and islands are used deliberately.
Sharing State Between Islands
Here is an important interview trap.
Astro islands are isolated. Each island hydrates independently.
That means two separate React islands do not automatically share React context just because they are on the same page.
┌───────────────────────────────┐
│ Astro page │
│ │
│ ┌─────────────┐ ┌───────────┐ │
│ │ React Cart │ │ React Nav │ │
│ │ island │ │ island │ │
│ └─────────────┘ └───────────┘ │
│ separate hydration roots │
└───────────────────────────────┘
If islands need shared state, common options are:
- Put them inside one larger island
- Use a shared client-side store
- Use browser events
- Use URL/search params
- Use cookies or server state
The best choice depends on the type of state.
For a cart dropdown and cart summary that must update together instantly, one shared island or a small client store may make sense.
For filters that should be shareable, URL search params are better.
For authenticated user data, server state is usually the source of truth.
Styling In Astro
Styles inside an Astro component are scoped by default.
<section class="card">
<h2>Fast by default</h2>
<p>Only this component gets these styles.</p>
</section>
<style>
.card {
border: 1px solid #ddd;
padding: 1rem;
}
</style>
That .card rule applies to this component’s rendered markup, not every .card on the site.
Need global styles?
Use a global CSS file or mark a style as global:
<style is:global>
body {
font-family: system-ui, sans-serif;
}
</style>
Astro also supports CSS imports, Sass through integrations, Tailwind, CSS modules, and normal browser CSS.
The important idea:
Astro does not require a CSS-in-JS runtime.
Again, less client-side JavaScript.
Scripts And Browser JavaScript
What if you want a tiny bit of browser behavior, but not a full React component?
Use a normal <script> tag.
<button id="theme-toggle">Toggle theme</button>
<script>
const button = document.querySelector('#theme-toggle');
button?.addEventListener('click', () => {
document.documentElement.classList.toggle('dark');
});
</script>
This is just browser JavaScript.
No island required.
Use this for small behavior:
- Theme toggles
- Copy buttons
- Simple menus
- DOM enhancements
- Analytics events
Use framework islands when the interaction has real component state, complex UI updates, or benefits from React/Vue/Svelte.
Interview answer:
Astro gives you three levels: static HTML, small vanilla scripts, and hydrated framework islands. Pick the smallest tool that fits.
Markdown And MDX
Astro is excellent for content because Markdown is a first-class citizen.
You can create Markdown pages directly:
---
title: "My First Post"
description: "A simple Astro blog post."
---
# My First Post
This becomes a page.
Markdown is perfect for articles, docs, changelogs, and content that should be easy to write.
MDX goes further. It lets you use components inside Markdown.
---
title: "Pricing Notes"
---
import PricingTable from '../components/PricingTable.astro';
# Pricing
<PricingTable />
Use Markdown for plain content.
Use MDX when content needs components.
Do not turn every post into component soup. Keep content readable.
Content Collections
Once a site grows, random Markdown files are not enough.
You need structure.
A blog post should have a title, description, publish date, maybe tags, maybe a draft flag. A product should have a price, category, image, and stock status.
Astro content collections solve this.
They let you define a schema for content.
// src/content/config.ts
import { defineCollection, z } from 'astro:content';
const blog = defineCollection({
type: 'content',
schema: z.object({
title: z.string(),
description: z.string(),
pubDate: z.coerce.date(),
tags: z.array(z.string()).optional(),
draft: z.boolean().optional().default(false),
}),
});
export const collections = { blog };
Now every blog entry must match that shape.
If a post is missing title, Astro can catch it. If pubDate is invalid, Astro can catch it. Your content becomes typed data, not a pile of loose files.
Query entries:
---
import { getCollection } from 'astro:content';
const posts = await getCollection('blog', ({ data }) => !data.draft);
---
<ul>
{posts.map((post) => (
<li>
<a href={`/blog/${post.slug}`}>{post.data.title}</a>
</li>
))}
</ul>
Interview answer:
Content collections give Markdown and structured content schema validation, type safety, and query APIs. They are one of Astro’s biggest advantages for content-heavy sites.
Data Fetching
Astro components can use await directly in frontmatter.
---
const response = await fetch('https://api.example.com/products');
const products = await response.json();
---
<ul>
{products.map((product) => (
<li>{product.name}</li>
))}
</ul>
Where does this run?
Depends on the rendering mode.
In a static page, it runs at build time.
In an on-demand rendered page, it runs on every request.
Static page
fetch runs during build ──▶ HTML generated once ──▶ CDN serves it
SSR page
request arrives ─────────▶ fetch runs now ───────▶ HTML returned
That distinction matters.
If product data changes once a day, build-time fetch may be fine.
If user data changes per request, you need on-demand rendering.
API Endpoints
Astro can create API endpoints using files in src/pages.
// src/pages/api/health.ts
export function GET() {
return new Response(
JSON.stringify({ ok: true }),
{
headers: {
'Content-Type': 'application/json',
},
}
);
}
That becomes:
/api/health
Endpoints can handle methods like GET, POST, PUT, and DELETE.
// src/pages/api/contact.ts
export async function POST({ request }) {
const body = await request.json();
return new Response(
JSON.stringify({ received: body.email }),
{ status: 200 }
);
}
Use endpoints when you need a route that returns data instead of HTML:
- Form submissions
- JSON APIs
- Webhooks
- RSS feeds
- Server-side utility routes
Actions
Astro Actions are a newer server-side pattern for calling backend functions with type-safe inputs.
Instead of manually writing a fetch('/api/...') call, parsing JSON, validating input, and standardizing errors, an action gives you a typed server function.
// src/actions/index.ts
import { defineAction } from 'astro:actions';
import { z } from 'astro/zod';
export const server = {
subscribe: defineAction({
input: z.object({
email: z.string().email(),
}),
handler: async ({ email }) => {
await saveSubscriber(email);
return { ok: true };
},
}),
};
Call it from client-side code:
<form id="newsletter">
<input name="email" type="email" required />
<button>Subscribe</button>
</form>
<script>
import { actions } from 'astro:actions';
const form = document.querySelector('#newsletter');
form?.addEventListener('submit', async (event) => {
event.preventDefault();
const formData = new FormData(form);
const email = String(formData.get('email'));
const { data, error } = await actions.subscribe({ email });
if (error) {
console.error(error);
return;
}
console.log(data);
});
</script>
When would you use actions instead of endpoints?
Use actions when your own frontend is calling your own backend logic and you want type-safe input validation.
Use endpoints when you need a public HTTP interface, webhook, external integration, or a custom response shape.
Middleware
Middleware runs before a route is rendered.
Use it for request-level logic:
- Authentication checks
- Redirects
- Logging
- Headers
- Locale detection
- Protecting private routes
// src/middleware.ts
import { defineMiddleware } from 'astro:middleware';
export const onRequest = defineMiddleware(async (context, next) => {
const isAdminRoute = context.url.pathname.startsWith('/admin');
const session = context.cookies.get('session');
if (isAdminRoute && !session) {
return context.redirect('/login');
}
return next();
});
The mental model:
Request
│
▼
Middleware
│
├── redirect / return response
│
▼
Route renders
│
▼
Response
Middleware is not for rendering UI. It is for controlling the request before UI rendering happens.
Server Islands
Client islands delay or control browser hydration.
Server islands solve a different problem.
A server island lets one dynamic server-rendered component load separately from the rest of the page.
Imagine a mostly static e-commerce page:
- Product description: static
- Reviews summary: static enough
- Header: static
- Personalized account widget: dynamic
You do not want the entire page to wait for personalized account data.
Use a server island:
---
import AccountMenu from '../components/AccountMenu.astro';
---
<h1>Product Name</h1>
<p>Static product content renders immediately.</p>
<AccountMenu server:defer />
Page render
┌────────────────────────────┐
│ Static HTML returns fast │
└──────────────┬─────────────┘
│
▼
┌────────────────────────────┐
│ Deferred server island │
│ renders separately │
└────────────────────────────┘
Client islands are about browser JavaScript.
Server islands are about dynamic server-rendered content.
Interview answer:
Client islands hydrate interactive components in the browser. Server islands defer dynamic server-rendered components so they do not block the rest of the page.
View Transitions
Astro is an MPA framework, but it can still give you smoother navigation.
View transitions let pages animate between navigations while keeping the Astro mental model.
---
import { ClientRouter } from 'astro:transitions';
---
<html lang="en">
<head>
<ClientRouter />
</head>
<body>
<slot />
</body>
</html>
Then Astro can preserve some client-side state and animate transitions between pages.
Use this when you want SPA-like navigation polish without turning the entire site into an SPA.
Do not confuse this with islands. Islands control component hydration. View transitions control navigation experience.
Integrations And Adapters
Astro integrations extend the framework.
Common examples:
- React, Vue, Svelte, Solid, Preact
- MDX
- Tailwind
- Sitemap
- Partytown
- Image tools
// astro.config.mjs
import { defineConfig } from 'astro/config';
import react from '@astrojs/react';
import mdx from '@astrojs/mdx';
export default defineConfig({
integrations: [react(), mdx()],
});
Adapters control where Astro runs when you need server rendering.
Examples:
- Vercel
- Netlify
- Cloudflare
- Node
Static Astro sites can be deployed almost anywhere. SSR Astro sites need an adapter for the runtime.
Interview answer:
Integrations add capabilities. Adapters target deployment runtimes.
Astro Performance Model
Astro is fast because of defaults.
Not because it has a magic compiler that makes all decisions perfect.
The defaults are:
- Render HTML on the server
- Ship zero JavaScript unless requested
- Hydrate only specific islands
- Let hydration happen later when possible
- Use static generation when content does not need per-request rendering
- Keep CSS and assets optimized through the build pipeline
Less JavaScript
│
▼
Less parse/execute work
│
▼
Faster page readiness
│
▼
Better Core Web Vitals
The main performance mistake in Astro is treating it like React with different file extensions.
If everything is client:load, you are pushing JavaScript back into the critical path.
Astro gives you control. You still have to use it well.
Astro vs Next.js
This comparison comes up a lot.
Next.js is a React framework for building full-stack React applications.
Astro is a content-first framework for building fast websites with optional islands of interactivity.
Astro Next.js
──────────────────────────── ─────────────────────────────
Content-first App-first / React-first
Zero JS by default React is central
Islands architecture Server/Client Components model
Great for blogs/docs/marketing Great for complex React apps
UI-framework agnostic React only
MPA by default App Router with nested layouts
Partial hydration by default Hydration depends on client boundary
Use Astro when:
- The site is mostly content
- SEO and first load speed are critical
- You want Markdown/content collections
- You need only some interactive widgets
- You want to mix UI frameworks carefully
Use Next.js when:
- The product is deeply React-based
- The app has lots of shared client state
- You need complex authenticated flows
- Most screens are interactive applications
- Your team wants the React ecosystem everywhere
Neither is universally better.
They optimize for different problems.
Common Interview Questions
What is Astro?
Astro is a content-focused web framework that renders HTML on the server and ships zero JavaScript by default. It supports static generation, server rendering, file-based routing, content collections, and islands of interactivity.
What is islands architecture?
Islands architecture means most of the page is static HTML, while specific interactive components are hydrated independently as islands. This avoids hydrating the entire page as one large JavaScript app.
What is partial hydration?
Partial hydration means only selected components receive client-side JavaScript. In Astro, this is controlled with directives like client:load, client:idle, and client:visible.
What is the difference between client:load and client:visible?
client:load hydrates immediately on page load. client:visible waits until the component enters the viewport. Use client:visible for below-the-fold interactive components.
Do Astro components run in the browser?
Not by default. .astro component frontmatter runs on the server at build time or request time. The output is HTML. Browser JavaScript only ships through scripts or hydrated client islands.
Can you use React in Astro?
Yes. Astro can render React, Vue, Svelte, Solid, Preact, and other framework components. To make a framework component interactive in the browser, add a client directive.
What is getStaticPaths() used for?
It tells Astro which pages to generate for dynamic static routes like [slug].astro. Each returned params object becomes one generated page.
SSG vs SSR in Astro?
SSG generates HTML at build time. SSR, also called on-demand rendering, generates HTML when a request arrives. Astro uses SSG by default but can opt routes into SSR.
What are content collections?
Content collections organize structured content and validate it with schemas. They give Markdown and data entries type safety, editor support, and query APIs.
What is a server island?
A server island is a deferred server-rendered component. It lets dynamic server content load separately so it does not block the rest of the page.
Actions vs API endpoints?
Actions are type-safe backend functions meant to be called from your Astro app. API endpoints are HTTP routes for custom responses, public APIs, webhooks, and external clients.
When should you not use Astro?
Do not choose Astro for an app where nearly every part of the page is dynamic, stateful, and client-interactive. At that point, a full SPA framework or a React-first framework may be a better fit.
The Mental Model
Astro is not trying to make the browser do more.
It is trying to make the browser do less.
That is the key.
Render HTML early. Ship JavaScript late. Ship JavaScript only when needed.
Content first
│
▼
HTML by default
│
▼
Interactivity as islands
│
▼
Less JavaScript
│
▼
Faster websites
Once you understand that, the rest of Astro makes sense.
.astro components are server-rendered.
Client directives opt components into hydration.
Content collections make content safe and structured.
Static generation gives you speed.
SSR gives you request-time flexibility.
Server islands stop dynamic server content from blocking the whole page.
Astro’s strength is restraint.
It gives you modern framework features, but it does not assume every page needs to become a JavaScript application.