Skip to content

Top 35 Astro Interview Questions and Answers [2026]

November 11, 2026 29 min read
Astro interview guide by Second Talent
TL;DR: Astro interviews in 2026 test whether a candidate can decide what ships JavaScript, what renders at build time and what renders per request, then defend that choice with caching. Candidates should know Astro 7 (7.0 shipped June 22, 2026; the current release is 7.4.7 from November 5, 2026), its Rust compiler and Vite 8 base, the Content Layer and live collections, server islands, Actions, sessions and stable route caching.

Astro 7.0 replaced the Go-based .astro compiler with a Rust one that no longer corrects invalid HTML and errors on unclosed tags, so templates that built for years can fail on upgrade.

The Astro 7.0 release post also moved the framework to Vite 8 with Rolldown and made queued rendering the default engine.

The questions below are for senior front-end and full-stack interviews on content sites, marketing sites and server-rendered Astro apps, with four find-the-bug rounds.

Key takeaways
  1. 1Astro 7 changed the compressHTML default from true to 'jsx', so two spans on separate lines can render with no space between them.
  2. 2Server island props travel encrypted in a GET query string, and Astro switches to an uncacheable POST once the URL passes 2,048 bytes.
  3. 3Every Action is a public endpoint at /_actions/<name>, so each handler needs its own authorization check.
  4. 4Astro 7.4 raised the minimum Node.js version to 22.22.0, up from the 22.12.0 floor that Astro 6 set when it dropped Node 18 and 20.

Islands and Rendering

1. What is the islands architecture, and what does Astro ship to the browser by default?

Astro renders every component to HTML on the server or at build time and ships no component JavaScript unless you mark a component as an island.

An island is a framework component (React, Preact, Svelte, Vue, Solid) with a client:* directive; Astro renders its HTML first, then loads that component's code and hydrates only that subtree.

A strong candidate separates the two kinds of components. .astro components never hydrate: their frontmatter runs only on the server, and any interactivity goes in a <script> tag that Astro bundles and processes.

Framework components without a directive render to static HTML and send zero bytes of their own code. A weak answer describes islands as "lazy loading" and misses that the default is no JavaScript at all.

2. Walk through the client directives. Which would you use for a header search box, a below-the-fold carousel and a mobile-only menu?

Per the directives reference:

  • client:load hydrates as soon as the page loads. Use it for the search box a user may click first.
  • client:idle waits for requestIdleCallback, with an optional timeout in milliseconds since 4.15.
  • client:visible hydrates when an IntersectionObserver sees the element, with an optional rootMargin. Use it for the carousel.
  • client:media hydrates when a media query matches. Use it for the mobile menu.
  • client:only skips server rendering and needs the framework name as its value.
<SearchBox client:load />
<Carousel client:visible={{ rootMargin: "200px" }} />
<MobileMenu client:media="(max-width: 50em)" />
<MapWidget client:only="react">
  <p slot="fallback">Loading map...</p>
</MapWidget>

Good candidates point out that client:media still renders the menu's HTML on desktop, which may need CSS to hide, and that client:only leaves a hole in the server HTML that the fallback slot should fill to avoid layout shift.

3. What can you pass as props to a hydrated island?

Only values Astro can serialize into the HTML. Astro writes island props into the page so the client can rebuild them, so plain objects, arrays, strings, numbers, dates and similar values work, while functions do not.

A callback prop such as onChange works in server-only rendering and silently fails once the component hydrates.

The fix is to move the interaction into the island: let the island own its state and event handlers, or share state between islands through a small store library or custom events. Senior candidates also watch prop size.

A 300 KB product array passed to client:load is duplicated into the HTML as serialized data and then parsed on the main thread.

4. How do prerendering and on-demand rendering combine in one project?

By default Astro prerenders every route to static HTML.

Add an adapter and you can opt individual pages or endpoints into on-demand rendering with export const prerender = false, or set output: 'server' to flip the default and opt static pages back in with export const prerender = true.

The on-demand rendering guide lists official adapters for Node.js, Netlify, Vercel and Cloudflare.

The deciding question is whether the response depends on the request. Cookies, sessions, headers and query strings need on-demand rendering.

A strong candidate keeps the marketing pages static, renders account pages on demand and uses server islands (question 19) for the parts of a static page that are personal.

5. Where can you set response headers and the status code, and where can you not?

Only at the page or endpoint level, on routes rendered on demand. Astro streams HTML, so by the time a nested component or a layout runs, the headers may already be on the wire.

The docs say features that modify the response headers are available only at the page level, not inside components, including layouts.

---
// src/pages/account.astro
export const prerender = false;
const user = Astro.locals.user;
if (!user) return Astro.redirect('/login');
Astro.response.headers.set('Cache-Control', 'private, no-store');
---

A weak answer puts the redirect in a shared layout and wonders why it fails intermittently. In Astro 7, a candidate who wants CDN caching should also mention Astro.cache.set() (question 22) instead of writing Cache-Control by hand.

6. How do <script> tags behave in an Astro component?

A plain <script> in an .astro file is processed: Astro bundles it as a module, resolves imports, supports TypeScript and deduplicates it, so a component used 20 times on a page ships its script once.

Adding any attribute other than src, or is:inline, opts out of processing and leaves the tag as written.

define:vars is the common surprise. On a <script> it implies is:inline, so the script is no longer bundled and renders once per component instance.

The better pattern for passing server values to a processed script is a data-* attribute that the script reads.

Since Astro 6, scripts and styles render in the order you define them, a behavior that used to sit behind the preserveScriptOrder flag.

7. What does queued rendering change, and when does it matter?

Queued rendering is the rendering engine that became the default in Astro 7, after shipping as experimental.queuedRendering in 6.0.

The 7.0 post reports about 2.4 times faster rendering on expression-dense pages, with less gain on static ones.

The same post reports build times 15% to 61% faster in Astro's benchmarks across the Rust compiler, the Rust Markdown pipeline and the new engine together.

A good answer stays measured: the speedup shows up on large builds with many dynamic expressions, such as a docs site with thousands of pages.

Candidates who have profiled a build should mention astro build --timings, added in 7.4, which breaks build time down by phase, Vite plugin, Markdown, rendering, images and integrations.

Content Layer and Collections

8. How do you define a typed content collection in Astro 7?

In src/content.config.ts, with defineCollection(), a required loader and an optional Zod schema. Since Astro 6, Zod comes from astro/zod (Zod 4), and the legacy type: 'content' collections are gone.

// src/content.config.ts
import { defineCollection, reference } from 'astro:content';
import { glob, file } from 'astro/loaders';
import { z } from 'astro/zod';

const authors = defineCollection({
  loader: file('src/data/authors.json'),
  schema: z.object({ name: z.string(), role: z.string() }),
});

const blog = defineCollection({
  loader: glob({ pattern: '**/*.{md,mdx}', base: './src/content/blog' }),
  schema: ({ image }) =>
    z.object({
      title: z.string(),
      pubDate: z.coerce.date(),
      draft: z.boolean().default(false),
      cover: image().optional(),
      author: reference('authors'),
    }),
});

export const collections = { blog, authors };

A strong candidate explains what each piece buys: the schema validates frontmatter at build time and generates types, image() resolves a relative path into optimizable image metadata, and reference() stores a { collection, id } pair that getEntry() resolves later.

9. How do you query and render entries, and what changed for code written against Astro 4?

getCollection('blog', filter) returns an array, getEntry('blog', id) returns one entry, and render(entry), imported from astro:content, returns a Content component and the headings. Per the Astro 6 upgrade guide, older APIs are removed:

  • getEntryBySlug() and getDataEntryById() became getEntry().
  • entry.slug became entry.id; a slug field in frontmatter overrides the generated ID.
  • entry.render() became render(entry).
  • Astro.glob() became import.meta.glob().
---
import { getCollection, render } from 'astro:content';

export async function getStaticPaths() {
  const posts = await getCollection('blog', ({ data }) => !data.draft);
  return posts.map((post) => ({ params: { id: post.id }, props: { post } }));
}

const { post } = Astro.props;
const { Content, headings } = await render(post);
---
<h1>{post.data.title}</h1>
<Content />

10. Write a custom loader that syncs a remote API incrementally.

An object loader has a name and a load(context) function, and the context gives it a store, a persisted meta key-value store, parseData, generateDigest and a logger.

The Content Loader API reference documents that store.set() returns false and skips the write when the new entry's digest matches the stored one.

import type { Loader } from 'astro/loaders';
import { z } from 'astro/zod';

export function jobsLoader({ url }: { url: string }): Loader {
  return {
    name: 'jobs-loader',
    schema: z.object({ title: z.string(), location: z.string(), updatedAt: z.string() }),
    async load({ store, meta, parseData, generateDigest, logger }) {
      const since = meta.get('lastSync');
      const res = await fetch(since ? `${url}?since=${since}` : url);
      if (!res.ok) throw new Error(`jobs API returned ${res.status}`);
      const jobs: Array<{ id: string; title: string; location: string; updatedAt: string }> =
        await res.json();

      for (const job of jobs) {
        const data = await parseData({ id: job.id, data: job });
        store.set({ id: job.id, data, digest: generateDigest(data) });
      }
      meta.set('lastSync', new Date().toISOString());
      logger.info(`synced ${jobs.length} jobs`);
    },
  };
}

A senior adds that an incremental sync cannot see deletions, so the API needs a tombstone list or the loader needs a periodic full sync that calls store.clear().

A weak answer writes unvalidated data to the store, which skips the schema and breaks type safety.

11. When do you choose live collections over build-time collections?

Live collections, stable since Astro 6, fetch data on each request instead of at build time.

You define them in src/live.config.ts with defineLiveCollection(), and query them with getLiveCollection() and getLiveEntry(), which return { entries } or { entry } plus an error and an optional cacheHint.

---
export const prerender = false;
import { getLiveEntry } from 'astro:content';

const { entry: product, error, cacheHint } = await getLiveEntry('products', Astro.params.slug);
if (error || !product) return Astro.redirect('/404');
if (cacheHint) Astro.cache.set(cacheHint);
---

Use build-time collections for content that changes on publish, for MDX and for optimized images. Use live collections for inventory, prices, CMS draft previews and request-specific filters.

The docs list the limits a candidate should know: live collections need an adapter, and they do not support MDX or image optimization. Errors come back as values, so the strong answer handles error on every call instead of assuming a throw.

12. A docs site has 40,000 Markdown files and builds run out of memory. What do you try?

Start by measuring, then reduce what the content layer holds in memory and what each build re-renders. Options available in Astro 7:

  • astro build --timings (7.4) to find out whether Markdown, rendering, images or an integration dominates.
  • deferRender: true on glob() (7.1), which stores entries without rendering Markdown during sync. It lowers memory but stops caching rendered HTML across builds, and it does not apply to MDX or Markdoc.
  • retainBody: false (5.17) when no page reads entry.body.
  • experimental.collectionStorage: 'chunked' (7.1), which splits the data store into files of about 10 MB instead of one data-store.json.
  • experimental.incrementalBuild (7.2) with a cacheKey per path, so unchanged pages are reused (question 31).

A strong candidate also checks the build machine's Node heap settings and image work before blaming content.

13. What did the move to Zod 4 break in content schemas?

Two things show up in upgrades. The import moved: z from astro:content and the astro:schema module are deprecated in favor of astro/zod.

And Zod 4 changed .default(): the default value must now match the schema's output type, and the upgrade guide points to .prefault() for the old behavior of running the default through the parser.

A candidate who did the migration can describe a schema like z.string().transform(Number).default('0') that type-checked in Zod 3 and fails in Zod 4.

Loader authors also met createSchema(), added in Astro 6, which replaced the loader schema function for schemas generated at build time.

Actions, Sessions, Env and Middleware

14. How do Astro Actions work, and how do you call one from a form and from client code?

Actions are type-safe server functions exported in a server object from src/actions/index.ts. Each one has an optional Zod input and a handler(input, context). The Actions guide covers both call styles.

// src/actions/index.ts
import { defineAction, ActionError } from 'astro:actions';
import { z } from 'astro/zod';

export const server = {
  newsletter: defineAction({
    accept: 'form',
    input: z.object({ email: z.email(), consent: z.coerce.boolean() }),
    handler: async ({ email, consent }, context) => {
      if (!consent) {
        throw new ActionError({ code: 'BAD_REQUEST', message: 'Consent required' });
      }
      await subscribe(email);
      return { subscribed: true };
    },
  }),
};

From client code, await actions.newsletter(formData) returns { data, error }, and .orThrow() returns the data or throws.

From HTML, a <form method="POST" action={actions.newsletter}> works without JavaScript, but the page must render on demand, and the page reads the outcome with Astro.getActionResult(actions.newsletter).

A strong candidate uses isInputError(error) to show error.fields next to each input.

15. Are Actions protected by default?

No. Each action is reachable as a public endpoint named after it, so blog.like is served at /_actions/blog.like, and anyone can call it with curl.

The handler must check who is calling, usually through context.locals populated by middleware, and throw ActionError with UNAUTHORIZED or FORBIDDEN.

Middleware can see which action is running through getActionContext(context) and can block RPC calls with action?.calledFrom === 'rpc', but the docs say that is not a substitute for authorization in the handler.

Weak candidates assume that an action only used by a logged-in page is only callable by logged-in users.

16. How do sessions work, and which adapters configure them for you?

Sessions store data on the server keyed by a cookie, through Astro.session in pages and context.session in endpoints, actions and middleware.

The sessions guide says the Node, Cloudflare and Netlify adapters configure a default driver; other adapters need one set in config, and sessions are not supported in edge middleware.

// astro.config.mjs
import { defineConfig, sessionDrivers } from 'astro/config';
import vercel from '@astrojs/vercel';

export default defineConfig({
  adapter: vercel(),
  session: { driver: sessionDrivers.lruCache({ max: 800 }) },
});
---
export const prerender = false;
const cart = (await Astro.session?.get('cart')) ?? [];
---

Follow-ups worth asking: get() is async because the driver may be a remote store, regenerate() issues a new session ID after login to prevent fixation, and an in-memory LRU driver loses data across instances and deploys.

Since 7.2, session: false removes the session runtime from the bundle and leaves Astro.session undefined.

17. How does astro:env keep secrets out of the client bundle?

You declare each variable in env.schema with an envField type, a context and an access level.

Client public values import from astro:env/client and end up in both bundles; server values import from astro:env/server; secrets are left out of the build output and read at runtime.

import { defineConfig, envField } from 'astro/config';

export default defineConfig({
  env: {
    schema: {
      PUBLIC_API_URL: envField.string({ context: 'client', access: 'public' }),
      PORT: envField.number({ context: 'server', access: 'public', default: 4321 }),
      STRIPE_SECRET_KEY: envField.string({ context: 'server', access: 'secret' }),
    },
    validateSecrets: true,
  },
});

The environment variables guide states that context: 'client' with access: 'secret' is not allowed.

A strong candidate also knows the contrast with import.meta.env: since Astro 6 its values are always inlined and never coerced, so "true" stays a string, and a secret read through it in shared code can leak into the bundle.

18. What belongs in middleware, and what does not?

Middleware in src/middleware.ts runs before every on-demand route (and at build time for prerendered ones), which suits authentication lookups, setting context.locals, request logging and rewrites. sequence() composes several handlers in order.

import { defineMiddleware, sequence } from 'astro:middleware';

const auth = defineMiddleware(async (context, next) => {
  const token = context.cookies.get('sid')?.value;
  context.locals.user = token ? await lookupUser(token) : null;
  return next();
});

const timing = defineMiddleware(async (context, next) => {
  const start = performance.now();
  const response = await next();
  response.headers.set('Server-Timing', `app;dur=${performance.now() - start}`);
  return response;
});

export const onRequest = sequence(auth, timing);

Two things do not belong there: heavy data fetching for one page, and authorization decisions based only on the pathname.

The Astro authentication docs warn that the pathname middleware sees is not guaranteed to match the route Astro resolves, so encoding and base-path tricks can bypass a path-based guard.

Seniors check permissions again in the page, endpoint or action.

Server Islands, Caching and Deployment

19. What is a server island, and what are its constraints?

A server island is an .astro component marked server:defer: the page ships with fallback content, and the browser fetches the component's HTML from the server afterwards.

It lets a cacheable static page carry a personal avatar, a cart count or a price, and it needs an adapter.

<Avatar server:defer>
  <GenericAvatar slot="fallback" />
</Avatar>

The server islands guide lists the constraints a senior should know:

  • Props must be serializable; no functions and no circular references.
  • Props are encrypted with a key generated per build. Rolling deploys or CDN-cached pages need a stable key from astro create-key, set as ASTRO_KEY.
  • Requests use GET with props in the query string, so normal Cache-Control rules apply, until the URL passes 2,048 bytes and Astro falls back to POST, which browsers do not cache.
  • Inside the island, Astro.url points at /_server-islands/<Name>, not the page (question 29).

20. Server island or client island: how do you choose for a "recommended for you" panel?

Choose a server island when the data needs credentials or a database on the server and the panel has no interactivity after it loads. It ships no framework runtime, keeps the API key on the server, and the outer page stays cacheable.

Choose a client island when the panel is interactive or needs to update without another server round trip.

The tradeoffs a strong candidate names: a server island adds one request after the page loads, so the fallback must reserve space to avoid layout shift; a client island ships the framework and the fetching code, and the API it calls must be public.

A combined answer is often right: a server island that renders the list and contains a small client island for the "dismiss" button.

21. How do you deploy one Astro codebase to Node, Vercel, Netlify or Cloudflare?

Swap the adapter. The adapter turns the build's server output into the platform's format and supplies platform features such as default session drivers. Static-only sites need no adapter.

  • @astrojs/node builds a standalone server or a middleware handler for an existing Node server.
  • @astrojs/cloudflare runs workerd in development, prerendering and production since Astro 6, with bindings such as KV, D1 and R2 available through cloudflare:workers.
  • @astrojs/vercel and @astrojs/netlify map routes to the platforms' functions and image services.

Since Astro 6 the dev server uses Vite's Environment API, so development runs the same runtime code path as production.

A good candidate explains why this matters for Cloudflare: Node-only APIs that used to work in astro dev now fail in dev, where you can fix them, instead of after deploy.

22. How does route caching work in Astro 7?

Route caching became stable in 7.0. You configure a cache provider, then set cache rules per request with Astro.cache.set() in pages or context.cache.set() in endpoints and middleware, and invalidate by tag or path.

The route caching guide covers the provider and rule options.

// astro.config.mjs
import { defineConfig, memoryCache } from 'astro/config';
import node from '@astrojs/node';

export default defineConfig({
  adapter: node({ mode: 'standalone' }),
  cache: { provider: memoryCache() },
  routeRules: {
    '/products/[...slug]': { maxAge: 3600, tags: ['products'] },
  },
});
---
// src/pages/products/[...slug].astro
export const prerender = false;
Astro.cache.set({ maxAge: 120, swr: 60, tags: ['products'] });
---

Follow-ups: memoryCache() is a per-instance LRU (1,000 entries by default), so it suits a single Node server, not a fleet; cache.set(false) opts a request out of a route rule; and caching is disabled in astro dev, so testing it needs a build and preview.

CDN providers for Netlify, Vercel and Cloudflare exist but were still experimental in 7.0.

23. What is Advanced Routing, and when would you write src/fetch.ts?

Advanced Routing, on by default in Astro 7, lets src/fetch.ts export a fetch handler that wraps Astro's own pipeline.

The astro() handler runs the full built-in pipeline, and astro/hono exposes it as Hono middleware, so you can put routes or checks before and after Astro.

// src/fetch.ts
import { Hono } from 'hono';
import { astro } from 'astro/hono';

const app = new Hono();
app.get('/healthz', (c) => c.text('ok'));
app.use(astro());

export default app;

Use it when a project needs framework-level routing that middleware handles poorly: a health check that must not touch sessions, a webhook route with raw body handling, or Hono middleware the team already uses.

Without the file, behavior is unchanged. A candidate upgrading from 6.x should know that src/fetch.ts is now a reserved name, configurable with fetchFile, and that the 6.3 experiment used src/app.ts.

View Transitions

24. What do you get from <ClientRouter /> that browser-native view transitions do not give you?

Browser cross-document view transitions animate between full page loads with CSS alone and no JavaScript. <ClientRouter /> from astro:transitions turns the site into a client-side router: it intercepts navigation, fetches the next page, swaps the DOM and runs the animation, with a fallback for browsers without the View Transitions API.

The router adds what full page loads cannot do: transition:persist keeps an element or island and its state, such as a playing video or a form in progress, across pages; navigate() from astro:transitions/client triggers navigation from code; and lifecycle events let you hook into each step.

The cost is client JavaScript and a new class of bugs around scripts (question 28).

A strong candidate picks native transitions for a content site that wants only animation, and ClientRouter when state must survive navigation. <ViewTransitions /> was removed in Astro 6.

25. List the navigation lifecycle events in order, and say where you would hook in.

Per the view transitions guide: astro:before-preparation, astro:after-preparation, astro:before-swap, astro:after-swap and astro:page-load.

  • before-preparation: show a loading indicator, or change the loader to fetch differently.
  • before-swap: copy state into the incoming document, for example a theme class on <html>, so it does not flash.
  • after-swap: restore things the swap reset, before the new page paints.
  • page-load: re-initialize page scripts; it fires on the first load too.

Astro 7 removed the exported constants such as TRANSITION_AFTER_SWAP and the helpers isTransitionBeforeSwapEvent(); code now uses the event name strings and compares event.type.

26. How do transition:name, transition:animate and transition:persist-props differ?

transition:name pairs an element on the old page with one on the new page so the browser morphs between them; each name may appear once per page. transition:animate picks the animation for an element: fade (the default), initial, slide or none. transition:persist keeps the element itself, and adding transition:persist-props keeps an island's original props instead of re-rendering it with the new page's props.

The common bug is a list page that renders transition:name="card" on every item, which breaks the uniqueness rule. The fix is a per-item name such as transition:name={`card-${post.id}`}.

Seniors also mention that Astro disables these animations when the user has prefers-reduced-motion set, and that data-astro-reload forces a full load for links that must leave the router.

Code Review Rounds

Give the candidate five minutes per snippet and ask for the bug, what the user sees and the fix.

27. Find the bugs: a byline and plan list after the Astro 7 upgrade.

---
const { first, last } = Astro.props;
---
<p class="byline">
  By
  <span class="first">{first}</span>
  <span class="last">{last}</span>
</p>
<ul>
  <li>Basic<li>Pro
</ul>

Bugs: two Astro 7 behavior changes. The list relies on HTML's implied closing of <li>; the Rust compiler errors on unclosed tags and no longer corrects markup, so the build fails.

The byline relies on whitespace between elements; with the new compressHTML: 'jsx' default, whitespace between elements collapses the way JSX does, so the page can show "AdaLovelace".

Fix: close every tag, and add {' '} where a space between inline elements matters, or set compressHTML: true to keep the old behavior while the team audits templates.

A strong candidate searches the codebase for adjacent inline elements before upgrading instead of waiting for visual bug reports.

28. Find the bug: a menu that stops working after the first navigation.

---
// src/layouts/Base.astro
import { ClientRouter } from 'astro:transitions';
---
<html>
  <head><ClientRouter /></head>
  <body>
    <button id="menu-btn">Menu</button>
    <nav id="menu" hidden>...</nav>
    <slot />
    <script>
      const btn = document.getElementById('menu-btn');
      btn?.addEventListener('click', () => {
        document.getElementById('menu')?.toggleAttribute('hidden');
      });
    </script>
  </body>
</html>

Bug: bundled module scripts run once. After the first client-side navigation, the router swaps in a new body with a new button, and nothing attaches a listener to it. The menu works on the landing page and nowhere else.

Fix: wrap the setup in a listener for astro:page-load, which fires on the first load and after every navigation:

<script>
  document.addEventListener('astro:page-load', () => {
    document.getElementById('menu-btn')?.addEventListener('click', () => {
      document.getElementById('menu')?.toggleAttribute('hidden');
    });
  });
</script>

Alternatives a senior might offer: transition:persist on the header so the original button survives, or event delegation on document attached once. data-astro-rerun applies to inline scripts, not this bundled one.

29. Find the bug: a personalized price in a server island.

---
// src/components/MemberPrice.astro, used as <MemberPrice server:defer />
const sku = Astro.url.searchParams.get('sku');
const user = Astro.locals.user;
const price = await getPrice(sku, user?.tier);
---
<p>Your price: {price}</p>

Bug: inside a server island, Astro.url is the island endpoint, /_server-islands/MemberPrice, not the product page, so sku is always null.

The page URL is only available through the Referer header, which browsers can trim or omit under some referrer policies.

Fix: pass the SKU as a prop from the page, <MemberPrice server:defer sku={product.sku} />, so it travels in the encrypted props.

Follow-ups a strong candidate raises: the parent page is static and CDN-cached, so deploys need a stable ASTRO_KEY or old cached pages send props the new build cannot decrypt; and the island response varies by user, so it must not be cached publicly.

30. Find the bugs: a blog post route migrated from Astro 4.

---
// src/pages/blog/[slug].astro
import { getCollection } from 'astro:content';
import Comments from '../../components/Comments.jsx';

export async function getStaticPaths() {
  const posts = await getCollection('blog');
  return posts.map((post) => ({ params: { slug: post.slug }, props: { post } }));
}

const { post } = Astro.props;
const { Content } = await post.render();
---
<Content />
<Comments client:visible postId={post.id} onSubmit={(c) => console.log(c)} />

Bugs:

  • post.slug no longer exists on Content Layer entries; it is post.id, so every path param is undefined and the build fails.
  • post.render() was removed in Astro 6; use render(post) imported from astro:content.
  • onSubmit is a function passed to a hydrated island. It cannot be serialized, so the handler never reaches the browser.
  • No draft filter, so drafts publish; add a filter callback to getCollection().

Fix: rename the route to [id].astro or map slug: post.id, call render(post), and move the submit logic inside Comments, posting to an Action.

31. Design question: make a 20,000-page build fast without breaking correctness.

The candidate should start with measurement (astro build --timings), then look at what can be skipped.

Astro 7.2 added experimental.incrementalBuild, which reuses a prerendered page when both its route's module graph hash and its cacheKey match the previous build.

export async function getStaticPaths() {
  const posts = await getCollection('blog');
  return posts.map((post) => ({
    params: { id: post.id },
    props: { post },
    cacheKey: post.digest,
  }));
}

The correctness trap: post.digest covers the entry, not data the page pulls from elsewhere.

If the page renders the author's name from a referenced collection, or a "related posts" list, a changed author leaves stale pages, because the cache key did not change.

A strong answer folds those dependencies into the key, or keeps those routes out of the cache.

Other levers: experimental.parallelPrerender (7.4) renders static pages in worker threads, but it has no effect with @astrojs/cloudflare; image work can move to a CDN image service through the separate build and runtime image services added in 7.4.

The build cache lives in cacheDir, so CI must persist node_modules/.astro/ between runs.

What Changed Recently

Mar 10, 2026
Astro 6.0: Node 22+, Zod 4, Vite Environment API dev server, stable live collections, Fonts and CSP
May 7, 2026
Astro 6.3 adds experimental advanced routing
Jun 22, 2026
Astro 7.0: Rust compiler, Vite 8 with Rolldown, Sätteri Markdown, queued rendering, stable route caching
Jul 16, 2026
Astro 7.1: deferRender on glob(), finer CSP directives, chunked collection storage
Aug 6, 2026
Astro 7.2: experimental incremental builds and session: false
Oct 15, 2026
Astro 7.4: --timings, split build and runtime image services, Node 22.22.0 minimum

32. What did Astro 6 remove that breaks older projects?

Astro 6.0 shipped on March 10, 2026 and removed most of the compatibility layer kept through 5.x:

  • Node 18 and 20 support; Node 22.12.0 or later is required.
  • Legacy content collections and the legacy.collections flag; every collection needs a loader.
  • Astro.glob(), <ViewTransitions />, getEntryBySlug(), entry.render() and emitESMImage().
  • CommonJS config files (.cjs, .cts).

Defaults also changed: i18n.routing.redirectToDefaultLocale now defaults to false, the default image service crops when no fit is set and never upscales, and getImage() throws on the client.

The same release made the Fonts API, the CSP API (security: { csp: true }) and live collections stable, and moved syntax highlighting to Shiki 4.

33. What does the Rust compiler and Vite 8 move in Astro 7 mean for a team upgrading?

Per the Astro 7 upgrade guide, the Rust compiler is now the only compiler.

It parses with Oxc, scopes CSS with Lightning CSS, errors on unclosed tags, does not fix invalid nesting, and can serialize CSS values such as colors and url() differently.

Vite 8 replaces esbuild and Rollup with Rolldown; Astro converts esbuild and rollupOptions settings through a compatibility layer, but custom Vite plugins that depend on Rollup internals need testing.

The upgrade plan a senior describes: run npx @astrojs/upgrade, fix compiler errors, diff the rendered HTML of key pages against the 6.x build to catch whitespace and CSS changes, and check every remark or rehype plugin.

Removed in 7.0: @astrojs/db, with node:sqlite or an ORM such as Drizzle suggested in its place.

34. Your project uses remark and rehype plugins. What happens when you upgrade to Astro 7?

Astro 7 made Sätteri, a Rust Markdown and MDX pipeline built on pulldown-cmark and Oxc, the default processor, and @astrojs/markdown-remark is no longer installed by default.

Sätteri covers GFM, smart punctuation, heading IDs, math, directives and frontmatter natively, and its plugins declare the node types they handle instead of walking the whole tree.

To keep existing plugins, install @astrojs/markdown-remark and set markdown.processor to unified(); the remarkPlugins and rehypePlugins options still work with it.

A strong candidate decides per plugin: a table-of-contents or heading-ID plugin often becomes unnecessary with Sätteri's built-ins, while a custom rehype transform may justify staying on unified until it is ported.

35. What did Astro 7.1 to 7.4 add that a senior should know?

  • 7.1 (July 16, 2026): script-src-elem, style-src-attr and related CSP directives; a format option on paginate(); astro dev --ignore-lock; deferRender on glob().
  • 7.2 (August 6, 2026): experimental incremental static builds, session: false, and astro preview --background.
  • 7.3 (September 3, 2026): astro preview --ignore-lock, the logger passed into custom image services and cache providers, and a finalize() helper for Cloudflare custom worker entrypoints.
  • 7.4 (October 15, 2026): astro build --timings, separate build and runtime image services, a notFound() handler for custom entrypoints, experimental parallelPrerender and redirectPage, and a Node.js 22.22.0 minimum.

Astro 7 also added an agent-aware dev server: astro dev --background with stop, status and logs subcommands, JSON logs with --json, and a /_astro/status health endpoint.

Candidates who pair with AI coding agents should know these, since the dev server switches to background mode and JSON logging when it detects an agent. The current patch release is astro@7.4.7 (November 5, 2026).

Signs of a Strong Answer

  • They say, for each component, whether it ships JavaScript and which directive decides when.
  • They decide prerender versus on-demand per route from whether the response depends on the request, and reach for a server island before turning a whole page dynamic.
  • They check authorization inside every Action handler and know the /_actions/ URL is public.
  • They set a stable ASTRO_KEY before caching pages that contain server islands.
  • They write script setup inside astro:page-load once <ClientRouter /> is in the layout.
  • They can list what Astro 6 and 7 removed, and diff rendered HTML when upgrading instead of trusting a green build.

Hiring Astro Developers

Astro developers who have run a large content build, migrated across the Astro 6 and 7 breaking releases and deployed server islands behind a CDN are rare on general job boards.

Second Talent helps US startups hire vetted AI-Native Engineers from Asia, including front-end developers and web developers who work in Astro, screened with questions like these.

For adjacent stacks, see our front-end developer, TypeScript and Svelte interview guides.

Rates run $23 to $39 USD an hour, billed monthly, with matches in 24 hours and a 90-day one-time replacement guarantee. Compare budgets on the cost to hire a front-end developer page, or tell us what you need.

Get developers who ship with AI agents.

Starts at $23/Hour.

Start Hiring

$0 cost until you hire