> ## Documentation Index
> Fetch the complete documentation index at: https://docs.sevalla.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Astro

> This guide explains how to deploy a basic Astro site to Sevalla.

[Astro](https://astro.build/) is a modern, content-focused web framework designed to build fast, lightweight, and highly optimized websites. Its core philosophy is “Ship Less JavaScript”, meaning Astro renders as much of your site as possible to static HTML and only loads JavaScript in the browser when necessary.

Astro can be deployed using either Application Hosting (SSR) or Static Site Hosting (SSG). The [deployment mode](https://docs.astro.build/en/reference/configuration-reference/#output) is determined by how you configure your `astro.config.mjs` file. Astro’s `defineConfig.output` setting can only be set to `static` or `server`you cannot mix both modes within a single project.

By default, Astro generates a fully static site (SSG). To use SSR, you must explicitly opt in by setting`prerender: false`. If any part of your application uses `prerender: false`, includes server-only code, or depends on dynamic rendering, you should use Application Hosting instead of Static Site Hosting.

## Application Hosting

Choose Application Hosting (Server-Side Rendering) when your application requires server-side logic or dynamic behavior that static files cannot provide.

### Configuration

We recommend the following best practices for configuring Astro as an SSR on Sevalla:

* Use Astro’s partial hydration (`client:*` directives) only for components that need interactivity.
* Leverage static generation (SSG) whenever possible for better performance.
* Use `output: "server"` only for pages that truly require SSR.
* Enable compression middleware in your Node adapter for smaller responses.
* Optimize images using Astro’s built-in `<Image />` component.
* Sanitize and validate environment variables, and never expose secrets using the `PUBLIC_` prefix.
* Use HTTPS in production for secure communication.
* Implement rate limiting for API routes to prevent abuse.
* Add an `/api/health` endpoint for health checks.
* Configure structured logging for easier debugging and observability.
* Monitor build sizes and server response times to maintain performance.

The following is an example `astro.config.mjs` file for deploying an Astro SSR app on Sevalla:

```typescript theme={null}
// astro.config.mjs
import { defineConfig } from "astro/config";
import node from "@astrojs/node";

export default defineConfig({
  output: "server", // Enable SSR
  adapter: node({
    mode: "standalone", // Required for Sevalla deployments
  }),
  server: {
    host: true, // Listen on all network interfaces
    port: process.env.PORT || 4321,
  },
});
```

### Containerization

#### Dockerfile

The build for [Dockerfiles](https://docs.sevalla.com/applications/build-options/dockerfile) is fully customizable. The following is an example Dockerfile for Astro:

```javascript expandable theme={null}
# Dockerfile for Astro on Sevalla

FROM node:lts-alpine AS deps
WORKDIR /app

COPY package*.json ./
RUN npm ci

FROM node:lts-alpine AS builder
WORKDIR /app

COPY --from=deps /app/node_modules ./node_modules
COPY . .

RUN npm run build

FROM node:lts-alpine AS runner
WORKDIR /app

ENV NODE_ENV=production
ENV HOST=0.0.0.0
ENV PORT=3000

RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 astrouser

COPY --from=builder --chown=astrouser:nodejs /app/dist ./dist
COPY --from=builder --chown=astrouser:nodejs /app/package*.json ./

RUN npm ci --only=production

USER astrouser

EXPOSE 3000

CMD ["node", "./dist/server/entry.mjs"]
```

#### Nixpacks

You can customize the [Nixpacks](https://docs.sevalla.com/applications/build-options/nixpacks) build process by defining a `nixpacks.toml` file and using [**Nixpacks-specific environment variables**](https://nixpacks.com/docs/configuration/environment). This allows you to fine-tune how dependencies are installed, how your application is built, and which runtime settings are applied.

The following is an example `nixpacks.toml` configuration:

```javascript theme={null}
[phases.setup]
nixPkgs = ["nodejs", "yarn"]

[phases.install]
cmds = ["yarn install --frozen-lockfile"]

[phases.build]
cmds = ["yarn build"]

[start]
cmd = "yarn start"
```

Nixpacks will automatically detect your project’s lock file and select the appropriate package manager during deployment.

Additionally, you can specify the [**Node.js version**](https://nixpacks.com/docs/providers/node) used during the build by setting the `NIXPACKS_NODE_VERSION`[**environment variable**](https://nixpacks.com/docs/providers/node#:~:text=Setting%20the%20NIXPACKS_NODE_VERSION%20environment%20variable).

#### Buildpacks

If you're using [Buildpacks](https://docs.sevalla.com/applications/build-options/buildpacks), you cannot modify the underlying build phases directly or control dependencies. You must rely on the runtime environment that Buildpacks detects.

You can influence the build process by adjusting the `build` script in your `package.json`. Buildpacks will run whatever command you specify under the build script. For example, the standard command used to compile an Astro application is:

```javascript theme={null}
 "build": "astro build"
```

You can also add additional logic before the build runs, for example:

```javascript theme={null}
"build": "echo \"Hi mom!\" && astro build"
```

### CDN

Sevalla provides a premium, Cloudflare-powered CDN for Application Hosting at no additional cost. To get the most out of Sevalla’s CDN when deploying your Astro application, we recommend the following best practices:

* [Enable the CDN](https://docs.sevalla.com/applications/cdn) for all production applications.
* Use Astro’s `<Image>` component for automatic image optimization and responsive delivery.
* Set appropriate `Cache-Control` headers on API routes to ensure proper caching behavior.
* [Purge the CDN cache](https://docs.sevalla.com/applications/cdn#clear-the-cdn-cache) after deploying critical updates to avoid serving stale content.
* Leverage Astro’s fingerprinted assets in the `_astro/` directory for safe long-term caching.
* Organize static files in the `public/` directory for optimal delivery through the CDN.
* Use `getStaticPaths` for dynamic routes to pre-render content at build time.
* Combine static generation with incremental builds for frequently updated content.

### Edge caching

[**Edge caching**](https://docs.sevalla.com/applications/edge-caching) stores your Sevalla site cache on Cloudflare’s 260+ global data centers, delivering responses from the location nearest to each visitor for faster performance. To maximize the benefits of Sevalla’s Edge Caching for your Astro application, we recommend the following best practices:

* Set appropriate `Cache-Control` headers on server-rendered pages to control caching behavior.
* Combine edge caching with the CDN to cover both dynamic and static assets.
* Monitor cache efficiency using the `cf-cache-status` header returned by Cloudflare.

Fully static pages are cached indefinitely by default, providing maximum performance.

Edge caching works best when you combine static and dynamic content with proper cache headers. Below are strategies to optimize your Astro application on Sevalla for edge caching.

#### `Cache-Control`

With Sevalla’s Cloudflare integration, Cache-Control headers are respected at the edge, giving you precise control over how content is cached and served globally. The following are some common directives you can use in your Astro application:

* `public, s-maxage=3600` - Caches the response on the CDN for 1 hour, improving performance for frequently accessed content.
* `public, max-age=31536000, immutable` - Ideal for versioned static assets (e.g., files in `dist/_astro/`), allowing them to be cached for up to 1 year with no revalidation.
* `private` - Prevents CDN caching and ensures the response is only cached by the end user's browser, for personalized or sensitive data.

<Info>
  Sevalla does not yet support the `stale-while-revalidate` Cache-Control directive. To prevent unexpected caching behavior, we recommend not using this directive in your API or asset caching settings.
</Info>

For example:

```javascript theme={null}
// src/pages/api/public-data.ts
import type { APIRoute } from "astro";

export const GET: APIRoute = async () => {
  const data = await fetchPublicData();

  return new Response(JSON.stringify(data), {
    headers: {
      "Content-Type": "application/json",
      "Cache-Control": "public, s-maxage=3600",
    },
  });
};
```

#### API routes with edge caching

Edge-cache API responses by returning appropriate headers in your API routes. This speeds up API responses for repeated requests and reduces load on your origin server.

```typescript theme={null}
// src/pages/api/products.ts
import type { APIRoute } from "astro";

export const GET: APIRoute = async () => {
  const products = await fetchProducts();

  return new Response(JSON.stringify(products), {
    headers: {
      "Content-Type": "application/json",
      "Cache-Control": "public, s-maxage=3600",
    },
  });
};
```

#### Time-based revalidation

Use short-lived caching for content that updates frequently. This ensures frequently updated pages remain fresh and balances edge caching with dynamic content delivery.

```typescript theme={null}
---
// src/pages/news.astro
export const prerender = false;

const cacheTime = 300; // 5 minutes
const news = await fetchNews();

Astro.response.headers.set(
  'Cache-Control',
  `public, s-maxage=${cacheTime}`
);
---

<div>
  {news.map(item => (
    <article>{item.title}</article>
  ))}
</div>
```

### Health checks

Ensure your application remains available during deployments by implementing health checks:

* Always implement [**health checks**](https://docs.sevalla.com/applications/processes#health-checks) for production applications.
* Keep checks lightweight; responses should complete in under 1 second.
* Verify critical dependencies (e.g., databases, Redis) as part of the checks.
* Return 200 for degraded states to allow deployments to continue smoothly.
* Return 503 only for critical failures that require pod restarts.
* Close connections cleanly (e.g., database pools, Redis) to prevent resource leaks.
* Test deployments in staging with monitoring scripts to validate health checks.

**Basic health check**

```typescript theme={null}
// src/pages/api/health.ts
import type { APIRoute } from "astro";

export const GET: APIRoute = async () => {
  return new Response(
    JSON.stringify({
      status: "ok",
      timestamp: new Date().toISOString(),
    }),
    {
      status: 200,
      headers: { "Content-Type": "application/json" },
    }
  );
};
```

### Multi-tenancy

Sevalla fully supports multi-tenancy. You can build multi-tenant Astro applications using [**wildcard domains**](https://docs.sevalla.com/applications/domains), allowing you to serve multiple tenants from separate subdomains efficiently and securely. Use the following best practices for multi-tenancy on Sevalla with your Astro application:

* Use wildcard domains to serve subdomain-based tenants (e.g., `tenant1.app.com`).
* Add custom domains individually for tenants who have their own domains.
* Cache tenant data to reduce database queries and improve performance.
* Validate tenant existence before rendering pages to prevent errors or unauthorized access.
* Isolate tenant data in the database using separate schemas or a `tenant_id` column.
* Leverage free SSL certificates, which are automatically provided for both wildcard and custom domains.

#### Astro multi-tenant implementation

Below is a reference architecture for extracting tenant information from the hostname and injecting it into Astro’s request lifecycle.

**Extract tenant from subdomain (middleware)**

```typescript theme={null}
// src/middleware.ts
import { defineMiddleware } from "astro:middleware";

export const onRequest = defineMiddleware(async (context, next) => {
  const hostname = context.request.headers.get("host") || "";
  const subdomain = hostname.split(".")[0];

  // Skip for main domain
  if (subdomain === "www" || subdomain === "yourdomain") {
    return next();
  }

  // Add tenant to locals for access in pages
  context.locals.tenantId = subdomain;

  return next();
});
```

**Use tenant information in pages**

```typescript theme={null}
---
// src/pages/index.astro
import { getTenantData } from '../lib/tenant';

const tenantId = Astro.locals.tenantId;
const tenant = await getTenantData(tenantId);
---

<html>
  <head>
    <title>Welcome to {tenant?.name || 'Our Platform'}</title>
  </head>
  <body>
    <h1>Welcome to {tenant?.name}</h1>
    <p>Tenant ID: {tenantId}</p>
  </body>
</html>
```

**Tenant metadata lookup**

```typescript theme={null}
// src/lib/tenant.ts
import { pool } from "./db";

export async function getTenantData(tenantId: string | undefined) {
  if (!tenantId) return null;

  const result = await pool.query(
    "SELECT * FROM tenants WHERE subdomain = $1",
    [tenantId]
  );

  return result.rows[0] || null;
}
```

**Tenant-aware API routes**

```typescript theme={null}
// src/pages/api/data.ts
import type { APIRoute } from "astro";
import { getTenantData } from "../../lib/tenant";

export const GET: APIRoute = async ({ locals }) => {
  const tenant = await getTenantData(locals.tenantId);

  if (!tenant) {
    return new Response(JSON.stringify({ error: "Tenant not found" }), {
      status: 404,
      headers: { "Content-Type": "application/json" },
    });
  }

  return new Response(JSON.stringify(tenant), {
    headers: { "Content-Type": "application/json" },
  });
};
```

#### Custom domains per tenant

Tenants may want to use their own domains instead of subdomains. Sevalla supports this through domain mapping.

```typescript theme={null}
// src/middleware.ts
import { defineMiddleware } from "astro:middleware";
import { getTenantByDomain } from "./lib/tenant";

export const onRequest = defineMiddleware(async (context, next) => {
  const hostname = context.request.headers.get("host") || "";
  const tenant = await getTenantByDomain(hostname);

  context.locals.tenantId = tenant?.id || hostname.split(".")[0];

  return next();
});
```

### Troubleshooting common SSR issues

#### Cannot use import.meta.env on the client

* Use the `PUBLIC_` prefix for any environment variables that must be exposed to client-side code.
* Keep all server-only variables **unprefixed** and ensure they are accessed only in server-side modules.

#### Route handler not found

* Confirm the file is located in the correct directory: `src/pages/`.
* Check that the file uses a supported extension: `.astro`, `.js`, or `.ts`.
* Verify that your `output` mode in `astro.config.mjs` matches your intended deployment (e.g., `server` for SSR).

## Static Site Hosting

Static Site Generation (SSG) pre-renders all pages as static HTML files at build time, delivering fast and reliable performance. Use SSG when your site includes:

* Content that changes infrequently, such as blog posts, documentation, or product catalogs.
* Pages that can be pre-rendered for all users without personalization.
* Requirements for maximum performance and minimal hosting costs.
* A global audience, as static pages benefit from fast CDN distribution.

### Configuration

To deploy a static Astro site on Sevalla, set `output: "static"` in your `astro.config.mjs` file and ensure that no pages or components use `prerender: false`, which would force SSR.

/

```javascript theme={null}
export default defineConfig({
  output: "static", // SSG by default
  adapter: node({
    mode: "standalone",
  }),
});
```

### **Creating SSG pages**

All `.astro` pages are automatically pre-rendered at build time in static mode:

```
---
// src/pages/my-static-page.astro
// This page is automatically static (SSG)
const data = await fetchData(); // Runs at build time
---

<html>
  <body>
    <h1>Static Page</h1>
    <p>Built at: {new Date().toISOString()}</p>
  </body>
</html>
```

### **Dynamic routes with SSG**

Generate multiple pages from data using `getStaticPaths()`:

```
---
// src/pages/blog/[slug].astro
export async function getStaticPaths() {
  const posts = await fetchBlogPosts();

  return posts.map(post => ({
    params: { slug: post.slug },
    props: { post }
  }));
}

const { post } = Astro.props;
---

<html>
  <body>
    <h1>{post.title}</h1>
    <div set:html={post.content} />
  </body>
</html>
```

This generates a static page for each blog post at build time.

### [**​**](https://docs.sevalla.com/quick-starts/javascript/next#configuration-2)
