# Mount it in your Astro config

> One integration line, one actions export, one tsconfig block — and why output must be 'server' rather than a preference you get to have.

Source: https://editor.astrocraftthemes.com/docs/mounting/

Astro finds pages under `src/pages/` and actions at `src/actions/index.ts` and nowhere else, so a package that owns routes has to hand them over through the integration hooks Astro provides for exactly this. That is what `adminPackage` is. It injects the admin's routes, its session guard, and the build-time check that keeps those routes on demand.

Three edits, in files you already have.

## 1 · `astro.config.mjs`

```js
import { execFileSync } from "node:child_process";

import { defineConfig, envField } from "astro/config";
import node from "@astrojs/node";
import tailwindcss from "@tailwindcss/vite";

import { adminPackage, adminSession } from "./src/admin/mount.ts";

// The build stamps the commit it was built from, so a deploy is observable with no per-platform
// deploy API. `unknown` rather than a failed build for a tree that is not a checkout.
const buildRev = (() => {
  try {
    return execFileSync("git", ["rev-parse", "HEAD"], { encoding: "utf8" }).trim();
  } catch {
    return "unknown";
  }
})();

export default defineConfig({
  output: "server", // not a preference — see below
  adapter: node({ mode: "standalone" }), // or cloudflare / netlify / vercel — one line
  integrations: [adminPackage],

  // Sessions follow the account store: `undefined` unless ASTROCRAFT_DB_URL is set, in which case
  // the adapter's filesystem default is replaced by the same database. This has to be host config
  // rather than part of the integration — adapters claim the session slot in their own setup hook
  // and run first, so an integration arrives too late to take it.
  session: adminSession(),

  // Astro writes the content store to `.astro/` in dev and to `cacheDir` everywhere else, so
  // `astro sync` and Vitest (which resolves through `getViteConfig` as dev) otherwise fill and
  // read DIFFERENT files. Leave this out and the suite reports your collections as empty —
  // thirty-odd failures, and no error message anywhere pointing at this line.
  cacheDir: "./.astro/",

  // Both are optional as VALUES; the SCHEMA is structurally required. The invite and verification
  // actions import these from `astro:env/server`, so without this block the build does not warn,
  // it dies:
  //   [MISSING_EXPORT] "RESEND_API_KEY" is not exported by "\0astro:env/server".
  env: {
    schema: {
      RESEND_API_KEY: envField.string({ context: "server", access: "secret", optional: true }),
      MAIL_FROM: envField.string({ context: "server", access: "secret", optional: true }),
    },
  },

  vite: {
    plugins: [tailwindcss()],
    // The stamp above, as a compile-time constant.
    define: { "import.meta.env.BUILD_REV": JSON.stringify(buildRev) },
  },
});
```

That is a real working config, taken from an actual install rather than assembled from the parts. Four things in it are load-bearing and each produces a failure that names something else entirely:

- **`cacheDir`** — without it the test suite reports your collections as empty.
- **`env.schema`** — without it the build dies with `[MISSING_EXPORT]`.
- **`session: adminSession()`** — without it a database-backed store still leaves sessions on the filesystem, so a redeploy logs everybody out even though their accounts survived.
- **`define` for `BUILD_REV`** — without it publishing works and never confirms.

## 2 · `src/actions/index.ts`

Add one line to whatever is already there:

```ts
export { server } from "../admin/actions/index.ts";
```

If you already export your own actions, spread rather than replace — two `export { server } from` lines are a redeclaration, and `export *` twice silently keeps one:

```ts
import { server as admin } from "../admin/actions/index.ts";
import { server as mine } from "./mine.ts";

export const server = { ...admin, ...mine };
```

This is the one file you hand-write rather than copy, which is why it uses a relative specifier. Astro has no `injectAction` hook, so the mount cannot do it for you.

## 3 · `tsconfig.json`

Copy the `paths` block across. The package's own imports use `@admin/*`, `@config/*`, `@layouts/*`, `@components/*`, `@assets/*` and `@/*` internally.

> **`@layouts/*` resolves to `src/admin/layouts/` — the dashboard's, not yours.** If you write a page through the composer, it must not use that alias. Composed pages import through `@/*`, which is the only alias reaching an arbitrary directory.

## Why `output: 'server'` is not negotiable

Middleware runs at request time only for an on-demand route. For a prerendered one it runs at **build** time and never sees a visitor. So a statically built admin would ship its HTML — drafts included — straight past the session guard, and look perfect while doing it.

Under `output: 'server'`, on demand is the default and public pages opt back out. That inversion is the load-bearing decision of the whole rendering model, and you do not have to remember it: two build-time hooks refuse both directions of the mistake. Set `output: 'static'` and the build fails, naming the route.

The practical consequence for your own site is that **every page under `src/pages/` that is part of your public site now needs `export const prerender = true`**. Miss it and the page still works — it just renders per request and silently costs you a CDN cache hit.

```astro
---
export const prerender = true;
---
```

Astro's own detection regex is `/^\s*export\s+const\s+prerender\s*=\s*(true|false);?/m`, so indentation is fine. What defeats it is a `//` in front.

## The build stamp

Keep one line in whatever page answers `/`:

```astro
<meta name="build-rev" content={import.meta.env.BUILD_REV} />
```

The config above already defines `BUILD_REV` from `git rev-parse HEAD`, so this is the only half left to you — and it has to be a page the CMS can fetch over HTTP, which in practice means whatever answers `/`.

After a publish, the review screen polls your production site until that stamp is a descendant of the commit it pushed, which is what turns _"publishing…"_ into _"Your changes are live ✓"_. Without it, publishing still works; you just never get told when it landed.

## Restart your dev server

Astro reads its configuration once, at startup. A dev server that was already running before you mounted the CMS keeps serving a site with no CMS in it, and typically fails with a missing-file error naming a file that is plainly there. Stop it and start it again.

One related cost worth knowing: the route table is scanned off disk at config load rather than per request. A page **added** under `src/admin/pages/` while `astro dev` is running answers 404 until you restart. Editing an existing one still hot-reloads normally.

## Check it

```bash
pnpm check && pnpm build
```

`pnpm check` catches the `env.schema` and `paths` mistakes. `pnpm build` is the half no test replaces — the two rendering rules are build-time hooks, so a route on the wrong side of the static/on-demand line is caught there or nowhere.

Next: [Configuration](/docs/configuration/), the one file you write yourself.