# Contributing URL: https://analogjs.org/docs/contributing # Contributing Analog is an MIT-licensed open source project with its ongoing development made by contributors. ## Contributing to the framework ### Folder structure Source code for the Analog framework exists under the `packages/` folder. To contribute features or bug fixes to the framework, locate the relevant code in one of the `packages` sub-folder. In addition to the `create-analog` sub-folder, there is a sub-folder for each of the `npm` packages in the `@analogjs/*` scope: - `packages/create-analog` → `create-analog` - `packages/vite-plugin-angular` → `@analogjs/vite-plugin-angular` ### Setup Analog uses [pnpm](https://pnpm.io/) to manage its dependencies. Before opening a pull request, run the following command from the root folder to make sure your development dependencies are up-to-date: ```shell pnpm i ``` ### Running locally To serve the example application locally, run the following command from the root folder: ```shell pnpm dev ``` ### Build Analog uses [Nx](https://nx.dev) for builds. To build all projects locally, run the following command from the root folder: ```shell pnpm build ``` ### Testing Analog uses [Vitest](https://vitest.dev) for tests. To test all projects locally, run the following command from the root folder: ```shell pnpm test ``` ## Contributing to the docs and analogjs.org website ### Folder structure Source code for the Analog docs and the analogjs.org website exists under the `apps/docs-analog` project folder. To contribute documentation or website content, locate the relevant source code in one of the sub-folders: - `src/content` - Documentation and website content in Markdown, with translations under `src/content/`. - `src/app` - Angular pages, components, and docs chrome. - `src/styles.css` - Global styles. - `public` - Images and other static assets. ### Setup Analog uses [pnpm](https://pnpm.io/) to manage its dependencies. Before opening a pull request, run the following command from the root folder to make sure your development dependencies are up-to-date: ```shell pnpm i ``` ### Running locally Analog dogfoods itself to develop the docs and analogjs.org website. Run the following command from the `apps/docs-analog` folder to serve the website: ```shell pnpm nx serve ``` or alternatively run this command from the root folder: ```shell pnpm nx serve docs-analog ``` Once the development server is up and running, you can preview the docs and website by visiting [http://localhost:3000](http://localhost:3000). ### Build Analog uses [Nx](https://nx.dev) to build the docs and analogjs.org website. To build the website locally, run the following command from the `apps/docs-analog` folder: ```shell pnpm nx build ``` or alternatively run this command from the root folder: ```shell pnpm nx build docs-analog ``` ### Running static website locally To run the generated static website locally, run the following command from the `apps/docs-analog` folder: ```shell pnpm nx serve-static ``` or alternatively run this command from the root folder: ```shell pnpm nx serve-static docs-analog ``` ## Submitting pull requests **Please follow these basic steps to simplify pull request reviews. If you don't you'll probably just be asked to anyway.** - Please rebase your branch against the current `beta` branch. - Follow the `Setup` steps above to make sure your development dependencies are up-to-date. - Please ensure the test suite passes before submitting a PR. - If you've added new functionality, **please** include tests which validate its behavior. - Make reference to possible [issues](https://github.com/analogjs/analog/issues) on PR comment. - PRs may include multiple commits. However, please keep content of all commits related. Raise separate PRs for disjoint changes. - Use the PR template to capture the affected scope, your test plan, and any maintainer-facing merge recommendation. - `Squash merge` is highly preferred. If you recommend a non-squash merge, add a brief note explaining why the commit boundaries matter and why the PR should bypass focused changes per package. ### Pull Request title guidelines This allows the commit to be easier to read on GitHub as well as in various git tools. Samples: (even more [samples](https://github.com/analogjs/analog/commits/beta)) ``` feat(content): update prismjs to latest version ``` ``` fix(content): fix error when rendering markdown ``` ### Type Must be one of the following: - **build**: Changes that affect the build system or external dependencies (example scopes: gulp, broccoli, npm) - **ci**: Changes to our CI configuration files and scripts (example scopes: Travis, Circle, BrowserStack, SauceLabs) - **docs**: Documentation only changes - **feat**: A new feature - **fix**: A bug fix - **perf**: A code change that improves performance - **refactor**: A code change that neither fixes a bug nor adds a feature - **style**: Changes that do not affect the meaning of the code (white-space, formatting, missing semi-colons, etc.) - **test**: Adding missing tests or correcting existing tests ### Scope The scope should be the name of the npm package affected (as perceived by the person reading the changelog generated from commit messages). The following is the list of currently supported scopes: - **astro-angular** - **content** - **content-plugin** - **create-analog** - **nx-plugin** - **platform** - **router** - **storybook-angular** - **trpc** - **vite-plugin-angular** - **vite-plugin-nitro** - **vitest-angular** ### Breaking Changes If any breaking changes are included, they should be explained in the body of the pull request. Example: ``` feat(scope): commit message BREAKING CHANGES: Describe breaking changes here BEFORE: Previous code example here AFTER: New code example here ``` ## Submitting bug reports - Search through issues to see if a previous issue has already been reported and/or fixed. - Provide a _small_ reproduction using a [StackBlitz project](https://analogjs.org/new) or a GitHub repository. - Please detail the affected browser(s) and operating system(s). - Please be sure to state which version of Angular, node, and package manager (npm, pnpm, yarn) you're using. ## Submitting new features - We value keeping the API surface small and concise, which factors into whether new features are accepted. - Submit an issue with the prefix `Feature:` with your feature request. Use the `RFC:` prefix for a large feature with bigger impact to the codebase. - The feature will be discussed and considered. - After the PR is submitted, reviewed and approved, it will be merged. ## Questions and requests for support Questions and requests for support should not be opened as issues and should be handled in the following ways: - Start a new [Q&A Discussion](https://github.com/analogjs/analog/discussions/new?category=q-a) on GitHub. - Start a new thread in the `#help` forum on the [Discord server](https://chat.analogjs.org/) --- # Contributors URL: https://analogjs.org/docs/contributors # Contributors AnalogJS is maintained by a team of contributors and community. ## Analog core team ### Brandon Roberts ![Brandon Roberts](https://github.com/brandonroberts.png?size=240) Brandon is an NgRx core team member, Angular GDE, sports ranter, and gif slinger. ### Robin Goetz ![Robin Goetz](https://github.com/goetzrobin.png?size=240) Robin is a fullstack developer working with Angular, Spring, and AWS. He's also a core team member for Spartan UI. ### Chau Tran ![Chau Tran](https://github.com/nartc.png?size=240) Chau is a senior engineer for Nx, Angular GDE, Heavy VIM user and creator of Angular Three. ### Marko Stanimirović ![Marko Stanimirović](https://github.com/markostanimirovic.png?size=240) Marko is an NgRx core team member, Angular GDE, and an organizer of the Angular Belgrade group. ### Josh Morony ![Josh Morony](https://github.com/joshuamorony.png?size=240) Josh is a full-time content creator, Angular GDE, Ionic Dev Expert, and creator of Angular Start and Ionic Start courses. ### Luis Castro ![Luis Castro](https://github.com/luishcastroc.png?size=240) Luis is a software engineer for NAFC, passionate about technology, sports and food. ### Andres Villanueva ![Andres Villanueva](https://github.com/Villanuevand.png?size=240) Andres is a software engineer for HeroDevs, Angular and Firebase GDE. ## Contributors
Brandon
Brandon

πŸ’» πŸ“– πŸ€”
Lars Gyrup Brink Nielsen
Lars Gyrup Brink Nielsen

πŸ“– ⚠️
Marko Stanimirović
Marko Stanimirović

πŸ”§ πŸš‡ πŸ“– πŸ’» 🎨
Jason Hodges
Jason Hodges

πŸ“–
Tim Deschryver
Tim Deschryver

πŸš‡
Dale Nguyen
Dale Nguyen

πŸ’» 🎨
AndrΓ©s Villanueva
AndrΓ©s Villanueva

πŸ“– 🌍
Umair Hafeez
Umair Hafeez

πŸš‡
Brandon Largeau
Brandon Largeau

πŸš‡
Maina Wycliffe
Maina Wycliffe

πŸ’» πŸš‡
Preston Lamb
Preston Lamb

πŸ’» πŸ“–
Andrew Luca
Andrew Luca

πŸ’»
Chau Tran
Chau Tran

πŸ’» πŸš‡
Simone
Simone

πŸ’»
Kyler Johnson
Kyler Johnson

πŸ’»
Marc
Marc

πŸ“– πŸ’»
himyjan
himyjan

πŸ’»
Alex Kovalev
Alex Kovalev

πŸ“–
Nuhman Pk
Nuhman Pk

πŸ“–
MiloΕ‘ Lajtman
MiloΕ‘ Lajtman

πŸ’»
profanis
profanis

πŸ’»
Reece McDonald
Reece McDonald

πŸ“–
Matteo Pietro Dazzi
Matteo Pietro Dazzi

πŸ’»
LukΓ‘Ε‘ Matta
LukΓ‘Ε‘ Matta

πŸ“–
Luciano
Luciano

πŸ“– πŸ’»
Robin Goetz
Robin Goetz

πŸ’»
Vadim Evseev
Vadim Evseev

πŸ’»
Danny Koppenhagen
Danny Koppenhagen

πŸ“– πŸ’»
Tomasz Flis
Tomasz Flis

πŸ“–
AdditionAddict
AdditionAddict

πŸ“– πŸ’»
Sander
Sander

πŸ’»
Chris Perko
Chris Perko

πŸ’» πŸ“–
Christian LΓΌdemann
Christian LΓΌdemann

πŸ’»
Yasser
Yasser

πŸ’»
MichaΕ‚ Dyrcz
MichaΕ‚ Dyrcz

πŸ’»
Otoniel Guajardo
Otoniel Guajardo

πŸ“–
gergobergo
gergobergo

πŸ’» πŸ“–
saurajit
saurajit

πŸ“–
Mircea Rilă
Mircea Rilă

πŸ“– πŸš‡
Dominik
Dominik

πŸ“– πŸ’» πŸš‡
Henrique CustΓ³dia
Henrique CustΓ³dia

πŸ“–
ISODA Yu
ISODA Yu

πŸ“–
Cynthia Iradukunda
Cynthia Iradukunda

πŸ“–
Drunkenpilot
Drunkenpilot

πŸ“– πŸ’»
Jeremy Hofer
Jeremy Hofer

πŸ“– πŸ’» πŸš‡
Olalekan Raheem
Olalekan Raheem

πŸ“–
Luis Castro
Luis Castro

πŸ’» πŸ“– 🌍
Q
Q

πŸ’» πŸ“–
Glenn Latomme
Glenn Latomme

πŸ“–
Justin Rassier
Justin Rassier

πŸ“– πŸ’» πŸš‡
Matthieu Riegler
Matthieu Riegler

πŸ“– πŸš‡ πŸ’»
Ashley Hunter
Ashley Hunter

πŸ’»
Artur Androsovych
Artur Androsovych

πŸ’» πŸ“–
Bjorn Lu
Bjorn Lu

πŸš‡
Omar BELKHODJA
Omar BELKHODJA

πŸ’»
Deepak Rudra Paul
Deepak Rudra Paul

πŸ“–
Michael Avrukin
Michael Avrukin

πŸ“–
Rafael Mestre
Rafael Mestre

πŸ’» πŸ“– πŸš‡
Santosh Yadav
Santosh Yadav

πŸ“–
Tenessy
Tenessy

πŸš‡ πŸ’» ⚠️
Jad Chahed
Jad Chahed

πŸ“– 🌍
Gesiel Rosa
Gesiel Rosa

πŸ“– 🌍
Besim GΓΌrbΓΌz
Besim GΓΌrbΓΌz

πŸ“– 🌍
Lukas Nys
Lukas Nys

πŸ“–
Andreas LΓ€ndle
Andreas LΓ€ndle

πŸ’»
Pascal KΓΌsgen
Pascal KΓΌsgen

πŸ“–
Alejandro Cuba Ruiz
Alejandro Cuba Ruiz

πŸ“– 🌍
Shreyas0410
Shreyas0410

πŸ“–
Denis Bendrikov
Denis Bendrikov

πŸ“–
iancharlesdouglas
iancharlesdouglas

πŸ“–
Olivier Combe
Olivier Combe

πŸ’»
Sasidharan SD
Sasidharan SD

πŸ“–
Ajit Panigrahi
Ajit Panigrahi

πŸ’»
nepage-l
nepage-l

πŸ’»
Jeff
Jeff

πŸ’»
Sammy Mohamed
Sammy Mohamed

πŸ“–
Josh Morony
Josh Morony

πŸ’»
Ilir Beqiri
Ilir Beqiri

πŸ“–
MichaΕ‚ Nieruchalski
MichaΕ‚ Nieruchalski

πŸ’»
Angel Fraga Parodi
Angel Fraga Parodi

πŸš‡ πŸ’»
Alex
Alex

πŸš‡ πŸ’»
Doguhan Uluca
Doguhan Uluca

πŸ“–
N. Can KIRIK
N. Can KIRIK

πŸ’»
ShPelles
ShPelles

πŸ“–
Pavan Kumar Jadda
Pavan Kumar Jadda

πŸ’»
Esther White
Esther White

πŸ’» πŸ“–
Michael Richter
Michael Richter

πŸ’»
Rafael Triantafillidis
Rafael Triantafillidis

πŸ’»
Pooya Parsa
Pooya Parsa

πŸ“–
Corbin Crutchley
Corbin Crutchley

πŸ“–
Leblanc Meneses
Leblanc Meneses

πŸš‡ πŸ’» πŸ“–
James Culveyhouse
James Culveyhouse

πŸ’»
Naji
Naji

πŸ’»
Bitcollage
Bitcollage

πŸ“–
Sonu Kapoor
Sonu Kapoor

πŸ’»
ezzabuzaid
ezzabuzaid

πŸ’»
Eduardo Roth
Eduardo Roth

πŸ“–
Ryan Clements
Ryan Clements

πŸ“–
ByeongGi
ByeongGi

πŸ“– 🌍
Younes Jaaidi
Younes Jaaidi

πŸ’» ⚠️
BoogMon
BoogMon

πŸ“–
Anthony Garera
Anthony Garera

πŸ“–
Stewan
Stewan

πŸ’» ⚠️
Nate Radebaugh
Nate Radebaugh

πŸ’»
Wolfram Sokollek
Wolfram Sokollek

πŸš‡
Muhammad Uzair
Muhammad Uzair

πŸ“–
Pranav Ramesh
Pranav Ramesh

πŸ“–
Ben Snyder
Ben Snyder

πŸ“– πŸš‡
Jan-Niklas W.
Jan-Niklas W.

πŸ“–
Sergey Gultyayev (Serhii Hultiaiev)
Sergey Gultyayev (Serhii Hultiaiev)

πŸ’» πŸ“–
Anderson Feitosa
Anderson Feitosa

πŸ’»
Jun
Jun

πŸ“– 🌍
Felix Herold
Felix Herold

πŸ“– 🌍
Soheil Nazari [CHECK24]
Soheil Nazari [CHECK24]

πŸ“–
Maksymilian Szokalski
Maksymilian Szokalski

πŸš‡ πŸ’»
Alfonso AndrΓ©s LΓ³pez Molina
Alfonso AndrΓ©s LΓ³pez Molina

πŸ’» πŸ“–
Nermal
Nermal

πŸ“–
tobiasegli
tobiasegli

πŸ’»
Larson
Larson

πŸ’»
Ilyass
Ilyass

πŸ’»
Brad Bodine
Brad Bodine

πŸ“–
Kilesh Maharjan
Kilesh Maharjan

πŸ’»
Diego JesΓΊs
Diego JesΓΊs

πŸ’» πŸš‡
Rockerturner
Rockerturner

πŸ’»
Emmanuel DEMEY
Emmanuel DEMEY

πŸ“–
Nick Taylor
Nick Taylor

πŸ’»
Alex Rose-Pizant
Alex Rose-Pizant

πŸš‡
Matt Lewis
Matt Lewis

πŸ’»
Baptiste MahΓ©
Baptiste MahΓ©

πŸ’»
MiloΕ‘ Danilov
MiloΕ‘ Danilov

πŸ’»
r-yanyo
r-yanyo

πŸš‡
Nikos Anifantis
Nikos Anifantis

πŸ“–
Daichi Kudo
Daichi Kudo

πŸ’»
Roydon Gyles-Bedford
Roydon Gyles-Bedford

πŸ’»
Jordan
Jordan

πŸš‡
Shannon Moeller 🧒
Shannon Moeller 🧒

πŸ’»
Vlad Sternbach
Vlad Sternbach

πŸ’»
Miroslav JonaΕ‘
Miroslav JonaΕ‘

πŸš‡
Leosvel PΓ©rez Espinosa
Leosvel PΓ©rez Espinosa

πŸš‡
Panagiotis Bakatselos
Panagiotis Bakatselos

πŸ“–
Will Stewart
Will Stewart

πŸ’»
--- # Open Graph (OG) Image Generation URL: https://analogjs.org/docs/features/api/og-image-generation # Open Graph (OG) Image Generation Open Graph images can be used to display previews of pages when shared on social media sites such as Twitter/X, LinkedIn, Facebook, etc. Analog supports generating Open Graph images using [API Routes](./overview). ## Setup First, install the necessary [satori](https://github.com/vercel/satori) dependencies: ```shell npm install satori satori-html sharp --save ``` ```shell yarn add satori satori-html sharp ``` ```shell pnpm install -w satori satori-html sharp ``` ## Setting Up An API Route Next, define an API route in the `src/server/routes/api` directory. ```ts // src/server/routes/api/v1/og-images.ts export default defineEventHandler(async (event) => { const fontFile = await fetch( 'https://og-playground.vercel.app/inter-latin-ext-700-normal.woff', ); const fontData: ArrayBuffer = await fontFile.arrayBuffer(); const query = getQuery(event); // query params const template = `

${query['title'] ? `${query['title']}` : 'Hello World'}

`; return new ImageResponse(template, { debug: true, // disable caching fonts: [ { name: 'Inter Latin', data: fontData, style: 'normal', }, ], }); }); ``` - The API route uses the `ImageResponse` class from the `@analogjs/content/og` sub-package. - Provide HTML that is rendered to a png. - Tailwind class are supported, and optional. ## Adding Open Graph Metadata Open Graph images are registered through meta tags inside the HTML `head` tag. ```html ... ``` The meta tags can be set manually in the `index.html` or dynamically using [Route Metadata](/docs/features/routing/metadata#open-graph-meta-tags) --- # API Routes URL: https://analogjs.org/docs/features/api/overview # API Routes Analog supports defining API routes that can be used to serve data to the application. ## Defining an API Route API routes are defined in the `src/server/routes/api` folder. API routes are also filesystem based, and are exposed under the default `/api` prefix. ```ts export default defineEventHandler(() => ({ message: 'Hello World' })); ``` ## Validating an API Route with a Schema `defineApiRoute` adds Standard Schema validation to an API handler. For example, with Zod 3.24+: ```ts // src/server/routes/api/users.post.ts import { defineApiRoute } from '@analogjs/router/server/actions'; import { z } from 'zod'; export default defineApiRoute({ body: z.object({ name: z.string().min(1) }), handler: ({ body }) => ({ name: body.name }), }); ``` - `params`, `query`, and `body` validate their respective request values and infer the corresponding handler argument types. Body validation runs for methods other than GET and HEAD. - `input` validates query parameters for GET/HEAD, or the body for other methods, and provides its result as `data`. If separate schemas are also configured, they are validated too. Without `input`, `data` uses the validated query for GET/HEAD, or the validated body (falling back to query) for other methods. - Repeated query and form fields remain arrays. JSON, URL-encoded forms, and multipart forms are supported. Empty or unparseable bodies fall back to `{}`. - Invalid input returns HTTP 422 with a Standard Schema issues array and the `X-Analog-Errors` header, without calling the handler. - Plain return values become JSON responses. A returned `Response`, including one from `json`, `redirect`, or `fail`, passes through unchanged. - An optional `output` schema checks plain return values in development and tests. Failures produce a warning; they do not change the response. Output validation does not run in production. Schemas may validate asynchronously. The handler also receives the original h3 `event` for cookies, headers, and other request operations. ## Defining XML Content To create an RSS feed for your site, set the `content-type` to be `text/xml` and Analog serves up the correct content type for the route. ```ts //server/routes/api/rss.xml.ts export default defineEventHandler((event) => { const feedString = ` `; setHeader(event, 'content-type', 'text/xml'); return feedString; }); ``` **Note:** For SSG content, set Analog to prerender an API route to make it available as prerendered content: ```ts // vite.config.ts ... prerender: { routes: async () => { return [ ... '/api/rss.xml', ... . ]; }, sitemap: { host: 'https://analog-blog.netlify.app', }, }, ``` The XML is available as a static XML document at `/dist/analog/public/api/rss.xml` ## Dynamic API Routes Dynamic API routes are defined by using the filename as the route path enclosed in square brackets. Parameters can be accessed via `event.context.params`. ```ts // /server/routes/api/v1/hello/[name].ts export default defineEventHandler( (event) => `Hello ${event.context.params?.['name']}!`, ); ``` Another way to access route parameters is by using the `getRouterParam` function. ```ts // /server/routes/api/v1/hello/[name].ts export default defineEventHandler((event) => { const name = getRouterParam(event, 'name'); return `Hello, ${name}!`; }); ``` ## Specific HTTP request method File names can be suffixed with `.get`, `.post`, `.put`, `.delete`, etc. to match the specific HTTP request method. ### GET ```ts // /server/routes/api/v1/users/[id].get.ts export default defineEventHandler(async (event) => { const id = getRouterParam(event, 'id'); // TODO: fetch user by id return `User profile of ${id}!`; }); ``` ### POST ```ts // /server/routes/api/v1/users.post.ts export default defineEventHandler(async (event) => { const body = await readBody(event); // TODO: Handle body and add user return { updated: true }; }); ``` The [h3 JSDocs](https://www.jsdocs.io/package/h3#package-index-functions) provide more info and utilities, including readBody. ## Requests with Query Parameters Sample query `/api/v1/query?param1=Analog¶m2=Angular` ```ts // routes/api/v1/query.ts export default defineEventHandler((event) => { const { param1, param2 } = getQuery(event); return `Hello, ${param1} and ${param2}!`; }); ``` ## Typed API Routes Use `defineServerRoute` from `@analogjs/router/server/actions` to validate the request with any [Standard Schema](https://standardschema.dev) library, such as Valibot or Zod, and return JSON from the handler. Validation failures respond with a `422` status and the schema issues. ```ts // /server/routes/api/v1/todos.get.ts import { defineServerRoute } from '@analogjs/router/server/actions'; import * as v from 'valibot'; export const route = defineServerRoute({ query: v.object({ scope: v.optional(v.string(), 'default') }), handler: ({ query }) => getTodos(query.scope), }); export default route; ``` - `query` validates the URL search params and `body` validates the request body for `POST`, `PUT`, and `PATCH` requests. - `input` validates the body, or the search params on `GET`, and provides the result as `data`. - `params` validates the dynamic route params. - `output` validates the returned value in development and logs a warning on mismatch. Returning a `Response` from the handler sends it unchanged. Exporting the `route` lets the [TanStack Query integration](/docs/integrations/tanstack-query) infer the query, body, and result types on the client. ## Catch-all Routes Catch-all routes are helpful for fallback route handling. ```ts // routes/api/[...].ts export default defineEventHandler((event) => `Default page`); ``` ## Error Handling If no errors are thrown, a status code of 200 OK will be returned. Any uncaught errors will return a 500 Internal Server Error HTTP Error. To return other error codes, throw an exception with createError ```ts // routes/api/v1/[id].ts export default defineEventHandler((event) => { const param = getRouterParam(event, 'id'); const id = parseInt(param ? param : ''); if (!Number.isInteger(id)) { throw createError({ statusCode: 400, statusMessage: 'ID should be an integer', }); } return `ID is ${id}`; }); ``` ## Accessing Cookies Analog allows setting and reading cookies in your server-side calls. ### Setting cookies ```ts //(home).server.ts return { products: products, }; }; ``` ### Reading cookies ```ts //index.server.ts console.log('products cookie', cookies['products']); return { shipping: true, }; }; ``` ## More Info API routes are powered by [Nitro](https://nitro.unjs.io/guide/routing) and [h3](https://h3.unjs.io/). See the Nitro and h3 docs for more examples around building API routes. --- # WebSocket URL: https://analogjs.org/docs/features/api/websockets # WebSocket Analog also supports `WebSockets` and `Server-Sent Events` through Nitro. ## Enabling WebSockets Currently, WebSocket support in [Nitro](https://nitro.unjs.io/guide/websocket) is experimental and it can be enabled in the `analog` plugin: `vite.config.ts` ```typescript export default defineConfig({ // ... plugins: [ analog({ // ... nitro: { experimental: { websocket: true, }, }, }), ], // ... }); ``` **Note:** In development, the Vite HMR WebSocket server runs on the same port as the dev server by default. To prevent conflicts, you need to change this port. The dev server port is usually defined in `project.json`/`angular.json`, which takes precedence over `vite.config.ts`. To allow the port settings in `vite.config.ts` to take effect, remove the port definition from `project.json`/`angular.json`. Additionally, you can specify an optional path to easily differentiate connections in the browser dev tools: `vite.config.ts` ```typescript export default defineConfig({ // ... server: { port: 3000, // dev-server port hmr: { port: 3002, // hmr ws port path: 'vite-hmr', // optional }, }, // ... }); ``` ## Defining a WebSocket Handler Similar to [API routes](/docs/features/api/overview), WebSocket Handlers are defined in the `src/server/routes/api` folder. ```typescript // src/server/api/routes/ws/chat.ts export default defineWebSocketHandler({ open(peer) { peer.send({ user: 'server', message: `Welcome ${peer}!` }); peer.publish('chat', { user: 'server', message: `${peer} joined!` }); peer.subscribe('chat'); }, message(peer, message) { if (message.text().includes('ping')) { peer.send({ user: 'server', message: 'pong' }); } else { const msg = { user: peer.toString(), message: message.toString(), }; peer.send(msg); // echo peer.publish('chat', msg); } }, close(peer) { peer.publish('chat', { user: 'server', message: `${peer} left!` }); }, }); ``` ### WebSocket Routes WebSocket routes are exposed with the same path as API routes. For example, `src/server/routes/api/ws/chat` is exposed as `ws://example.com/api/ws/chat`. ## Defining a Server-sent Event Handler Server-sent event handlers can be created using `createEventStream` function in the event handler. ```typescript // src/server/routes/api/sse.ts export default defineEventHandler(async (event) => { const eventStream = createEventStream(event); const interval = setInterval(async () => { await eventStream.push(`Message @ ${new Date().toLocaleTimeString()}`); }, 1000); eventStream.onClosed(async () => { clearInterval(interval); await eventStream.close(); }); return eventStream.send(); }); ``` ## More Info WebSockets are powered by [Nitro](https://nitro.unjs.io/guide/websocket), [h3](https://h3.unjs.io/guide/websocket) and [crossws](https://crossws.unjs.io/guide). See the Nitro, h3 and crossws docs for more details. --- # Overview URL: https://analogjs.org/docs/features/data-fetching/overview # Overview Data fetching in Analog builds on top of concepts in Angular, such as using `HttpClient` for making API requests. To call typed server-side code directly instead of writing an API route and a client request for it, see [Server Functions](/docs/features/data-fetching/server-functions). ## Using HttpClient Using `HttpClient` is the recommended way to make API requests for internal and external endpoints. The context for the request is provided by the `provideServerContext` function for any request that uses `HttpClient` and begins with a `/`. ## Server Request Context On the server, use the `provideServerContext` function from the Analog router in the `main.server.ts`. ```ts // Analog server context if (import.meta.env.PROD) { enableProdMode(); } export function bootstrap() { return bootstrapApplication(AppComponent, config); } export default async function render( url: string, document: string, serverContext: ServerContext, ) { const html = await renderApplication(bootstrap, { document, url, platformProviders: [provideServerContext(serverContext)], }); return html; } ``` This provides the `Request` and `Response`, and `Base URL` from the server and registers them as providers that can be injected and used. ## Injection Functions ```ts class MyService { request = injectRequest(); // <- Server Request Object response = injectResponse(); // <- Server Response Object baseUrl = injectBaseURL(); // <-- Server Base URL } ``` ## Request Context Interceptor Analog also provides `requestContextInterceptor` for the HttpClient that handles transforming any request to URL beginning with a `/` to a full URL request on the server, client, and during prerendering. Use it with the `withInterceptors` function from the `@angular/common/http` packages. ```ts export const appConfig: ApplicationConfig = { providers: [ provideFileRouter(withNavigationErrorHandler(console.error)), provideHttpClient( withFetch(), withInterceptors([requestContextInterceptor]), ), provideClientHydration(), ], }; ``` > Make sure the `requestContextInterceptor` is **last** in the array of interceptors. ## Making Requests In your component/service, use `HttpClient` along with [API routes](/docs/features/api/overview) with providing a full URL. An example API route that fetches todos. ```ts // src/server/routes/api/v1/todos.ts -> /api/v1/todos export default eventHandler(async () => { const response = await fetch('https://jsonplaceholder.typicode.com/todos'); const todos = await response.json(); return todos; }); ``` An example service that fetches todos from the API endpoint. ```ts // todos.service.ts @Injectable({ providedIn: 'root', }) export class TodosService { http = inject(HttpClient); getAll() { return this.http.get('/api/v1/todos'); } getData() { return this.http.get('/assets/data.json'); } } ``` Data requests also use Angular's `TransferState` to store any requests made during Server-Side Rendering, and are transferred to prevent an additional request during the initial client-side hydration. --- # Server Functions URL: https://analogjs.org/docs/features/data-fetching/server-functions # Server Functions Server functions are typed, validated operations that run on the server and are called from anywhere in your app (a component, an effect, a route resolver) with full type inference on both ends. They are defined in a `.server.ts` file, so their code and dependencies never reach the browser bundle. Where `load` fetches data for one page and `action` handles one form post, a server function is an arbitrary operation you can call by name. :::info Server functions require Angular v19 or higher, as the client half is built on `resource()`. ::: ## Defining a Server Function Server functions are defined with `serverFn` from `@analogjs/router/server` in any `.server.ts` file under `src`. They can live alongside an existing `load` or `action`. ```ts // src/app/server-fns/products.server.ts import { serverFn } from '@analogjs/router/server'; import { inject } from '@angular/core'; import * as v from 'valibot'; import { CatalogService } from './catalog.service'; // A handler alone is an input-less read, served over GET. export const getProducts = serverFn(async () => { return inject(CatalogService).list(); }); // A validation schema in front of the handler declares validated input, served over POST. export const getProduct = serverFn( v.object({ id: v.string() }), async (input) => { return inject(CatalogService).find(input.id); }, ); ``` Pass a config object as the first argument when you need to override a default: ```ts export const search = serverFn( { method: 'POST', input: v.object({ term: v.string() }) }, async (input) => inject(CatalogService).search(input.term), ); ``` The `input` schema is any [Standard Schema](https://standardschema.dev) validator; valibot, zod, and arktype all conform. It runs on the server before the handler, and invalid input is rejected without the handler ever running. Input travels in the request body, so any server function that takes input uses `POST`. `GET` is reserved for input-less reads, where it buys HTTP and CDN cacheability. ## Calling a Server Function Import the same exported function in a component and read it with `injectServerFn`, which returns an Angular [`resource`](https://angular.dev/guide/signals/resource): ```ts // src/app/pages/products.page.ts import { Component, input } from '@angular/core'; import { injectServerFn } from '@analogjs/router'; import { getProduct } from '../server-fns/products.server'; @Component({ selector: 'app-product-card', template: ` @if (product.value(); as p) {

{{ p.name }}

} @else if (product.error()) {

Could not load this product.

} @else {

Loading…

} `, }) export default class ProductCard { id = input.required(); protected product = injectServerFn(getProduct, () => ({ id: this.id() })); } ``` The args factory is reactive: the resource refetches whenever a signal it reads changes. Return `undefined` from it to leave the resource idle, which is how you express a read that is waiting on an input that isn't available yet. For writes, use `injectServerFnMutation`, which returns a callable bound to the current injector: ```ts import { injectServerFnMutation } from '@analogjs/router'; export default class Checkout { private placeOrder = injectServerFnMutation(placeOrderFn); async submit(sku: string, qty: number) { const { orderId } = await this.placeOrder({ sku, qty }); } } ``` Both helpers must be called in an injection context, and both dispatch through `HttpClient`, so client `HttpInterceptorFn`s apply and `HttpTestingController` works in tests. ### Hydration A read resolved while rendering on the server is transferred to the client and used as the resource's first value, so the browser does not refetch it on hydration. This works for `GET` and `POST` reads alike and needs no transfer cache configuration. During server-side rendering, calls skip HTTP entirely and run in-process in the same request injector as the render. ## Using Dependency Injection A server function handler runs inside the request injector Analog builds for server-side rendering, so `inject()` works at the top of the handler body, for your own services and for Analog's request tokens. ```ts import { serverFn } from '@analogjs/router/server'; import { REQUEST } from '@analogjs/router/tokens'; import { inject } from '@angular/core'; export const getGreeting = serverFn(async () => { const req = inject(REQUEST); return `Hello from ${req.headers['user-agent']}`; }); ``` `REQUEST`, `RESPONSE`, and `BASE_URL` are always available. `LOCALE` is provided only when a locale can be detected from the URL prefix or the `Accept-Language` header, so read it with `inject(LOCALE, { optional: true })`. The raw h3 event is deliberately not exposed, which keeps handlers testable by overriding those tokens. Handlers resolve dependencies from **your app's own server config**. There is no separate provider list to maintain. The dispatch endpoint bootstraps the application from `app.config.server.ts` (the same config `main.server.ts` renders with), so anything the app configures is available in a handler exactly as it is inside a component during SSR: `providedIn: 'root'` services, tokens bound with `useValue`, and app-level providers alike. A `providedIn: 'root'` service just works with no registration at all. ## Adding Interceptors Interceptors are functional, provided through DI, and apply to every server function in registration order, the same model as `HttpInterceptorFn`. Use them for authentication, tenancy, and logging. ```ts // src/app/server-fns/auth.interceptor.ts import type { ServerFnInterceptorFn } from '@analogjs/router/server'; import { fail } from '@analogjs/router/server/actions'; import { inject } from '@angular/core'; export const authInterceptor: ServerFnInterceptorFn = (ctx, next) => { const session = inject(SessionService); if (!session.user()) { return fail(401, { message: 'unauthenticated' }); } return next(ctx.with({ user: session.user() })); }; ``` Register them in your server config with `provideServerFns`, the same file the handlers bootstrap from: ```ts // src/app/app.config.server.ts import { mergeApplicationConfig } from '@angular/core'; import { provideServerRendering } from '@angular/platform-server'; import { provideServerFns, withServerFnInterceptors, } from '@analogjs/router/server'; import { appConfig } from './app.config'; import { authInterceptor } from './server-fns/auth.interceptor'; export const config = mergeApplicationConfig(appConfig, { providers: [ provideServerRendering(), provideServerFns(withServerFnInterceptors([authInterceptor])), ], }); ``` `provideServerFns` is server-only, so it belongs in `app.config.server.ts`, not the shared `app.config.ts`. Register the same way with `withAllowedOrigins([...])` when you need cross-origin access. `ctx.with({ ... })` accumulates a context object down the chain and delivers it to the handler as its second argument. Extend the `ServerFnContext` interface by declaration merging to type what your interceptors add. ## Handling Errors Return `fail(status, errors)` from a handler or an interceptor to stop the call and respond with an error, and `redirect(url)` to redirect. Both come from `@analogjs/router/server/actions` and are the same helpers used by form actions. ```ts export const placeOrder = serverFn(orderSchema, async (input) => { const result = await inject(OrderService).place(input); if (!result.ok) { return fail(409, { reason: result.reason }); } return { orderId: result.id }; }); ``` On the client, failures surface as an `HttpErrorResponse` on `resource.error()`, or as a rejected promise from a mutation. ## Security Server functions are HTTP endpoints, and validation checks the shape of the input, not the caller's authority. Authorization belongs in an interceptor or in the handler itself. Two protections are built in: - **Route ids are derived at build time** from the file and export name, not chosen by you. Each function is served from an opaque `/_analog/fn/` route, so two functions sharing a name cannot collide, and the endpoint surface cannot be enumerated by guessing export names. Treat the id as an opaque address rather than a secret. It is reproducible from the source and present in the client bundle, so it is not an authorization boundary. - **Calls are same-origin by default.** Because server functions are often cookie-authenticated, cross-origin browser calls are rejected with a `403` before the function is even looked up, and input-bearing calls must send a JSON body. If an app genuinely needs to be called from another origin, opt in explicitly in `app.config.server.ts`: ```ts import { provideServerFns, withAllowedOrigins } from '@analogjs/router/server'; export const config = mergeApplicationConfig(appConfig, { providers: [ provideServerRendering(), provideServerFns(withAllowedOrigins(['https://admin.example.com'])), ], }); ``` --- # Server-Side Data Fetching URL: https://analogjs.org/docs/features/data-fetching/server-side-data-fetching # Server-Side Data Fetching Analog supports fetching data from the server before loading a page. This can be achieved by defining an async `load` function in `.server.ts` file of the page. ## Fetching the Data To fetch the data from the server, create a `.server.ts` file that contains the async `load` function alongside the `.page.ts` file. ```ts // src/app/pages/index.server.ts import { PageServerLoad } from '@analogjs/router'; export const load = async ({ params, // params/queryParams from the request req, // H3 Request res, // H3 Response handler fetch, // internal fetch for direct API calls, event, // full request event }: PageServerLoad) => { return { loaded: true, }; }; ``` ## Validating Params and Query Use `definePageLoad` from `@analogjs/router/server/actions` to validate the route params and query with a [Standard Schema](https://standardschema.dev) before the handler runs. The validated values are typed in the handler, and a validation failure responds with a `422` status and the schema issues. ```ts // src/app/pages/users/[id].server.ts import { definePageLoad } from '@analogjs/router/server/actions'; import * as v from 'valibot'; export const load = definePageLoad({ params: v.object({ id: v.pipe(v.string(), v.regex(/^\d+$/)) }), handler: async ({ params, fetch }) => { return fetch(`/api/v1/users/${params.id}`); }, }); ``` ## Injecting the Data Accessing the data fetched on the server can be done using the `injectLoad` function provided by `@analogjs/router`. The `load` function is resolved using Angular route resolvers, so setting `requireSync: false` and `initialValue: {}` offers no advantage, as load is fetched before the component is instantiated. ```ts // src/app/pages/index.page.ts import { Component } from '@angular/core'; import { toSignal } from '@angular/core/rxjs-interop'; import { injectLoad } from '@analogjs/router'; import { load } from './index.server'; // not included in client build @Component({ standalone: true, template: `

Home

Loaded: {{ data().loaded }} `, }) export default class BlogComponent { data = toSignal(injectLoad(), { requireSync: true }); } ``` Accessing the data can also be done with Component Inputs and Component Input Bindings provided in the Angular Router configuration. To configure the Angular Router for `Component Input Bindings`, add `withComponentInputBinding()` to the arguments passed to `provideFileRouter()` in the `app.config.ts`. ```ts import { provideHttpClient } from '@angular/common/http'; import { ApplicationConfig } from '@angular/core'; import { provideClientHydration } from '@angular/platform-browser'; import { provideFileRouter } from '@analogjs/router'; import { withNavigationErrorHandler } from '@angular/router'; export const appConfig: ApplicationConfig = { providers: [ provideFileRouter( withComponentInputBinding(), withNavigationErrorHandler(console.error), ), provideHttpClient(), provideClientHydration(), ], }; ``` Now to get the data in the component add an input called `load`. ```ts // src/app/pages/index.page.ts import { Component } from '@angular/core'; import { LoadResult } from '@analogjs/router'; import { load } from './index.server'; // not included in client build @Component({ standalone: true, template: `

Home

Loaded: {{ data.loaded }} `, }) export default class BlogComponent { @Input() load(data: LoadResult) { this.data = data; } data!: LoadResult; } ``` ## Accessing to the server load data Accessing to the server load data from `RouteMeta` resolver can be done using the `getLoadResolver` function provided by `@analogjs/router`. ```ts import { getLoadResolver } from '@analogjs/router'; export const routeMeta: RouteMeta = { resolve: { data: async (route) => { // call server load resolver for this route from another resolver const data = await getLoadResolver(route); return { ...data }; }, }, }; ``` ## Overriding the Public Base URL Analog automatically infers the public base URL to be set when using the server-side data fetching through its [Server Request Context](/docs/features/data-fetching/overview#server-request-context) and [Request Context Interceptor](/docs/features/data-fetching/overview#request-context-interceptor). To explcitly set the base URL, set an environment variable, using a `.env` file to define the public base URL. ``` // .env VITE_ANALOG_PUBLIC_BASE_URL="http://localhost:5173" ``` The environment variable must also be set when building for deployment. --- # Deployment URL: https://analogjs.org/docs/features/deployment/overview # Deployment Node.js deployment is the default Analog output preset for production builds. When running `npm run build` with the default preset, the result will be an entry point that launches a ready-to-run Node server. To start up the standalone server, run: ```bash $ node dist/analog/server/index.mjs Listening on http://localhost:3000 ``` ### Environment Variables You can customize server behavior using following environment variables: - `NITRO_PORT` or `PORT` (defaults to `3000`) - `NITRO_HOST` or `HOST` ## Built-in Presets Analog can generate different output formats suitable for different [hosting providers](/docs/features/deployment/providers) from the same code base, you can change the deploy preset using an environment variable or `vite.config.ts`. Using environment variable is recommended for deployments depending on CI/CD. **Example:** Using `BUILD_PRESET` ```bash BUILD_PRESET=node-server ``` **Example:** Using `vite.config.ts` ```ts export default defineConfig({ plugins: [ analog({ nitro: { preset: 'node-server', }, }), ], }); ``` ## Deploying with a Custom URL Prefix If you are deploying with a custom URL prefix, such as https://domain.com/ `basehref` you must do these steps for [server-side-data-fetching](https://analogjs.org/docs/features/data-fetching/server-side-data-fetching), [html markup and assets](https://angular.io/api/common/APP_BASE_HREF), and [dynamic api routes](https://analogjs.org/docs/features/api/overview) to work correctly on the specified `basehref`. 1. Update your `vite.config.ts` file. ```ts export default defineConfig(({ mode }) => ({ base: '/basehref', plugins: [ analog({ ...(mode === 'production' ? { apiPrefix: 'basehref' } : { apiPrefix: 'basehref/api' }), prerender: { routes: async () => { return []; }, }, }), ], })); ``` 2. Update the `app.config.ts` file. This instructs Angular on how recognize and generate URLs. ```ts export const appConfig: ApplicationConfig = { providers: [ [{ provide: APP_BASE_HREF, useValue: import.meta.env.BASE_URL || '/' }], ... ], }; ``` 3. HttpClient use `injectAPIPrefix()`. ```ts const response = await firstValueFrom( this.httpClient.get<{ client: string }>(`${injectAPIPrefix()}/v1/hello`), ); ``` 4. In CI production build **Do not** set `VITE_ANALOG_PUBLIC_BASE_URL` it should use what is in `vite.config.ts`. If `VITE_ANALOG_PUBLIC_BASE_URL` is present during build, ssr data will be refetched on server and client. ```bash # if using nx: npx nx run appname:build:production # if using angular build directly: npx vite build && NITRO_APP_BASE_URL='/basehref/' node dist/analog/server/index.mjs ``` 5. In production containers specify the env flag `NITRO_APP_BASE_URL`. ```bash NITRO_APP_BASE_URL="/basehref/" ``` 6. Preview locally: ```bash npx vite build && NITRO_APP_BASE_URL='/basehref/' node dist/analog/server/index.mjs ``` --- # Providers URL: https://analogjs.org/docs/features/deployment/providers # Providers Analog supports deployment to many providers with little or no additional configuration using [Nitro](https://nitro.unjs.io) as its underlying server engine. You can find more providers in the [Nitro deployment docs](https://nitro.unjs.io/deploy). ## Zerops :::info [Zerops](https://zerops.io) is the **official** deployment partner for AnalogJS. ::: Analog supports deploying both static and server-side rendered apps to [Zerops](https://zerops.io) with a simple configuration file. > One Zerops project can contain multiple Analog projects. ### Deploy with one click The fastest way to get started. Zerops has ready-to-deploy Analog recipes with the correct infrastructure and configuration already set up. Go to [app.zerops.io/recipes?lf=analog](https://app.zerops.io/recipes?lf=analog) and pick the variant that matches your app: - **Analog SSR Hello World**: Node.js + PostgreSQL, server-side rendered - **Analog Static Hello World**: Static site compiled by Vite, served by Nginx Hit **Deploy**. If you don't have a Zerops account yet, it gets created in the same step. --- ### Static (SSG) Analog app If your project is not SSG Ready, set up your project for [Static Site Generation](/docs/features/server/static-site-generation). #### 1. Create a project in Zerops Projects and services can be added either through a [Project add](https://app.zerops.io/dashboard/project-add) wizard or imported using a YAML structure: ```yaml project: name: recipe-analog services: - hostname: app type: static ``` This creates a project called `recipe-analog` with a Zerops Static service called `app`. #### 2. Add zerops.yaml configuration To tell Zerops how to build and run your site, add a `zerops.yaml` to your repository: ```yaml zerops: - setup: app build: base: nodejs@22 buildCommands: - pnpm i - pnpm build deployFiles: - public - dist/analog/public/~ run: base: static ``` #### 3. [Trigger the build & deploy pipeline](#build--deploy-your-code) ### Server-side rendered (SSR) Analog app If your project is not SSR Ready, set up your project for [Server Side Rendering](/docs/features/server/server-side-rendering). #### 1. Create a project in Zerops Projects and services can be added either through a [Project add](https://app.zerops.io/dashboard/project-add) wizard or imported using a YAML structure: ```yaml project: name: recipe-analog services: - hostname: app type: nodejs@22 ``` This creates a project called `recipe-analog` with a Zerops Node.js service called `app`. #### 2. Add zerops.yaml configuration To tell Zerops how to build and run your site, add a `zerops.yaml` to your repository: ```yaml zerops: - setup: app build: base: nodejs@22 buildCommands: - pnpm i - pnpm build deployFiles: - dist run: base: nodejs@22 ports: - port: 3000 httpSupport: true start: node dist/analog/server/index.mjs ``` #### 3. [Trigger the build & deploy pipeline](#build-deploy-your-code) --- ### Build & deploy your code #### Connect your GitHub / GitLab repository Your code deploys automatically on every commit or tag. Connect your repository in the service detail - see [GitHub Integration](https://docs.zerops.io/references/github-integration) / [GitLab Integration](https://docs.zerops.io/references/gitlab-integration) for more. #### Trigger a deploy with Zerops CLI (zcli) You can also trigger deploys manually from your terminal or existing CI/CD. 1. Install the Zerops CLI: ```bash # To download the zcli binary directly, # use https://github.com/zeropsio/zcli/releases npm i -g @zerops/zcli ``` 2. Open [Settings > Access Token Management](https://app.zerops.io/settings/token-management) in the Zerops app and generate a new access token. 3. Log in using your access token: ```bash zcli login ``` 4. Navigate to the root of your app (where `zerops.yaml` is located) and run: ```bash zcli push ``` ## Netlify Analog supports deploying on [Netlify](https://netlify.com/) with no additional configuration. ### Deploying the project Configuration is easiest when using [Netlify CLI](https://developers.netlify.com/cli/). 1. Start by running this command: ```bash npx netlify init ``` If this is a new Netlify project, you'll be prompted to initialize it; build settings will be automatically configured in a `netlify.toml` file. 2. Deploy your app: ```bash npx netlify deploy ``` #### Manual configuration Alternatively, you can configure your project's build settings in the Netlify app. Set the [publish directory](https://docs.netlify.com/configure-builds/overview/#definitions) to `dist/analog/public` to deploy the static assets and the [functions directory](https://docs.netlify.com/configure-builds/overview/#definitions) to `netlify/functions` to deploy the server. In the build settings of your Netlify project on the web UI, do the following. 1. Set the [build command](https://docs.netlify.com/configure-builds/overview/#definitions) to `nx build [your-project-name]` 2. Set the [publish directory](https://docs.netlify.com/configure-builds/overview/#definitions) to `dist/[your-project-name]/analog/public` to deploy the static assets 3. Set the [functions directory](https://docs.netlify.com/configure-builds/overview/#definitions) to `dist/[your-project-name]/analog` to deploy the server. You can also configure this by putting a `netlify.toml` at the root of your repository. Below is an example config. ```toml # replace "my-analog-app" with the name of the app you want to deploy [build] command = "nx build my-analog-app" publish = "dist/my-analog-app/analog/public" functions = "dist/my-analog-app/analog" ``` ## Vercel Analog supports deploying on [Vercel](https://vercel.com/) with no additional configuration. ### Deploying the Project By default, when deploying to Vercel, the build preset is handled automatically. 1. Create a new project and select the repository that contains your code. 2. Click 'Deploy'. And that's it! In order to make it work with Nx, we need to define the specific app we want to build. There are several ways to do this, and you can choose one of the following methods (replace <app> with your app name): 1. Define the `defaultProject` in your `nx.json` ```json [nx.json] { "defaultProject": "" } ``` 2. Create a `vercel.json` file in the root of your project and define the `buildCommand`: ```json [vercel.json] { "$schema": "https://openapi.vercel.sh/vercel.json", "buildCommand": "nx build " } ``` 3. Define the `buildCommand` in your `package.json`: ```json [package.json] { "scripts": { "build": "nx build " } } ``` #### Nx and Vercel When using Nx and reusing the build cache on the Vercel build platform, there is a possibility that the cache is reused if you have built it locally. This can lead to the output being placed in the wrong location. To resolve this issue, you can use the preset in the `vite.config.ts` file as a workaround. ### Setting the Preset Manually There might be a case where Vercel doesn't load the preset automatically. In that case, you can do one of the following. - Set the `BUILD_PRESET` environment variable to `vercel`. - Set the preset in the `vite.config.ts` file: ```ts [vite.config.ts] // https://vitejs.dev/config/ export default defineConfig(({ mode }) => ({ /// ...other config plugins: [ analog({ nitro: { preset: 'vercel', }, }), ], })); ``` ## Cloudflare Pages Analog supports deploying to [Cloudflare](https://cloudflare.com/) Pages with minimal configuration. ### Deploying to Cloudflare To connect your repository and deploy automatically to Cloudflare: 1. Log in to the Cloudflare dashboard and select your account. 2. In Account Home, select Workers & Pages. 3. Select Create application > Pages > Connect to Git. 4. Enter `npm run build` as the `Build Command`. 5. Enter `dist/analog/public` as the `Build output directory`. 6. Leave the other default settings, click `Save and Deploy`. The application deploys to Cloudflare's network on each push to the repository. #### Nx and Cloudlfare For Nx workspaces, the build output is under the app name. Update the `Build output directory` accordingly. For example: Build output directory: `dist/[your-project-name]/analog/public` To test the build locally, run the following command: ```bash BUILD_PRESET=cloudflare-pages npx nx build [your-project-name] ``` ### Running the application locally using Wrangler You can also preview the application running on Cloudflare locally: 1. Set the environment variable `BUILD_PRESET` to `cloudflare-pages` before running the build ```bash BUILD_PRESET=cloudflare-pages npm run build ``` 2. Use the `wrangler` CLI to run the application locally ```bash npx wrangler pages dev ./dist/analog/public ``` ## Firebase App Hosting Analog supports [Firebase App Hosting](https://firebase.google.com/docs/app-hosting) with no additional configuration out of the box. **Note**: You need to be on the **Blaze plan** to deploy Analog applications with Firebase App Hosting. Follow the [Getting Started instructions](https://firebase.google.com/docs/app-hosting/get-started#step-1:) to connect your GitHub repository to Firebase App Hosting. ## Firebase Hosting Analog supports [Firebase Hosting](https://firebase.google.com/docs/hosting) with Cloud Functions and [Firebase App Hosting](https://firebase.google.com/docs/app-hosting) out of the box. See a [Sample Repo](https://github.com/brandonroberts/analog-angular-firebase-example) with Firebase configured **Note**: You need to be on the **Blaze plan** to use Analog with Cloud Functions. If you don't already have a `firebase.json` in your root directory, Analog will create one the first time you run it. In this file, you will need to replace `` with the ID of your Firebase project. This file should then be committed to version control. You can also create a `.firebaserc` file if you don't want to manually pass your project ID to your `firebase` commands (with `--project `): ```json [.firebaserc] { "projects": { "default": "" } } ``` Then, just add Firebase dependencies to your project: ```bash npm install -D firebase-admin firebase-functions firebase-functions-test ``` ### Using Firebase CLI If prefer to set up your project with the Firebase CLI, which will fetch your project ID for you, add required dependencies (see above) and even set up automated deployments with GitHub Actions. #### Install Firebase CLI globally ```bash npm install -g firebase-tools ``` **Note**: You need to be on [^11.18.0](https://github.com/firebase/firebase-tools/releases/tag/v11.18.0) to deploy a nodejs18 function. #### Initialize your Firebase project Login to Firebase and select the **Hosting** and **Functions** options as shown below: ```bash firebase login firebase init β—‰ Functions: Configure a Cloud Functions directory and its files β—‰ Hosting: Configure files for Firebase Hosting and (optionally) set up GitHub Action deploys ``` Unless you have an existing Firebase project, select **Create a new project** to continue. Firebase will provision a new project and provide the URL to access the web console to manage it. Once your project is created, select **TypeScript** as the language to use to write Cloud Functions. Proceed with accepting the default parameters by pressing _Enter._ When prompted for the **public directory**, enter `dist/analog/public`. In the next step, take the default option, N, on whether to configure as a **single-page app.** This is important! **Do not** configure your project as a single-page app. After setup completes, ensure that the following properties are configured correctly in your `firebase.json` file. This ensures server-side rendering will work correctly with Cloud Functions: ```json [firebase.json] { "functions": { "source": "dist/analog/server" }, "hosting": [ { "site": "", "public": "dist/analog/public", "cleanUrls": true, "rewrites": [ { "source": "**", "function": "server" } ] } ] } ``` You can find more details in the [Firebase documentation](https://firebase.google.com/docs/hosting/quickstart). ### Firebase functions Ensure that you set up Firebase functions as described in the previous section. Next, you must [configure Nitro](overview) correctly for Firebase Cloud Functions to work. In `vite.config.ts` update the `nitro` property with the configuration options that fit your needs, like your Node.js version and preferred region. ```js [vite.config.ts] nitro: { preset: 'firebase', firebase: { nodeVersion: '20', gen: 2, httpsOptions: { region: 'us-east1', maxInstances: 100, }, }, }, ``` ### Alternatively, multiple AnalogJS projects (/app1, /app2) in a single Firebase Hosting site This leverages cloud run services to host AnalogJS projects and uses rewrite rules for forwarding traffic from firebase to cloud run. [Deploying with a custom URL prefix](/docs/features/deployment/overview#deploying-with-a-custom-url-prefix). ```json [firebase.json] { "hosting": [ { "site": "", "public": "public", "cleanUrls": true, "rewrites": [ { "source": "/app1", "run": { "serviceId": "app1", "region": "us-central1", "pinTag": false } }, { "source": "/app1/**", "run": { "serviceId": "app1", "region": "us-central1", "pinTag": false } } ] } ] } ``` ### Local preview You can preview a local version of your site to test things out without deploying. ```bash BUILD_PRESET=firebase npm run build firebase emulators:start ``` ### Deploy to Firebase Hosting using the CLI To deploy to Firebase Hosting, run the `firebase deploy` command. ```bash BUILD_PRESET=firebase npm run build firebase deploy ``` ### Firebase Warnings When configuring or deploying Firebase you may see warnings like: ``` npm WARN EBADENGINE Unsupported engine { npm WARN EBADENGINE package: undefined, npm WARN EBADENGINE required: { node: '18' }, npm WARN EBADENGINE current: { node: 'v20.11.0', npm: '10.2.4' } npm WARN EBADENGINE } ``` ``` ⚠ functions: Couldn't find firebase-functions package in your source code. Have you run 'npm install'? ``` These are benign errors and can be ignored, so long as you make sure your environment configuration matches `Nitro`. ## Render.com Analog supports deploying on [Render](https://render.com/) with minimal configuration. ### Web Service Deployment 1. [Create a new Web Service](https://dashboard.render.com/select-repo?type=web) and select the repository that contains your code. 2. Ensure the 'Node' environment is selected. 3. [Specify your Node version for Render to use](https://render.com/docs/node-version) (v18.13.0 or higher recommended) - Render by default uses Node 14, which fails to correctly build an Analog site 4. Depending on your package manager, set the build command to `yarn && yarn build`, `npm install && npm run build`, or `pnpm i --shamefully-hoist && pnpm build`. 5. Update the start command to `node dist/analog/server/index.mjs` 6. Click 'Advanced' and add an environment variable with `BUILD_PRESET` set to `render-com`. 7. Click 'Create Web Service'. ### Static Site Deployment If using Analog to pre-render static content, you can deploy a static site on Render with minimal configuration 1. [Create a new Static Site](https://dashboard.render.com/select-repo?type=static) and select the repository that contains your code. 2. Depending on your package manager, set the build command to `yarn && yarn build`, `npm install && npm run build`, or `pnpm i --shamefully-hoist && pnpm build`.. 3. Set the publish directory to the `public` directory inside of the `dist` build directory (e.g. `dist/analog/public`) 4. Click 'Create Static Site' ## Edgio Analog supports deploying on [Edgio](https://edg.io) with minimal configuration. 1. Install the Edgio CLI: ```bash npm i -g @edgio/cli ``` 2. In your project's directory, initialize Edgio: ```bash edgio init --connector=@edgio/analogjs ``` 3. Deploy To Edgio ```bash edgio deploy ``` ## GitHub Pages (Static Site Deployment) Analog supports deploying a static site on [GitHub Pages](https://pages.github.com/). When deploying your site to GitHub Pages, you must add an empty file called `.nojekyll` in the root directory of the `gh-pages` branch. You can automate the deployment using the [Analog Publish Github Pages](https://github.com/marketplace/actions/analog-publish-github-pages) action: ```yaml name: Build and Deploy on: push: branches: - 'main' jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: '20.x' - uses: k9n-dev/analog-publish-gh-pages@v1.0.0 with: access-token: ${{ secrets.ACCESS_TOKEN }} # further options are available. # see: https://github.com/marketplace/actions/analog-publish-github-pages ``` Or you can do it by your own like this: ```yaml name: Build Deploy on: push: branches: - '*' # deploy on all branches (but a --dry-run flag is added for branches (see code below)) env: TARGET_DIR: dist/analog/public jobs: # build the project and push it to the gh-pages branch build-and-push: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: '20.x' - name: Set environment variable based on branch run: | if [[ $GITHUB_REF == refs/heads/main || $GITHUB_REF == refs/heads/master ]]; then echo "Branch is main or master. Setting DRY_RUN_OPTION to empty." echo "DRY_RUN_OPTION=" >> $GITHUB_ENV else echo "Branch is not main or master. Setting DRY_RUN_OPTION to '--dry-run'." echo "DRY_RUN_OPTION=--dry-run" >> $GITHUB_ENV fi - name: Install run: npm ci - name: Build run: npm run build - name: Deploy Website (gh-pages branch) env: GH_TOKEN: ${{ secrets.ACCESS_TOKEN }} # A token must be created to be able to deploy on the gh-pages branch CNAME_OPTION: --cname=yourdomain.dev # omit if your not running it on a custom domain # run the deployment script to push the built project to the gh-pages branch # the default contributor is github-actions[bot] run: | echo "DRY_RUN_OPTION=$DRY_RUN_OPTION" npx angular-cli-ghpages --no-silent --dir="${{env.TARGET_DIR}}" \ --name="github-actions[bot]" \ --email="github-actions[bot]@users.noreply.github.com" \ --branch="gh-pages" \ --message="Deploy: $(git log -1 --pretty=%B)" \ $DRY_RUN_OPTION # deploy from gh-pages branch deploy-pages: needs: build-and-push runs-on: ubuntu-latest environment: name: github-pages url: ${{ steps.deployment.outputs.page_url }} steps: - name: Checkout gh-pages uses: actions/checkout@v4 with: ref: gh-pages - name: Setup Pages uses: actions/configure-pages@v4 - name: Upload artifact uses: actions/upload-pages-artifact@v3 with: path: '.' - name: Deploy to GitHub Pages id: deployment uses: actions/deploy-pages@v4 ``` --- # Code Generation URL: https://analogjs.org/docs/features/generation/code-generation # Code Generation Analog supports automated code generation using Nx Generators and Angular Schematics. ### Generators The Analog plugin for Nx provides a series of generators that help automate some of the frequent tasks inside an Nx workspace, like generating an **application** or **page**. To use these generators, the **Nx Console** extension can be installed or they can be invoked manually using the Nx CLI: ### Generating an application To generate a new Analog application within an Nx workspace, use the application generator: ```shell npx nx generate @analogjs/platform:application my-app ``` ### Generating pages ```shell npx nx generate @analogjs/platform:page --pathname=index --project=analog-app ``` it also works with the Analog specific filenames, **Note: this names needs to be surrounded by single quotes** ex: ```shell npx nx generate @analogjs/platform:page --pathname='(blog)' --project=analog-app ``` The schematic as well accepts subfolders to structure our project properly. ```shell npx nx generate @analogjs/platform:page --pathname='products/[products]' --project=analog-app ``` ### Angular Schematics Analog provides a series of schematics that help automate some of the frequent tasks inside an Angular CLI workspace, like generating an **application** or a **page**. To use these schematics, use the generate command: ### Generating an application To generate a new Analog application within an Angular CLI workspace, use the application schematic: ```shell npx ng generate @analogjs/platform:application my-app ``` ### Generating pages ```shell npx ng g @analogjs/platform:page --pathname=index --project=/ ``` it also works with the Analog specific filenames, **Note: this names needs to be surrounded by single quotes** ex: ```shell npx ng g @analogjs/platform:page --pathname='(blog)' --project=/ ``` The schematic as well accepts subfolders to structure our project properly. ```shell npx ng g @analogjs/platform:page --pathname='products/[products]' --project=/ ``` --- # Internationalization (i18n) URL: https://analogjs.org/docs/features/i18n/overview # Internationalization (i18n) Analog supports runtime internationalization using Angular's built-in `$localize` system. This allows you to serve translated content with a single build, detecting the user's locale at runtime on both the server and client. ## Setup ### 1. Install `@angular/localize` Add the `@angular/localize` package to your project: ```bash npm install @angular/localize ``` ### 2. Initialize `$localize` Import the `$localize` polyfill before your application imports in both entry points, `src/main.ts` and `src/main.server.ts`: ```ts import '@angular/localize/init'; ``` ### 3. Create a shared translation loader Create translation files such as `src/i18n/fr.json` and `src/i18n/de.json`. Each file contains a message ID-to-string map: ```json { "greeting": "Bonjour", "farewell": "Au revoir" } ``` Export a loader from a module that does not import your application: ```ts // src/i18n.ts export default async function loadTranslations(locale: string) { switch (locale) { case 'fr': return (await import('./i18n/fr.json')).default; case 'de': return (await import('./i18n/de.json')).default; default: return {}; } } ``` The first entry in `locales` is the source language. Its messages are already in your templates, so the loader is not called for that locale. ### 4. Configure the platform Configure your locales and the loader module's path in `vite.config.ts`: ```ts import analog from '@analogjs/platform'; import { defineConfig } from 'vite'; export default defineConfig({ plugins: [ analog({ i18n: { defaultLocale: 'en', locales: ['en', 'fr', 'de'], loader: './src/i18n.ts', }, }), ], }); ``` The loader path enables [locale workers](#concurrent-server-rendering) automatically for supported production Node builds, including prerendering. ### 5. Register the runtime provider Pass the same loader function to `provideI18n()` for browser and development rendering: ```ts // src/app/app.config.ts import { ApplicationConfig } from '@angular/core'; import { provideFileRouter } from '@analogjs/router'; import { provideI18n } from '@analogjs/router/i18n'; import loadTranslations from '../i18n'; export const appConfig: ApplicationConfig = { providers: [provideFileRouter(), provideI18n({ loader: loadTranslations })], }; ``` `provideI18n()` reads locales from the platform configuration, or accepts them explicitly: | Property | Type | Description | | --------------- | ------------------------------------------------------------------------------- | -------------------------------------------------------------------------------- | | `defaultLocale` | `string` | Fallback locale; optional when configured in the platform | | `locales` | `string[]` | Supported locales, with the source language first; optional with platform config | | `loader` | `(locale: string) => Promise> \| Record` | Required function returning translations for a locale | ## Using Translations in Templates Use Angular's `i18n` attribute to mark text for translation: ```html

Hello

Goodbye

``` Or use `$localize` directly in component code: ```ts @Component({ selector: 'app-home', template: `

{{ title }}

`, }) export class HomeComponent { title = $localize`:@@greeting:Hello`; } ``` ## Locale Detection Analog detects the user's locale automatically in both SSR and client-only modes. ### Server-Side Rendering During SSR, the locale is detected from the incoming request using two strategies, in order of priority: 1. **URL path prefix**: a locale prefix in the URL path (e.g., `/fr/about` resolves to `fr`) 2. **`Accept-Language` header**: the browser's preferred language from the request headers ### Client-Only Mode When SSR is disabled (`ssr: false`), `provideI18n()` detects the locale from `window.location.pathname` by matching the first URL segment against the configured `locales` list. If no match is found, `defaultLocale` is used. ### Accessing the Current Locale The detected locale is available through the `LOCALE` injection token. Inject it anywhere in your application: ```ts @Component({ selector: 'app-language-switcher', template: `Current locale: {{ locale }}`, }) export class LanguageSwitcherComponent { locale = injectLocale(); } ``` ## URL-Based Locale Routing Locale detection chooses translations; it does not add locale segments to the file router. For `/en/about` and `/fr/about` to match a page, include that segment in your page structure. ### Explicit pages under `[locale]` Use a dynamic `[locale]` directory when each URL has its own page component: ```text src/app/pages/ β”œβ”€β”€ index.page.ts └── [locale]/ β”œβ”€β”€ index.page.ts β”œβ”€β”€ about.page.ts └── products/ └── [id].page.ts ``` | File | Route | Example URL | | -------------------------------- | ----------------------- | ----------------- | | `index.page.ts` | `/` | `/` | | `[locale]/index.page.ts` | `/:locale` | `/fr` | | `[locale]/about.page.ts` | `/:locale/about` | `/fr/about` | | `[locale]/products/[id].page.ts` | `/:locale/products/:id` | `/fr/products/42` | For example, the same about page renders in each locale: ```ts // src/app/pages/[locale]/about.page.ts import { Component } from '@angular/core'; @Component({ standalone: true, template: `

About us

`, }) export default class AboutPage {} ``` Configure the same supported locales in the platform's `i18n` options and `provideI18n()`. A dynamic route parameter accepts any segment: `[locale]` alone does not restrict URLs to your supported languages. Add application route guards if unsupported prefixes should redirect or return a not-found page. A componentless `[locale]` directory needs no layout file. If you add `[locale].page.ts` as a shared layout, import `RouterOutlet` from `@angular/router` and render `` for its child pages. ### Catch-all content routes For a content-driven site where one component resolves many slugs, use `[locale]/[...slug].page.ts`. It captures the remaining path, such as `/fr/guides/getting-started`, in one wildcard route. Your component must resolve the requested content and handle missing content; a catch-all does not create a separate component for each page. See [nested content routes](/docs/features/routing/content#hierarchical-nested-content) for the content-loading pattern. For ordinary application pages such as checkout, account, and product details, prefer the explicit page structure above. You do not need a catch-all solely to enable localization. ### Redirecting the root URL A common pattern is to redirect the root URL to the user's preferred locale: ```ts // src/app/pages/index.page.ts import { Component, inject } from '@angular/core'; import { Router } from '@angular/router'; import { injectLocale } from '@analogjs/router/tokens'; @Component({ template: '', }) export default class IndexPage { constructor() { const router = inject(Router); const locale = injectLocale() ?? 'en'; router.navigate([locale]); } } ``` ## Concurrent server rendering Angular's `$localize` state is shared within a JavaScript context, so overlapping SSR requests in different locales can produce mixed-language pages. Locale workers isolate each language while keeping requests concurrent. Keep the usual `render(App, config)` server entry. ### Worker selection The shared loader in [Setup](#setup) automatically enables workers when: - The resolved Nitro preset is `node-server` and SSR is enabled. - `i18n.loader` is configured with at least two locales. - Progressive Angular streaming, WebSockets, and scheduled tasks are disabled. Selection follows the deployment preset. To target a standalone Node server explicitly, set `nitro: { preset: 'node-server' }` in `analog()`. | `i18n.workers` | Behavior | | -------------- | -------------------------------------------------------------------- | | Omitted | Automatic when eligible; unsupported setups warn and fall back | | `false` | Disables workers | | `true` | Requires a supported worker configuration; fails the build otherwise | Existing configurations without `i18n.loader` are unchanged. To adopt workers, export your existing loader from a shared module, pass it to `provideI18n()`, and add its path to the platform configuration as shown in [Setup](#setup). Without workers, concurrent SSR requests in different locales remain unisolated. ### Deployment limits - Each locale worker uses additional memory and initializes its own Nitro plugins. - Workers use HTTP TCP. Terminate TLS at a reverse proxy; direct TLS and Unix sockets are unsupported. HTTP response streaming is supported. - H3's `getRequestIP()` uses the incoming connection address from `event.context.clientAddress`. Forwarded headers are preserved, but behind a trusted reverse proxy, read its forwarded address explicitly: H3 gives `clientAddress` precedence. - Shutdown drains requests for up to 30 seconds. An unexpected worker failure stops the server; use a process supervisor to restart it. ## Switching Locale at Runtime Angular's `$localize` resolves translations at template evaluation time, so switching locale requires a full page navigation to re-evaluate all templates with the correct translations. Use `injectSwitchLocale()` in your components. It reads the configured locales from `provideI18n()` automatically: ```ts @Component({ selector: 'app-language-switcher', template: `

Current: {{ locale }}

`, }) export class LanguageSwitcherComponent { locale = injectLocale(); switchLang = injectSwitchLocale(); } ``` Calling `switchLang('fr')` navigates from `/en/about` to `/fr/about` with a full page load. If no locale prefix exists in the current URL, the target locale is prepended. ### Low-Level: `loadTranslationsRuntime()` If you need to update the `$localize` translation map without a navigation (e.g., preloading translations), use `loadTranslationsRuntime()`: ```ts const translations = await fetch('/i18n/fr.json').then((r) => r.json()); loadTranslationsRuntime(translations); ``` :::info `loadTranslationsRuntime()` updates the translation map in memory, but components that have already rendered will not re-render. Use `switchLocale()` or `injectSwitchLocale()` for a full locale switch. ::: ## Extracting Messages Analog can extract i18n message IDs from your compiled build output. Enable extraction in the platform plugin options: ```ts // https://vitejs.dev/config/ export default defineConfig(({ mode }) => ({ plugins: [ analog({ i18n: { defaultLocale: 'en', locales: ['en', 'fr', 'de'], extract: { format: 'json', outFile: 'src/i18n/messages.json', }, }, }), ], })); ``` When `extract` is configured, a production build (`npm run build`) will scan the compiled JavaScript for `$localize` tagged templates and write a translation source file. ### Supported Formats | Format | Extension | Description | | -------- | --------- | ------------------------------- | | `json` | `.json` | Simple key-value JSON (default) | | `xliff` | `.xlf` | XLIFF 1.2 | | `xliff2` | `.xlf` | XLIFF 2.0 | | `xmb` | `.xmb` | XML Message Bundle | ### Extraction with `@angular/localize/tools` If `@angular/localize` is installed, Analog uses its `MessageExtractor` for accurate extraction with full source map support. If the package is not installed, a built-in regex-based extractor is used as a fallback. For the best results, install `@angular/localize`: ```bash npm install @angular/localize ``` ### Using Extracted Messages After extraction, use the generated file as a template for your translations. For example, with JSON format: ```json // src/i18n/messages.json (generated) { "greeting": "Hello", "farewell": "Goodbye" } ``` Copy this file for each locale and translate the values: ```json // src/i18n/fr.json { "greeting": "Bonjour", "farewell": "Au revoir" } ``` Then reference the translation files in your `provideI18n()` loader. ## Content i18n Analog's content system supports locale-aware content resolution for blogs, docs, and other markdown content. Add `withLocale()` to your `provideContent()` configuration: ```ts // src/app/app.config.ts export const appConfig: ApplicationConfig = { providers: [ provideFileRouter(), provideI18n({ defaultLocale: 'en', locales: ['en', 'fr', 'de'], loader: async (locale) => { const translations = await import(`../i18n/${locale}.json`); return translations.default; }, }), provideContent( withMarkdownRenderer(), withLocale({ loadLocale: injectLocale }), ), ], }; ``` ### Organizing Content by Locale Use locale subdirectories under `src/content/`: ``` src/content/ en/ blog/ my-post.md another-post.md fr/ blog/ my-post.md another-post.md blog/ shared-post.md ← no locale, shown for all locales ``` With this setup, `injectContentFiles()` and `injectContent()` automatically resolve to the correct locale: ```ts // Blog list: returns only posts for the active locale const posts = injectContentFiles((file) => file.filename.includes('/blog/'), ); // Blog detail: resolves /content/fr/blog/my-post.md when locale is 'fr' const post$ = injectContent({ param: 'slug', subdirectory: 'blog', }); ``` No locale-specific code is needed in components; the content APIs handle it internally. ### Frontmatter Locale Attribute Alternatively, set the locale in frontmatter instead of using subdirectories: ```markdown --- title: Mon article locale: fr slug: my-post --- ``` Files with a `locale` frontmatter attribute are filtered by that value. Files without a `locale` attribute and outside any locale subdirectory are treated as universal content and included for all locales. ### Prerendering Content Routes Use `PrerenderContentDir` with locale-aware transforms: ```ts analog({ i18n: { defaultLocale: 'en', locales: ['en', 'fr', 'de'], }, prerender: { routes: [ { contentDir: '/src/content', transform: (file) => { // file.path includes the locale: '/src/content/fr/blog' const segments = file.path.split('/').filter(Boolean); const localeIndex = segments.indexOf('content') + 1; const locale = segments[localeIndex]; const rest = segments.slice(localeIndex + 1).join('/'); return `/${locale}/${rest}/${file.attributes['slug'] || file.name}`; }, }, ], }, }); ``` ## Development Development uses the existing SSR path rather than fixed-locale workers. The Analog dev server provides: - **`` injection**: the `lang` attribute on the `` tag is set automatically based on the detected locale for each request. - **Translation file HMR**: editing translation files in `i18n/` directories (`.json`, `.xlf`, `.xmb`, `.arb`) triggers an automatic page reload so changes are reflected immediately. - **Locale-prefixed routes**: URLs like `http://localhost:5173/fr/about` work when your file routes include the locale segment, as shown above. The SSR middleware detects the locale and loads the correct translations. ## Prerendering When `i18n` is configured, prerendering generates locale-prefixed variants for each route. Locale workers support parallel prerendering and crawling; configure concurrency with `nitro.prerender.concurrency`. With `static: true`, workers run only during the build and the deployed output needs no Node server. ```ts // https://vitejs.dev/config/ export default defineConfig(({ mode }) => ({ plugins: [ analog({ i18n: { defaultLocale: 'en', locales: ['en', 'fr', 'de'], }, prerender: { routes: ['/', '/about', '/contact'], sitemap: { host: 'https://example.com', }, }, }), ], })); ``` This configuration will: 1. **Expand routes.** Each route is prerendered for every locale: `/en/about`, `/fr/about`, `/de/about`, etc. The unprefixed routes are also kept for the default locale. 2. **Set ``.** Each prerendered page receives the correct `lang` attribute (e.g., ``). 3. **Generate hreflang links in the sitemap.** The sitemap includes `` entries for each locale variant, plus an `x-default` entry pointing to the default locale. ## Platform Configuration Configure `i18n` in `analog()`: | Property | Type | Description | | --------------- | --------------------- | ---------------------------------------------------------------------------------------------------- | | `defaultLocale` | `string` | Locale used when no supported locale is selected | | `locales` | `string[]` | Supported locales; the first is the source language | | `loader` | `string` | Optional translation-loader module path relative to the app root; enables automatic worker selection | | `workers` | `boolean` | Omit for automatic selection, use `false` to opt out, or `true` to require workers | | `extract` | `{ format, outFile }` | Optional [message extraction](#extracting-messages) settings | --- # Content Routes URL: https://analogjs.org/docs/features/routing/content # Content Routes Analog also supports using markdown content as routes, and rendering markdown content in components. ### Setup In the `src/app/app.config.ts`, add the `provideContent()` function, along with the `withMarkdownRenderer()` feature to the `providers` array when bootstrapping the application. ```ts export const appConfig: ApplicationConfig = { providers: [ // ... other providers provideContent(withMarkdownRenderer()), ], }; ``` Next, enable the content package in the `vite.config.ts` ```ts /// // https://vitejs.dev/config/ export default defineConfig(({ mode }) => ({ plugins: [ analog({ // enable content/highlighter content: { highlighter: 'prism', }, }), ], })); ``` ## Defining Content Routes Content routes include support for frontmatter, metatags, and syntax highlighting with PrismJS. The example route below in `src/app/pages/about.md` defines an `/about` route. ```md --- title: About meta: - name: description content: About Page Description - property: og:title content: About --- ## About Analog Analog is a meta-framework for Angular. [Back Home](./) ``` ### PrismJS Syntax Highlighting Analog supports syntax highlighting with PrismJS. To enable syntax highlighting with `PrismJS`, add `withPrismHighlighter()` to the `provideContent()` function in `app.config.ts`. ```diff-ts + import { withPrismHighlighter } from '@analogjs/content/prism-highlighter'; export const appConfig: ApplicationConfig = { providers: [ // ... other providers - provideContent(withMarkdownRenderer()), + provideContent(withMarkdownRenderer(), withPrismHighlighter()), ], }; ``` Import a Prism theme into to your global stylesheet: ```css @import 'prismjs/themes/prism.css'; ``` #### Using the `diff` Highlight Plugin Analog supports highlighting diff changes with PrismJS. Add the `prism-diff` language to the `additionalLangs` in the `analog` plugin: ```ts export default defineConfig({ // ... plugins: [ analog({ content: { highlighter: 'prism', prismOptions: { additionalLangs: ['prism-diff'], }, }, }), ], }); ``` Add the `diff-highlight` plugin import to the `app.config.ts`: ```ts + import { withShikiHighlighter } from '@analogjs/content/shiki-highlighter'; export const appConfig: ApplicationConfig = { providers: [ // ... other providers - provideContent(withMarkdownRenderer()), + provideContent(withMarkdownRenderer(), withShikiHighlighter()), ], }; ``` To enable build-time syntax highlighting with `shiki`, configure the `analog` plugin in the `vite.config.ts`. ```ts export default defineConfig({ // ... plugins: [ analog({ content: { highlighter: 'shiki', }, }), ], }); ``` #### Configure Shiki Highlighter > Please check out [Shiki Documentation](https://shiki.style/) for more information on configuring Shiki. To configure Shiki, you can pass options to the `shikiOptions` object. ```ts export default defineConfig({ // ... plugins: [ analog({ content: { highlighter: 'shiki', shikiOptions: { highlight: { // alternate theme theme: 'ayu-dark', }, highlighter: { // add more languages for Shiki itself additionalLangs: ['diff'], }, }, }, }), ], }); ``` For Mermaid-heavy content, keep the existing `loadMermaid` runtime path and skip Mermaid grammar loading in Shiki to avoid unnecessary server-side highlighting work in constrained CI environments: ```ts export default defineConfig({ plugins: [ analog({ content: { highlighter: 'shiki', shikiOptions: { highlighter: { additionalLangs: ['mermaid'], skipLangs: ['mermaid'], }, }, }, }), ], }); ``` With `skipLangs: ['mermaid']`, Analog keeps Mermaid blocks on the existing `
` path for `loadMermaid`, while Shiki skips loading and tokenizing the Mermaid grammar.

By default, `shikiOptions` has the following options.

```ts
{
  "container": "%s",
  "highlight": {
    "theme": "github-dark"
  }
  "highlighter": {
    "langs": [
      "json",
      "ts",
      "tsx",
      "js",
      "jsx",
      "html",
      "css",
      "angular-html",
      "angular-ts",
    ],
    "themes": ["github-dark", "github-light"]
  }
}
```

## Defining Content Files

For more flexibility, markdown content files can be provided in the `src/content` folder. Here you can list markdown files such as blog posts.

```md
---
title: My First Post
slug: 2022-12-27-my-first-post
description: My First Post Description
coverImage: https://images.unsplash.com/photo-1493612276216-ee3925520721?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=464&q=80
---

Hello World
```

## Using the Content Files List

To get a list using the list of content files in the `src/content` folder, use the `injectContentFiles(filterFn?: InjectContentFilesFilterFunction)` function from the `@analogjs/content` package in your component. To narrow the files, you can use the `filterFn` predicate function as an argument. You can use the `InjectContentFilesFilterFunction` type to set up your predicate.

> `injectContentFiles` returns metadata only: `filename`, `slug`, and `attributes`. The `content` body is not loaded; reading it from the returned items yields `undefined`. Use `injectContent` to load and render an individual file's body.

```ts
export interface PostAttributes {
  title: string;
  slug: string;
  description: string;
  coverImage: string;
}

@Component({
  imports: [RouterOutlet, RouterLink],
  template: `
    
  `,
})
export default class BlogComponent {
  readonly posts = injectContentFiles((contentFile) =>
    contentFile.filename.includes('/src/content/blog/'),
  );
}
```

## Using the Analog Markdown Component

Analog provides a `MarkdownComponent` and `injectContent()` function for rendering markdown content with frontmatter.

The `injectContent()` function uses the `slug` route parameter by default to get the content file from the `src/content` folder.

```ts
// /src/app/pages/blog/posts.[slug].page.ts
export interface PostAttributes {
  title: string;
  slug: string;
  description: string;
  coverImage: string;
}

@Component({
  imports: [MarkdownComponent, AsyncPipe],
  template: `
    @if (post$ | async; as post) {
      

{{ post.attributes.title }}

} `, }) export default class BlogPostComponent { readonly post$ = injectContent(); } ``` ### Using A Resolver For Metatags In your route configuration, you can use the `RouteMeta` object to resolve meta tags for a route. This is done by assigning the `postMetaResolver` function to the `meta` property. Below is an example of using a `postMetaResolver` function that fetches the meta tags for a post. This function returns an array of meta tags. ```ts export const postMetaResolver: ResolveFn = (route) => { const postAttributes = injectActivePostAttributes(route); return [ { name: 'description', content: postAttributes.description, }, { name: 'author', content: 'Analog Team', }, { property: 'og:title', content: postAttributes.title, }, { property: 'og:description', content: postAttributes.description, }, { property: 'og:image', content: postAttributes.coverImage, }, ]; }; ``` The meta tags can be done asynchronously also. Assign the `postMetaResolver` function to the `meta` property. ```ts export const routeMeta: RouteMeta = { title: postTitleResolver, meta: postMetaResolver, }; ``` The resolved meta tags can also be accessed in the component using the `ActivatedRoute` service. ```ts export default class BlogPostComponent { readonly route = inject(ActivatedRoute); readonly metaTags$ = this.route.data.pipe(map(data => data['meta'])); // In the template } ``` ### Enabling support for Mermaid Analog's markdown component supports [Mermaid](https://mermaid.js.org/). To enable support by the `MarkdownComponent` define a dynamic import for `loadMermaid` in `withMarkdownRenderer()`. ```ts withMarkdownRenderer({ loadMermaid: () => import('mermaid'), }); ``` After it is enabled, Mermaid blocks are transformed by mermaid into SVGs. Example of mermaid graph: ```mermaid graph TD A[Before] -->|Playing with AnalogJS| B(Now Yes !) ``` ## Support for Content Subdirectories Analog also supports subdirectories within your content folder. The `injectContent()` function can also be used with an object that contains the route parameter and subdirectory name. This can be useful if, for instance, you have blog posts, as well as a portfolio of project markdown files to be used on the site. ```treeview src/ └── app/ β”‚ └── pages/ β”‚ └── project.[slug].page.ts └── content/ β”œβ”€β”€ posts/ β”‚ β”œβ”€β”€ my-first-post.md β”‚ └── my-second-post.md └── projects/ β”œβ”€β”€ my-first-project.md └── my-second-project.md ``` ```ts // /src/app/pages/project.[slug].page.ts export interface ProjectAttributes { title: string; slug: string; description: string; coverImage: string; } @Component({ imports: [MarkdownComponent, AsyncPipe], template: ` @if (project$ | async; as project) {

{{ project.attributes.title }}

} `, }) export default class ProjectComponent { readonly project$ = injectContent({ param: 'slug', subdirectory: 'projects', }); } ``` ### Hierarchical (Nested) Content When content is organized into category subdirectories under a `subdirectory`, pair `injectContent({ param, subdirectory })` with a catch-all route. The catch-all parameter captures the full path under the subdirectory and resolves to the matching nested file. ```treeview src/ └── app/ β”‚ └── pages/ β”‚ └── docs/ β”‚ └── [...slug].page.ts └── content/ └── docs/ β”œβ”€β”€ getting-started/ β”‚ β”œβ”€β”€ welcome.md β”‚ └── first-upload.md └── assets/ └── upload.md ``` ```ts // /src/app/pages/docs/[...slug].page.ts export interface DocAttributes { title: string; } @Component({ imports: [MarkdownComponent, AsyncPipe], template: ` @if (doc$ | async; as doc) {

{{ doc.attributes.title }}

} `, }) export default class DocComponent { readonly doc$ = injectContent({ param: 'slug', subdirectory: 'docs', }); } ``` A request for `/docs/getting-started/welcome` resolves to `src/content/docs/getting-started/welcome.md`. Frontmatter `slug` is optional in this layout; when omitted, the file is keyed by its on-disk path. If a file does specify a `slug` containing path separators, that slug is interpreted relative to its top-level subdirectory (e.g. `slug: getting-started/welcome` on a file under `src/content/docs/` resolves to `src/content/docs/getting-started/welcome`). ## Loading Custom Content By default, Analog uses the route params to build the filename for retrieving a content file from the `src/content` folder. Analog also supports using a custom filename for retrieving content from the `src/content` folder. This can be useful if, for instance, you have a custom markdown file that you want to load on a page. The `injectContent()` function can be used by passing an object that contains the `customFilename` property. ```ts readonly post$ = injectContent({ customFilename: 'path/to/custom/file', }); ``` --- # Route Metadata URL: https://analogjs.org/docs/features/routing/metadata # Route Metadata Additional metadata to add to the generated route config for each route can be done using the `RouteMeta` type. This is where you can define the page title, any necessary guards, resolvers, providers, and more. ## Defining Route Metadata ```ts export const routeMeta: RouteMeta = { title: 'About Analog', canActivate: [() => true], providers: [AboutService], }; @Component({ template: `

Hello Analog

Analog is a meta-framework on top of Angular. `, }) export default class AboutPageComponent { private readonly service = inject(AboutService); } ``` ## Redirect Routes Routes can be defined for the sole purpose of redirecting to another route. To create a redirect route, add the `redirectTo` and `pathMatch` properties to the `routeMeta` object inside the route file: ```ts // src/app/pages/index.page.ts export const routeMeta: RouteMeta = { redirectTo: '/home', pathMatch: 'full', }; ``` The example above is a redirect from the `/` route to the `/home` route. :::tip Redirect route files should not export a component. ::: It's also possible to define nested redirect routes. For the following file structure: ```treeview src/ └── app/ └── pages/ └── cities/ β”œβ”€β”€ index.page.ts β”œβ”€β”€ new-york.page.ts └── san-francisco.page.ts ``` and the following `routeMeta` definition to the `src/app/pages/cities/index.page.ts` file: ```ts export const routeMeta: RouteMeta = { redirectTo: '/cities/new-york', pathMatch: 'full', }; ``` Navigating to `/cities` redirects to `/cities/new-york`. :::note Nested redirects always require an absolute path. ::: ## Route Meta Tags The `RouteMeta` type has a property `meta` which can be used to define a list of meta tags for each route: ```ts export const routeMeta: RouteMeta = { title: 'Refresh every 30 sec', meta: [ { httpEquiv: 'refresh', content: '30', }, ], }; @Component({ template: `

Hello Analog

See you again in 30 seconds. `, }) export default class RefreshComponent {} ``` The above example sets meta tag ``, which forces the browser to refresh the page every 30 seconds. To read more about possible standard meta tags, please visit official [docs](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/meta). ## Open Graph meta tags The above property `meta` can also be used to define OpenGraph meta tags for SEO and social apps optimizations: ```ts export const routeMeta: RouteMeta = { meta: [ { name: 'description', content: 'Description of the page', }, { name: 'author', content: 'Analog Team', }, { property: 'og:title', content: 'Title of the page', }, { property: 'og:description', content: 'Some catchy description', }, { property: 'og:image', content: 'https://somepage.com/someimage.png', }, ], }; ``` This example will allow social apps like Facebook or Twitter to display titles, descriptions, and images optimally. --- # Middleware URL: https://analogjs.org/docs/features/routing/middleware # Middleware Analog supports server-side middleware that can be used to modify requests, check for authentication, send redirects, and more. ## Setting up Middleware Middleware is automatically registered when placed in the `src/server/middleware` folder. ```treeview src/ └── server/ └── middleware/ └── auth.ts ``` Also, if not present, add the middleware files to `include` array in the `tsconfig.app.json`. ```json { // other config ... "include": [ "src/**/*.d.ts", "src/app/pages/**/*.page.ts", "src/server/middleware/**/*.ts" <---- ], } ``` Middleware is defined using the `defineEventHandler` function. ```ts export default defineEventHandler((event) => { if (event.node.req.originalUrl === '/checkout') { console.log('event url', event.node.req.originalUrl); setHeaders(event, { 'x-analog-checkout': 'true', }); } }); ``` - Middleware should only modify requests and should not return anything! - Middleware is run in order of the defined filenames. Prefix filenames with numbers to enforce a particular order. ## Filtering in Middleware Middleware can only be applied to specific routes using filtering. ```ts export default defineEventHandler(async (event) => { // Only execute for /admin routes if (getRequestURL(event).pathname.startsWith('/admin')) { const cookies = parseCookies(event); const isLoggedIn = cookies['authToken']; // check auth and redirect if (!isLoggedIn) { sendRedirect(event, '/login', 401); } } }); ``` ## Accessing Environment Variables Use the `process.env` global to access environment variables inside the middleware functions. Both server-only and publicly accessible environment variables defined in `.env` files can be read from the middleware. ```ts export default defineEventHandler((event) => { console.log('Path:', getRequestURL(event).pathname); console.log( 'Server Only Environment Variable:', process.env['SERVER_ONLY_VARIABLE'], ); console.log( 'Public Environment Variable:', process.env['VITE_EXAMPLE_VARIABLE'], ); }); ``` Learn more about [environment variables](https://vite.dev/guide/env-and-mode.html#env-variables) in the Vite documentation. --- # Routing URL: https://analogjs.org/docs/features/routing/overview # Routing Analog supports filesystem-based routing on top of the Angular Router. ## Defining Routes Routes are defined using folders and files in the `src/app/pages` folder. Only files ending with `.page.ts` are collected and used to build the set of routes. :::info Route components **must** be defined as the default export and all route components are **lazy-loaded**. ::: There are 5 primary types of routes: - [Routing](#routing) - [Defining Routes](#defining-routes) - [Index Routes](#index-routes) - [Static Routes](#static-routes) - [Route Groups](#route-groups) - [Dynamic Routes](#dynamic-routes) - [Using Route Component Input Bindings](#using-route-component-input-bindings) - [Layout Routes](#layout-routes) - [Pathless Layout Routes](#pathless-layout-routes) - [Catch-all Routes](#catch-all-routes) - [Putting It All Together](#putting-it-all-together) These routes can be combined in different ways to build URLs for navigation. :::note In addition to the 5 primary types of routes, Analog also supports [Redirect Routes](/docs/features/routing/metadata#redirect-routes) and [Content Routes](/docs/features/routing/content). ::: ## Index Routes Index routes are defined by using the filename as the route path enclosed in parenthesis. The example route below in `src/app/pages/(home).page.ts` defines an `/` route. ```ts @Component({ template: `

Welcome

`, }) export default class HomePageComponent {} ``` :::tip Index routes can also be defined by using `index.page.ts` as the route filename. ::: ## Static Routes Static routes are defined by using the filename as the route path. The example route below in `src/app/pages/about.page.ts` defines an `/about` route. ```ts @Component({ template: `

Hello Analog

Analog is a meta-framework on top of Angular. `, }) export default class AboutPageComponent {} ``` It's also possible to define nested static routes in two different ways: 1. By nesting the route files in folders - `src/app/pages/about/team.page.ts` defines an `/about/team` route. 2. By using the dot notation in the filename - `src/app/pages/about.team.page.ts` also defines an `/about/team` route. ### Route Groups Routes can be grouped together in the same folder without adding a route path segment by wrapping a folder name in parenthesis. ```treeview src/ └── app/ └── pages/ └── (auth)/ β”œβ”€β”€ login.page.ts └── signup.page.ts ``` The above example defines `/login` and `/signup` routes. ## Dynamic Routes Dynamic routes are defined by using the filename as the route path enclosed in square brackets. The parameter for the route is extracted from the route path. The example route below in `src/app/pages/products/[productId].page.ts` defines a `/products/:productId` route. ```ts @Component({ imports: [AsyncPipe], template: `

Product Details

ID: {{ productId$ | async }} `, }) export default class ProductDetailsPageComponent { private readonly route = inject(ActivatedRoute); readonly productId$ = this.route.paramMap.pipe( map((params) => params.get('productId')), ); } ``` Dynamic routes can also be defined using the dot notation in the filename - `src/app/pages/products.[productId].page.ts` defines a `/products/:productId` route. ### Using Route Component Input Bindings If you are using the `withComponentInputBinding()` feature with the Angular Router, you can use the **Input** decorator, along with the same **parameter name** to get the route parameter. First, add the `withComponentInputBinding()` to the arguments for the `provideFileRouter()` function. ```ts // src/app/app.config.ts export const appConfig: ApplicationConfig = { providers: [ provideFileRouter(withComponentInputBinding()), // other providers ], }; ``` Next, use the route parameter as an input. ```ts // src/app/pages/products/[productId].page.ts @Component({ template: `

Product Details

ID: {{ productId }} `, }) export default class ProductDetailsPageComponent { @Input() productId: string; } ``` ## Layout Routes Layout routes are defined by using a parent file and child folder with the same name. The following structure below represents a layout route. ```treeview src/ └── app/ └── pages/ β”œβ”€β”€ products/ β”‚ β”œβ”€β”€ [productId].page.ts β”‚ └── (products-list).page.ts └── products.page.ts ``` This defines two routes with a shared layout: - `/products` - `/products/:productId` The parent `src/app/pages/products.page.ts` file contains the parent page with a router outlet. ```ts @Component({ imports: [RouterOutlet], template: `

Products

`, }) export default class ProductsComponent {} ``` The nested `src/app/pages/products/(products-list).page.ts` file contains the `/products` list page. ```ts @Component({ template: `

Products List

`, }) export default class ProductsListComponent {} ``` The nested `src/app/pages/products/[productId].page.ts` file contains the `/products/:productId` details page. ```ts @Component({ imports: [AsyncPipe, JsonPipe], template: `

Product Details

ID: {{ productId$ | async }} `, }) export default class ProductDetailsPageComponent { private readonly route = inject(ActivatedRoute); readonly productId$ = this.route.paramMap.pipe( map((params) => params.get('productId')), ); } ``` ### Pathless Layout Routes Layout routes can also be defined without adding a route path segment. ```treeview src/ └── app/ └── pages/ β”œβ”€β”€ (auth)/ β”‚ β”œβ”€β”€ login.page.ts β”‚ └── signup.page.ts └── (auth).page.ts ``` The above example defines `/login` and `/signup` routes with a shared layout. The parent `src/app/pages/(auth).page.ts` file contains the parent page with a router outlet. ## Catch-all Routes Catch-all routes are defined by using the filename as the route path prefixed with 3 periods enclosed in square brackets. The example route below in `src/app/pages/[...page-not-found].page.ts` defines a wildcard `**` route. This route is usually for 404 pages. ```ts export const routeMeta: RouteMeta = { title: 'Page Not Found', canActivate: [ () => { const response = injectResponse(); if (import.meta.env.SSR && response) { response.statusCode = 404; response.end(); } return true; }, ], }; @Component({ imports: [RouterLink], template: `

Page Not Found

Go Back Home `, }) export default class PageNotFoundComponent {} ``` Catch-all routes can also be defined as nested child routes. ## Putting It All Together For the following file structure: ```treeview src/ └── app/ └── pages/ β”œβ”€β”€ (auth)/ β”‚ β”œβ”€β”€ login.page.ts β”‚ └── signup.page.ts β”œβ”€β”€ (marketing)/ β”‚ β”œβ”€β”€ about.md β”‚ └── contact.md β”œβ”€β”€ products/ β”‚ β”œβ”€β”€ (product-list).page.ts β”‚ β”œβ”€β”€ [productId].edit.page.ts β”‚ └── [productId].page.ts β”œβ”€β”€ (auth).page.ts β”œβ”€β”€ (home).page.ts β”œβ”€β”€ [...not-found].md └── products.page.ts ``` The filesystem-based router will generate the following routes: | Path | Page | | ------------------ | ---------------------------------------------------------------- | | `/` | `(home).page.ts` | | `/about` | `(marketing)/about.md` | | `/contact` | `(marketing)/contact.md` | | `/login` | `(auth)/login.page.ts` (layout: `(auth).page.ts`) | | `/signup` | `(auth)/signup.page.ts` (layout: `(auth).page.ts`) | | `/products` | `products/(product-list).page.ts` (layout: `products.page.ts`) | | `/products/1` | `products/[productId].page.ts` (layout: `products.page.ts`) | | `/products/1/edit` | `products/[productId].edit.page.ts` (layout: `products.page.ts`) | | `/unknown-url` | `[...not-found].md` | ## Providing Extra Routes Routes can be added manually in addition to the routes discovered through the filesystem. Use the `withExtraRoutes` with an array of routes to be prepended to the discovered routes array. All the routes are merged into a single array. ```ts const customRoutes: Routes = [ { path: 'custom', loadComponent: () => import('./custom-component').then((m) => m.CustomComponent), }, ]; export const appConfig: ApplicationConfig = { providers: [provideFileRouter(withExtraRoutes(customRoutes))], }; ``` ## Visualizing and Debugging Routes When you are building the pages for your application, it can help to visually see the routes based on the filesystem structure. You can use the `withDebugRoutes()` function to provide a debug route that displays the pages and layouts for your application. Use the `withDebugRoutes` function in the `app.config.ts`: ```ts export const appConfig: ApplicationConfig = { providers: [provideFileRouter(withDebugRoutes())], }; ``` Navigate the `__analog/routes` URL in the browser to see the routes table. ![debug routes page](/img/debug-routes.png) --- # Type-safe Routing URL: https://analogjs.org/docs/features/routing/typed-routes # Type-safe Routing Typed routing is an opt-in experimental feature that adds checked route paths and parameters to Analog's file router. It is disabled by default. Its APIs and generated types may change while the feature is experimental. Enable it explicitly with `experimental.typedRouting` in your existing Vite configuration: ```ts import analog from '@analogjs/platform'; import { defineConfig } from 'vite'; export default defineConfig({ plugins: [analog({ experimental: { typedRouting: true } })], }); ``` Analog generates `src/routeTree.gen.d.ts`. Include it in each tsconfig that checks your application, including separate browser, server, and test configurations. Add it to your existing `files` list, or ensure an `include` pattern covers it: ```json { "include": ["src/**/*.d.ts"] } ``` Preserve your other `files` and `include` entries. Paths are relative to the tsconfig declaring them. Inherited lists apply unless a child tsconfig overrides them. Analog checks inclusion in the tsconfig selected by its Angular compiler plugin and reports an error with the path to add when it is missing. Application entry files are not modified, and components do not need to import the declaration. Keep the generated file in source control. Start the dev server to generate it before running standalone type checks. Page additions, renames, and removals regenerate the declaration during development. Production builds reject stale checked-in route tables; the first build can generate a missing table. To customize the output or allow regeneration during builds: ```ts analog({ experimental: { typedRouting: { outFile: 'src/routeTree.gen.d.ts', verifyOnBuild: false, }, }, }); ``` Custom output paths must end in `.d.ts` and be included in the relevant tsconfigs. ## Build links Import `LinkTo` from `@analogjs/router` into your component's `imports` and bind a destination to `[linkTo]`: ```html Shipping Details ``` The generated route table checks the path, required named parameters, and their value types together. Static routes accept string shorthand, such as `linkTo="/shipping"` or `[linkTo]="'/shipping'"`. Only generated routes with no parameters allow this shorthand; dynamic and catch-all routes require a destination object. Unknown paths, unrestricted `string` values, positional command arrays, and `UrlTree` values are rejected with `strictTemplates` enabled. Use the destination object with `query` and `hash` to add query parameters and fragments, including for static routes. Binding `null` or `undefined` disables the link. `LinkTo` composes Angular's `RouterLink`, preserving href generation, navigation, modifier clicks, and target behavior. It exposes `target`, `queryParamsHandling`, `preserveFragment`, `skipLocationChange`, `replaceUrl`, and `state`. Import Angular's `RouterLinkActive` separately to use active classes on the link or an ancestor. Use `[linkTo]` on its own; do not also apply `[routerLink]` to the same element. Dynamic parameters accept strings or numbers. Numbers are converted to strings when building links and navigating. Required catch-all parameters accept non-empty arrays of string or number segments; optional catch-all parameters may be omitted or empty. For required catch-all arrays stored in variables, use the tuple type `[string | number, ...(string | number)[]]` to preserve the non-empty guarantee. Query values are strings, numbers, booleans, or arrays of them. Like parameters, they are converted to strings, so they read back as strings. A `null` or `undefined` value omits the key, or removes it when `queryParamsHandling` is `'merge'`. `hash` supplies the fragment. URL path segments are encoded automatically. ## Navigate programmatically Use `injectNavigate` inside an Angular injection context: ```ts import { injectNavigate } from '@analogjs/router'; const navigate = injectNavigate(); navigate('/products/[id]', { params: { id: 42 } }, { replaceUrl: true }); navigate('/search', { query: { page: 2 } }, { queryParamsHandling: 'merge' }); ``` The last argument accepts Angular's navigation options, such as `replaceUrl` and `state`, plus `queryParamsHandling` and `preserveFragment`. Use `query` and `hash` in place of `queryParams` and `fragment`. ## Build link data in TypeScript Use `toRoute` when you need link data in TypeScript. It does not require an injection context: ```ts import { toRoute } from '@analogjs/router'; const link = toRoute('/products/[id]', { params: { id: 42 } }); // link.path is ['/', 'products', '42'] ``` The result contains Angular router commands in `path`, plus `queryParams` and `fragment`. These properties can be passed to Angular's router APIs. For template links, use `[linkTo]` as shown above. ## Read parameters as signals ```ts import { injectParams, injectQuery } from '@analogjs/router'; const params = injectParams('/products/[id]'); const query = injectQuery('/products/[id]'); // params().id is a string; query()['page'] is a raw query value. ``` Use these helpers in a component rendered by the specified route. The path narrows TypeScript types; it does not select another active route. Parameters include ancestor parameters, and catch-all values are normalized to arrays. The helpers accept `{ injector }` when called outside an injection context. Values remain raw Angular router values. Exporting a schema does not validate or coerce these signals. For example, `"42"` stays a string. Without a path, `injectParams()`, `injectQuery()`, `injectRouteData()`, and `injectResources()` return untyped values for the current route, for example in a component shared by several routes. Catch-all segments need a path: a catch-all route has no parameter name at runtime, so `injectParams()` omits required catch-alls and does not split optional ones into arrays. Type checking comes from the generated table. The `experimental.typedRouting` option enables generation for the feature as a whole; no additional router provider or per-helper experimental flag is required. ## Read route data and load results `injectRouteData` reads the page's route data as a signal. Its type comes from the page module and its [layout routes](/docs/features/routing/overview#layout-routes): static `routeMeta.data`, resolved `routeMeta.resolve` values, and the page's server `load` result under `load`. ```ts // src/app/pages/products.page.ts (layout) import type { RouteMeta } from '@analogjs/router'; export const routeMeta = { data: { section: 'catalog' } } satisfies RouteMeta; ``` ```ts // src/app/pages/products/[id].page.ts import { Component, inject } from '@angular/core'; import { injectRouteData, type RouteMeta } from '@analogjs/router'; export const routeMeta = { resolve: { related: () => inject(ProductsService).related() }, } satisfies RouteMeta; @Component({ template: `{{ data().section }}` }) export default class ProductPage { readonly data = injectRouteData('/products/[id]'); // data().related and data().load are typed as well. } ``` Use `satisfies RouteMeta` instead of a `RouteMeta` annotation. An annotation widens the object, so data values are typed as `unknown`. `injectLoad` also accepts a route path. The result is typed from the page's `.server.ts` `load` function, so the page does not need to import it: ```ts readonly product = toSignal(injectLoad('/products/[id]'), { requireSync: true }); ``` Paths without a server `load` function are rejected. The generated declaration references page and `.server.ts` modules with `typeof import()`, so they must type-check under each application tsconfig. Adding or removing a `routeMeta` or `load` export regenerates the declaration during development. As with Angular's route data inheritance, a page's keys override the same keys from its layouts. `load` is always the page's own result. When several files define the same URL, such as a layout and its index page (`products.page.ts` and `products/index.page.ts`), `injectRouteData` has no typed keys for that path and `injectLoad` rejects it. Use `injectRouteData()` without a path or `injectLoad()` there. ## Access route resources On Angular 22.2+, when using `withRouterResources()`, `injectResources` returns the reactive resources defined on the route via `routeMeta.resources`: ```ts import { injectResources } from '@analogjs/router'; const resources = injectResources('/products/[id]'); // resources.user is a typed Resource instance const user = resources.user.value(); ``` Calling `injectResources()` without a route path returns untyped route resources. On older Angular versions without router resources, it returns an empty object. The other typed-routing helpers do not require router resources. ## Navigate relative to the current route Pass the current page's path to `injectNavigate`, or bind it to `from` on `LinkTo`, to navigate relative to it: ```ts // src/app/pages/users/[id].page.ts const navigate = injectNavigate('/users/[id]'); navigate('./posts/[postId]', { params: { postId: 7 } }); // /users/42/posts/7 navigate('.', { query: { tab: 'bio' } }); // /users/42?tab=bio navigate('../../about'); ``` ```html Profile Next ``` Relative targets resolve against the `from` path pattern: `.` is the route itself, `./child` is below it, and `..` or `../sibling` go through its ancestors. Each target has one relative form, through the nearest shared ancestor, and must be a generated route. Absolute paths remain available. Parameters in the leading segments shared by `from` and the target default to their current values, so they become optional. Parameters after the paths diverge are required, and explicit values override inherited ones. As with `injectParams`, use `from` in a component rendered by that route. With `injectNavigate(from)`, `params` and `query` can also be functions. They receive the current route's typed params or query and return the target's values: ```ts navigate('.', { params: (prev) => ({ id: Number(prev.id) + 1 }) }); // /users/43 navigate('.', { query: ({ tab, ...rest }) => rest }); // drops tab, keeps the rest ``` ## Compatibility Existing file routing, Markdown routes, Nitro configuration, and Angular compiler options remain unchanged. Additional page and content directories participate in generation. Without the generated declaration in the TypeScript program, typed helper calls fail to compile. Use Angular's existing router APIs for navigation without generated route types. ## Type checking Use Angular's compiler with your application tsconfig to check TypeScript and templates: ```sh pnpm exec ngc -p tsconfig.app.json --noEmit ``` Template checking requires `angularCompilerOptions.strictTemplates: true`. If you use `fastCompile` or disable type checking in Vite, run this check separately; enabling typed routing does not override those compiler settings. --- # Server Side Rendering URL: https://analogjs.org/docs/features/server/server-side-rendering # Server Side Rendering Analog supports server-side rendering during development and building for production. ## Transforming Packages for SSR Compatibility Some dependencies may need additional transforms to work for server-side rendering. If you receive an error during SSR in development, one option is to add the package(s) to the `ssr.noExternal` array in the Vite config. You can use glob patterns to include sets of packages or libraries. Some examples are listed below. ```ts // https://vitejs.dev/config/ export default defineConfig(({ mode }) => ({ ssr: { noExternal: [ 'apollo-angular', // npm package import 'apollo-angular/**', // npm package import along with sub-packages '@spartan-ng/**', // libs under the npmScope inside an Nx workspace ], }, // ...other config })); ``` For more information about externals with SSR, check out the [Vite documentation](https://vitejs.dev/guide/ssr.html#ssr-externals). ## Hybrid Rendering with Client-Only Routes SSR is enabled by default. For a hybrid approach, you can specify some routes to only be rendered client-side, and not be server side rendered. This is done through the `routeRules` configuration object by specifying an `ssr` option. ```ts // https://vitejs.dev/config/ export default defineConfig(({ mode }) => ({ // ...other config plugins: [ analog({ prerender: { routes: ['/', '/404.html'], }, nitro: { routeRules: { // All admin URLs are only rendered on the client '/admin/**': { ssr: false }, // Render a 404 page as a fallback page '/404.html': { ssr: false }, }, }, }), ], })); ``` ## Disabling SSR You can opt-out of it and generate a client-only build by adding the following option to the `analog()` plugin in your `vite.config.ts`: ```ts // https://vitejs.dev/config/ export default defineConfig(({ mode }) => ({ // ...other config plugins: [ analog({ ssr: false, prerender: { routes: [], }, }), ], })); ``` ## Prerendering routes With SSR, the `"/"` route is prerendered by default. It is a necessary step to return a rendered HTML when the user visits the root of the application. The prerendered routes can be customized, but keep in mind to include the `"/"` route also. ```js // https://vitejs.dev/config/ export default defineConfig(({ mode }) => ({ // ...other config plugins: [ analog({ prerender: { routes: ['/', '/about'], }, }), ], })); ``` You can opt-out of prerendering altogether by passing an empty array of routes. ```js // https://vitejs.dev/config/ export default defineConfig(({ mode }) => ({ // ...other config plugins: [ analog({ ssr: true, prerender: { routes: async () => { return []; }, }, }), ], })); ``` --- # Building Static Sites URL: https://analogjs.org/docs/features/server/static-site-generation # Building Static Sites Analog supports Static Site Generation when building for deployment. This includes prerendering provided routes to static HTML files along with the client-side application. ## Static Site Generation ### From Routes List To prerender pages, use the `prerender` property to configure routes to be rendered at build time. The routes to be prerendered can be provided asynchronously also. ```ts // https://vitejs.dev/config/ export default defineConfig(({ mode }) => ({ plugins: [ analog({ prerender: { routes: async () => [ '/', '/about', '/blog', '/blog/posts/2023-02-01-my-first-post', ], }, }), ], })); ``` ### From Content Directory You might want to prerender all routes that are the result of a rendered content directory. For example if you have a blog and all your articles are places as Markdown files in the `contents` directory. For such scenarios, you can add an object to the `routes` config to render everything within a directory. The directory structure may not be reflected 1:1 in your apps path. Therefore, you have to pass a `transform` function which maps the file paths to the URLs. The returning string should be the URL path in your app. Using `transform` allows you also filter out some routes by returning `false`. This does not include them in the prerender process, such as files marked as `draft` in the frontmatter. The `contentDir` value of that object can be a glob pattern or just a specific path. ```ts // https://vitejs.dev/config/ export default defineConfig(({ mode }) => ({ plugins: [ analog({ prerender: { routes: async () => [ '/', '/blog', { contentDir: 'src/content/blog', transform: (file: PrerenderContentFile) => { // do not include files marked as draft in frontmatter if (file.attributes.draft) { return false; } // use the slug from frontmatter if defined, otherwise use the files basename const slug = file.attributes.slug || file.name; return `/blog/${slug}`; }, }, ], }, }), ], })); ``` #### Recursing Into Subdirectories By default, `contentDir` only matches files at the top level of the configured directory. To prerender content organized into category subdirectories, set `recursive: true`. Each discovered file's directory relative to `contentDir` is exposed via `file.relativePath`, which `transform` can use to build the route. ```ts export default defineConfig(({ mode }) => ({ plugins: [ analog({ prerender: { routes: async () => [ '/', { contentDir: 'src/content/docs', recursive: true, transform: (file: PrerenderContentFile) => { const slug = file.attributes.slug || file.name; return file.relativePath ? `/docs/${file.relativePath}/${slug}` : `/docs/${slug}`; }, }, ], }, }), ], })); ``` ### Outputting Source Markdown Files To make prerendered pages more accessible to LLMs or other tools that prefer raw markdown, you can output the source markdown file alongside each prerendered route. The source file will be accessible at the route path with a `.md` extension (e.g., `/blog/my-post` would also be available at `/blog/my-post.md`). For individual routes, specify the path to the source file: ```ts // https://vitejs.dev/config/ export default defineConfig(({ mode }) => ({ plugins: [ analog({ prerender: { routes: async () => [ '/', { route: '/overview', outputSourceFile: 'src/content/overview.md', }, ], }, }), ], })); ``` For content directories, use a function that receives the file information and returns the content to output: ```ts // https://vitejs.dev/config/ export default defineConfig(({ mode }) => ({ plugins: [ analog({ prerender: { routes: async () => [ '/', { contentDir: 'src/content/blog', transform: (file: PrerenderContentFile) => { const slug = file.attributes.slug || file.name; return `/blog/${slug}`; }, outputSourceFile: (file: PrerenderContentFile) => file.content, }, ], }, }), ], })); ``` You can also conditionally skip outputting the source file by returning `false`: ```ts outputSourceFile: (file: PrerenderContentFile) => { // Don't output source for draft posts if (file.attributes.draft) { return false; } return file.content; }, ``` ## Only Prerendering Static Pages To only prerender the static pages without building the server, use the `static: true` flag. > The `ssr` flag must still be set to `true` for prerendering static pages. ```ts // https://vitejs.dev/config/ export default defineConfig(({ mode }) => ({ plugins: [ analog({ static: true, prerender: { routes: async () => [ '/', '/about', '/blog', '/blog/posts/2023-02-01-my-first-post', // Prerender 404.html page for SPAs '/404.html', ], }, nitro: { routeRules: { '/404.html': { ssr: false }, }, }, }), ], })); ``` The static pages can be deployed from the `dist/analog/public` directory. ## Prerendering Server-Side Data When using [Server-Side Data Fetching](/docs/features/data-fetching/server-side-data-fetching), the data is cached and reused using transfer state _only_ on the first request. To prerender the server-side data fetched along with the route, set the `staticData` flag to `true` in the config object for the prerendered route. For example, a route defined as `src/app/pages/shipping.page.ts` with an associated `src/app/pages/shipping.server.ts` has the route and server data prerendered to be completely static. ```ts // https://vitejs.dev/config/ export default defineConfig(({ mode }) => ({ plugins: [ analog({ static: true, prerender: { routes: async () => [ '/', { route: '/shipping', staticData: true, }, ], }, }), ], })); ``` ## Sitemap Generation Analog also supports automatic sitemap generation. Analog generates a sitemap in the `dist/analog/public` directory when running a build if a sitemap configuration is provided. ```ts // https://vitejs.dev/config/ export default defineConfig(({ mode }) => ({ plugins: [ analog({ prerender: { routes: async () => ['/', '/blog'], sitemap: { host: 'https://analogjs.org', }, }, }), ], })); ``` To customize the sitemap definition, use the `sitemap` callback function to customize the `lastmod`, `changefreq`, and `priority` fields. ```ts // https://vitejs.dev/config/ export default defineConfig(({ mode }) => ({ plugins: [ analog({ prerender: { sitemap: { host: 'https://analogjs.org', }, routes: async () => [ '/', '/blog', { route: '/blog/2022-12-27-my-first-post', sitemap: { lastmod: '2022-12-27', }, }, { contentDir: '/src/content/archived', transform: (file: PrerenderContentFile) => { return `/archived/${file.attributes.slug || file.name}`; }, sitemap: (file: PrerenderContentFile) => { return { lastmod: 'read last modified date for content file', changefreq: 'never', }; }, }, ], }, }), ], })); ``` As long as prerender routes are provided, Analog generates a `sitemap.xml` file containing a mapping of the pages' ``, ``, ``, and `` properties. ```xml https://analogjs.org/ 2023-07-01 https://analogjs.org/blog/2022-12-27-my-first-post 2022-12-27 https://analogjs.org/blog/archived/hello-world 2022-12-01 never ``` ## Post-rendering Hooks Analog supports the post-rendering hooks during the prerendering process. The use case for post-rendering hooks can be inlining critical CSS, adding/removing scripts in HTML files, etc. The sample code below shows how to use `postRenderingHooks` in your code: ```ts // https://vitejs.dev/config/ export default defineConfig(() => { return { publicDir: 'src/public', build: { target: ['es2020'], }, plugins: [ analog({ static: true, prerender: { routes: async () => [], postRenderingHooks: [ async (route: PrerenderRoute) => console.log(route), ], }, }), ], }; }); ``` The `PrerenderRoute` gives you information about `route`, `contents`, `data`, and `fileName`, which can be useful for making changes to your content during the prerendering phase. Below is a small example where we can append a script to include Google Analytics during the prerendering process using `postRenderingHooks`: ```ts /// // https://vitejs.dev/config/ export default defineConfig(() => { return { publicDir: 'src/public', build: { target: ['es2020'], }, plugins: [ analog({ static: true, prerender: { routes: async () => ['/', '/aboutus'], postRenderingHooks: [ async (route: PrerenderRoute) => { const gTag = ``; if (route.route === '/aboutus') { route.contents = route.contents?.concat(gTag); } }, ], }, }), ], }; }); ``` --- # Streaming SSR URL: https://analogjs.org/docs/features/server/streaming-ssr # Streaming SSR Analog supports progressive streaming server-side rendering, flushing the response to the browser as the app renders instead of buffering the whole document until it is complete. The document head is sent immediately, each `@defer (hydrate …)` block is sent the moment it resolves on the server, and the authoritative document arrives last, so a slow block never holds back the rest of the page. :::info Experimental Streaming SSR is experimental and opt-in. It requires **Angular 21 or later** and **Vite 6 or later**, and builds on [incremental hydration](https://angular.dev/guide/incremental-hydration). The default buffered [Server Side Rendering](/docs/features/server/server-side-rendering) path is unchanged. ::: ## Enabling streaming Enable the `experimental.streaming` option in your Vite config: ```ts // vite.config.ts import analog from '@analogjs/platform'; import { defineConfig } from 'vite'; export default defineConfig({ plugins: [analog({ experimental: { streaming: true } })], }); ``` Then use `renderStream` instead of `render` in `main.server.ts`: ```ts // src/main.server.ts import { renderStream } from '@analogjs/router/server'; import { config } from './app/app.config.server'; import { AppComponent } from './app/app.component'; export default renderStream(AppComponent, config); ``` Streaming builds on incremental hydration, so enable it in your client providers. On Angular 21 use `withIncrementalHydration()`; on Angular 22+ it is enabled by default with `provideClientHydration()`: ```ts // src/app/app.config.ts import { provideClientHydration, withIncrementalHydration, } from '@angular/platform-browser'; export const appConfig: ApplicationConfig = { providers: [ provideClientHydration(withIncrementalHydration()), // ... ], }; ``` ## Streaming deferred blocks Content that should stream progressively goes in a `@defer` block with a `hydrate` trigger. Each block is rendered eagerly on the server, streamed as it resolves, and hydrated on the client when its trigger fires: ```xml

Dashboard

@defer (hydrate on immediate) { } @placeholder {

Loading activity…

} @defer (hydrate on viewport) { } @placeholder {

Loading recommendations…

} ``` A block backed by asynchronous data (for example an [`httpResource`](https://angular.dev/guide/http/http-resource)) keeps the render pending until its data resolves, so the block streams with its final content and the page's time-to-first-byte is unaffected. ## Title and meta Because the document head is flushed before the app renders, a title or meta set during render (via the `Title`/`Meta` services or route metadata) is applied to the streamed document once the render completes, before hydration runs. Search engine crawlers are served a buffered render with a fully resolved head instead of the streamed shell. ## Completion and cancellation Bootstrap modules are preloaded with the shell and execute after the complete, hydration-ready document is installed. If rendering fails or the document is truncated, the browser shows a generic error view instead of bootstrapping an incomplete page. A disconnected client cancels pending streaming work and disposes the Angular platform. Progressive HTTP responses use `Cache-Control: no-store, no-transform` and identity encoding so intermediaries do not cache partial HTML or buffer it for compression. Prerendered pages and routes with `streaming: false` produce complete buffered documents. Use a buffered route when its final HTTP status must be known before sending response headers. ## Opting a route out of streaming Disable streaming for specific routes with a `streaming: false` route rule, the same way `ssr: false` disables SSR. Matching routes fall back to a buffered render (no streaming, but SSR and hydration still work): ```ts // vite.config.ts import analog from '@analogjs/platform'; import { defineConfig } from 'vite'; export default defineConfig({ plugins: [ analog({ experimental: { streaming: true }, nitro: { routeRules: { '/report': { streaming: false }, }, }, }), ], }); ``` --- # Running Tests URL: https://analogjs.org/docs/features/testing/overview # Running Tests Analog supports [Vitest](https://vitest.dev) for running unit tests. ## Vitest Features Vitest supports many features: - A Jest-compatible API. - Supports Vite's config, transforms, resolvers, and plugins. - Smart & instant watch mode. - TypeScript support. - Jest-compatible snapshots. - jsdom for DOM mocking. - In-source testing. - And more ... You can also [add Vitest](/docs/features/testing/vitest) to your existing project. ## Angular support for Vitest In Angular v21, stable support for Vitest directly through the Angular CLI was introduced for new Angular projects. While both Analog and Angular support running tests with Vitest, there are some similarities and key differences. The table below shows the features available across both choices. | Vitest | Analog | Angular | | -------------------- | --------- | ------------ | | Angular Versions | v17+ | v21+ | | Support | Community | Angular Team | | Builders | βœ… | βœ… | | Schematics | βœ… | βœ… | | Migrations | βœ… | βœ… | | Browser Mode | βœ… | βœ… | | Fully Configurable | βœ… | ⚠️ | | Vitest CLI | βœ… | ❌ | | Vitest Workpsaces | βœ… | ❌ | | Custom Environments | βœ… | ❌ | | Custom Providers | βœ… | ❌ | | IDE extensions | βœ… | ❌ | | Buildable Libs | βœ… | ❌ | | Module Mocking/Graph | βœ… | ❌ | | Types | βœ… | ❌ | The table above is not to compare the two solutions, but to provide the information on what features are supported by each implementation. Choose the solution that best fits your needs and priorities. ## Running Unit Tests To run unit tests, use the `test` command: ```shell npm run test ``` ```shell yarn test ``` ```shell pnpm run test ``` ## IDE Support Tests can also be run directly from your IDE using the Vitest [IDE integrations](https://vitest.dev/guide/ide) for VS Code or JetBrains IDEs. ## Known limitations - Only globals are patched with Zone.js. This means, that if you import `it`, `describe` etc from `vitest` directly, you won't be able to run `fakeAsync`. Instead, use the functions (`it`, `describe` etc. the way you used to do in Jest/Jasmine – without any imports of these functions in the test file). - `vmThreads` is used. This can lead to potential memory leaks and is used as a default to provide an environment closer to the Jest with JSDOM. More details you can read [here](https://github.com/vitest-dev/vitest/issues/4685). To change that – adjust your `vite.config.mts` ```typescript export default defineConfig(({ mode }) => { return { test: { pool: 'threads', // add this property }, }; }); ``` --- # Using Vitest with An Angular Project URL: https://analogjs.org/docs/features/testing/vitest # Using Vitest with An Angular Project [Vitest](https://vitest.dev) can be added to **_any_** existing Angular project with a few steps. ## Automated Setup Using a Schematic/Generator Vitest can be installed and setup using a schematic/generator for Angular CLI or Nx workspaces. First, install the `@analogjs/vitest-angular` package: ```shell npm install @analogjs/vitest-angular --save-dev ``` ```shell yarn add @analogjs/vitest-angular --dev ``` ```shell pnpm install -w @analogjs/vitest-angular --save-dev ``` Next, run the schematic to set up the Vite config, test configuration files, and update the test configuration. ```shell ng g @analogjs/vitest-angular:setup --project [your-project-name] ``` ### Schematic Options | Option | Type | Default | Description | | ------------- | ------- | ------- | ----------------------------------------------------------- | | `project` | string | - | The name of the project to configure (required) | | `browserMode` | boolean | `false` | Configure Vitest to run tests in a browser using Playwright | To enable browser mode during setup: ```shell ng g @analogjs/vitest-angular:setup --project [your-project-name] --browserMode ``` This automatically installs Playwright dependencies and configures Vitest for browser testing. See [Setup for Running Tests in the Browser](#setup-for-running-tests-in-the-browser) for more details. If using browser mode, run `npx playwright install` after the schematic to ensure playwright is installed and configured. Next, go to [running tests](#running-tests) ## Manual Installation To add Vitest manually, install the necessary packages: ```shell npm install @analogjs/vite-plugin-angular @analogjs/vitest-angular jsdom --save-dev ``` ```shell yarn add @analogjs/vite-plugin-angular @analogjs/vitest-angular jsdom --dev ``` ```shell pnpm install -w @analogjs/vite-plugin-angular @analogjs/vitest-angular jsdom --save-dev ``` ## Setup for Running Tests for Node To setup Vitest, create a `vite.config.ts` at the root of your project: ```ts /// export default defineConfig(({ mode }) => ({ plugins: [angular()], test: { globals: true, setupFiles: ['src/test-setup.ts'], environment: 'jsdom', include: ['src/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'], reporters: ['default'], }, })); ``` Next, define a `src/test-setup.ts` file to setup the `TestBed`: ### Zoneless setup As of Angular v21, `Zoneless` change detection is the default for new projects. Use the following setup: ```ts // src/test-setup.ts import '@angular/compiler'; import { setupTestBed } from '@analogjs/vitest-angular/setup-testbed'; setupTestBed(); ``` ### Zone.js setup If you are using `Zone.js` for change detection, import the `setup-zone` script before setting up the `TestBed`. This script patches the test environment for `Zone.js` helpers such as `fakeAsync`, and automatically includes support for setting up snapshot tests. ```ts // src/test-setup.ts import '@angular/compiler'; import '@analogjs/vitest-angular/setup-zone'; import { setupTestBed } from '@analogjs/vitest-angular/setup-testbed'; setupTestBed({ zoneless: false, }); ``` The two side effect imports must come first. If your formatter sorts side effect imports below named imports, keep them in a separate import group with a blank line, or exclude `src/test-setup.ts` from import sorting. :::warning With `Zone.js`, use Vitest's global test functions, with `globals: true` set in the `vite.config.ts`, and `vitest/globals` added to the `types` array in the `tsconfig.spec.json`. Importing `describe`, `it`, `beforeEach`, and other functions from `vitest` directly bypasses the `Zone.js` patching, and `fakeAsync` fails with the error `Expected to be running in 'ProxyZone', but it was not found`. Zoneless tests have no such restriction. When migrating to zoneless change detection, you can switch back to importing the functions from `vitest`. ::: ### Configuration Options The `setupTestBed()` function accepts an optional configuration object with the following properties: - `zoneless` (boolean): Whether to use zoneless change detection (default: `true`) - `providers` (`Type[]`): Additional providers to include in the test environment (default: `[]`) - `teardown.destroyAfterEach` (boolean): Whether to destroy the test environment after each test. Set to `false` to keep the component rendered, allowing you to inspect its final state. (default: `true`) - `errorOnUnknownElements` (boolean): Whether to throw when unknown elements are present in a component's template. Set to `false` (default) to log the error instead of throwing. - `errorOnUnknownProperties` (boolean): Whether to throw when unknown properties are present in a component's template. Set to `false` (default) to log the error instead of throwing. **Example with options:** ```ts setupTestBed({ zoneless: true, providers: [], teardown: { destroyAfterEach: false }, errorOnUnknownElements: true, errorOnUnknownProperties: true, }); ``` Next, update the `test` target in the `angular.json` to use the `@analogjs/vitest-angular:test` builder: ```json { "$schema": "./node_modules/@angular/cli/lib/config/schema.json", "version": 1, "newProjectRoot": "projects", "projects": { "your-project": { "projectType": "application", "architect": { "build": ..., "serve": ..., "extract-i18n": ..., "test": { "builder": "@analogjs/vitest-angular:test" } } } } } ``` > You can also add a new target and name it `vitest` to run alongside your `test` target. Lastly, add the `src/test-setup.ts` to `files` array in the `tsconfig.spec.json` in the root of your project, set the `target` to `es2022`, and update the `types`. ```json { "extends": "./tsconfig.json", "compilerOptions": { "outDir": "./out-tsc/spec", "target": "es2022", "types": ["vitest/globals", "node"] }, "files": ["src/test-setup.ts"], "include": ["src/**/*.spec.ts", "src/**/*.d.ts"] } ``` Next, go to [running tests](#running-tests) ## Setup for Running Tests in the Browser If you prefer to run your tests in a browser, Vitest has experimental support for browser testing also. First, follow the steps for [running tests in node](#setup-for-running-tests-for-node). Then, install the necessary packages for running tests in the browser: ```shell npm install @vitest/browser-playwright playwright --save-dev ``` ```shell yarn add @vitest/browser-playwright playwright --dev ``` ```shell pnpm install -w @vitest/browser-playwright playwright ``` Update the `test` object in the `vite.config.ts`. - Remove the `environment: 'jsdom'` property. - Add a `browser` config for Vitest. ```ts /// export default defineConfig(({ mode }) => ({ plugins: [angular()], test: { globals: true, setupFiles: ['src/test-setup.ts'], // environment: 'jsdom', include: ['src/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'], reporters: ['default'], // Vitest browser config browser: { enabled: true, headless: false, // set to true in CI provider: playwright(), instances: [{ browser: 'chromium' }], }, }, })); ``` When running tests with headed browser mode, you may want to update your `src/test-setup.ts` to keep the component rendered: ```ts setupTestBed({ teardown: { destroyAfterEach: false }, // Enables visual test preview }); ``` This keeps the component rendered after tests complete, allowing you to visually inspect the final state in the browser preview. ## Reducing TypeScript Checking During Test Startup The Angular plugin defaults to `disableTypeChecking: true`, which limits the diagnostics it reports. TypeScript can still perform semantic checking while emitting JavaScript. In large workspaces, especially those that import other libraries through TypeScript path mappings, this can add to test startup time. For the default JIT test compilation, you can opt out of full TypeScript checking with [`noCheck`](https://www.typescriptlang.org/tsconfig/noCheck.html), available in TypeScript 5.6 and later. Create a separate `tsconfig.vitest.json` next to your existing `tsconfig.spec.json`: ```json { "extends": "./tsconfig.spec.json", "compilerOptions": { "noCheck": true } } ``` Point the Angular plugin in your test configuration to this file. Keep your existing Vitest settings and other plugins: ```ts import { resolve } from 'node:path'; import angular from '@analogjs/vite-plugin-angular'; import { defineConfig } from 'vitest/config'; export default defineConfig({ plugins: [ angular({ tsconfig: resolve(import.meta.dirname, 'tsconfig.vitest.json'), }), ], test: { globals: true, environment: 'jsdom', setupFiles: ['src/test-setup.ts'], }, }); ``` Keep `noCheck` out of the original `tsconfig.spec.json` so editors and a separate type-check command continue to report type errors: ```shell tsc --project tsconfig.spec.json --noEmit ``` `noCheck` still reports critical parse and emit errors. This is an opt-in setting for JIT tests; it does not change the plugin defaults or replace Angular template checking for AOT builds. ## Running Tests To run unit tests, use the `test` command: ```shell npm run test ``` ```shell yarn test ``` ```shell pnpm test ``` > The `npx vitest` command can also be used directly. Hungry for more? Check out Younes Jaaidi's [video course](https://courses.marmicode.io/courses/pragmatic-angular-testing?ref=ec72c7) on Angular testing. [![Angular Testing Course](/img/pragmatic-angular-testing-banner-2.jpg)](https://courses.marmicode.io/courses/pragmatic-angular-testing?ref=ec72c7) ## Test Coverage When measuring coverage with `@vitest/coverage-v8` (for example, `vitest run --coverage`), its `coverage.include` option β€” or its own default, if you don't set one β€” can match source files that `tsconfig.spec.json` does not `include`. This is the same narrow source-file pattern (`include: ["src/**/*.spec.ts", "src/**/*.d.ts"]`) that the Angular CLI's own `ng generate` schematic uses, so it isn't specific to Analog projects. A file outside that tsconfig's TypeScript program β€” a component with no `.spec.ts` file importing it, for example β€” is skipped by the Angular compiler with a warning: ```text [@analogjs/vite-plugin-angular]: "src/app/unused.component.ts" contains Angular decorators but is not in the TypeScript program. Ensure it is included in your tsconfig. ``` Coverage then falls back to parsing that file's raw, uncompiled source, which can fail outright for a component using decorators (a constructor parameter decorated with `@Inject()`, for instance) instead of simply reporting it at 0% coverage. To make sure coverage can see every file it measures, widen `include` in `tsconfig.spec.json` to also cover the source files `coverage.include` matches, not just spec files: ```json { "extends": "./tsconfig.json", "compilerOptions": { "outDir": "./out-tsc/spec", "target": "es2022", "types": ["vitest/globals", "node"] }, "files": ["src/test-setup.ts"], "include": ["src/**/*.ts"] } ``` This brings every source file into the same Angular program used for your tests, so the existing compiler handles it correctly β€” including files no spec currently imports. ## Snapshot Testing For snapshot testing you can use `toMatchSnapshot` from `expect` API. The import of `setup-snapshots` and `setup-serializers` are complementary: - Use `setup-snapshots` to serialize Angular fixtures and component refs so Vitest snapshots print component markup instead of Angular testing internals. - Use `setup-serializers` to clean DOM snapshots by removing Angular runtime noise such as `_ngcontent-*`, `_nghost-*`, `ng-reflect-*`, generated ids and classes, and removes comments from DOM snapshots (e.g. ``). Below is a small example of how to write a snapshot test: ```ts // card.component.spec.ts describe('CardComponent', () => { let fixture: ComponentFixture; let component: CardComponent; beforeEach(() => TestBed.configureTestingModule({ imports: [CardComponent], }), ); beforeEach(() => { fixture = TestBed.createComponent(CardComponent); component = fixture.componentInstance; fixture.detectChanges(); }); it('should create the app', () => { expect(fixture).toMatchSnapshot(); }); }); ``` After you run the test, a `card.component.spec.ts.snap` file is created in the`__snapshots__` folder with the below content: ```ts // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html exports[`CardComponent > should create the app 1`] = ` `; ``` The snapshots generated should be reviewed and added to version control. ## Using TypeScript Config Path Aliases If you are using `paths` in your `tsconfig.json`, support for those aliases can be added to the `vite.config.ts`. ### With Angular CLI First, install the `vite-tsconfig-paths` package. ```shell npm install vite-tsconfig-paths --save-dev ``` ```shell yarn add vite-tsconfig-paths --dev ``` ```shell pnpm install -w vite-tsconfig-paths --save-dev ``` Next, add the plugin to the `plugins` array in the `vite.config.ts` with the `root` set as the relative path to the root of the project. ```ts /// export default defineConfig(({ mode }) => ({ plugins: [angular(), viteTsConfigPaths()], })); ``` ### With Nx Nx workspaces use the same `vite-tsconfig-paths` plugin. The `nxViteTsPaths` plugin from `@nx/vite` is deprecated and is removed in Nx 24, so replace it with `viteTsConfigPaths()`. ```ts /// import viteTsConfigPaths from 'vite-tsconfig-paths'; export default defineConfig(({ mode }) => ({ root: import.meta.dirname, plugins: [angular(), viteTsConfigPaths()], })); ``` Use `import.meta.dirname` instead of `__dirname` for the project `root`, so the config also loads with Vite's native config loader. The `@analogjs/platform` generators emit `import.meta.dirname`, and Nx 23.2 ships the `update-23-2-0-use-import-meta-dirname` migration that rewrites existing configs. Vitest 4 removed the `vitest.workspace` file. To run the tests of multiple projects from the workspace root, list them in the `test.projects` option of a root `vitest.config.ts`, which is also what Nx 23.2 generates: ```ts import { defineConfig } from 'vitest/config'; export default defineConfig({ test: { projects: ['apps/**/vite.config.ts', 'libs/**/vite.config.ts'], }, }); ``` --- # Updating to the latest version URL: https://analogjs.org/docs/features/updating/overview # Updating to the latest version You can use the `ng update` command for an Angular CLI workspace, or the `nx migrate` command for updating within an Nx workspace. ### ng update To update an Analog project with the `ng update` command: ```shell ng update @analogjs/platform@latest ``` ### Nx Migrate To update an Analog project with the `nx migrate` command: ```shell nx migrate @analogjs/platform@latest ``` --- # Getting Started URL: https://analogjs.org/docs/getting-started # Getting Started Creating an Analog project can be done with minimal steps. ## System Requirements Analog requires the following Node and Angular versions: - Node 22 and higher is recommended - Angular v17 or higher ## Creating a New Application To create a new Analog project, you can use the `create-analog` package with your package manager of choice: ```shell npm create analog@latest ``` ```shell yarn create analog ``` ```shell pnpm create analog ``` ```shell bun create analog ``` You can also [scaffold a new project with Nx](/docs/integrations/nx). ### Serving the application To start the development server for the application, run the `start` command. ```shell npm run start ``` ```shell yarn start ``` ```shell pnpm start ``` ```shell bun start ``` Visit [http://localhost:5173](http://localhost:5173) in your browser to view the running application. Next, you can [define additional routes using components](/docs/features/routing/overview) for navigation. ### Building the Application To build the application for deployment ```shell npm run build ``` ```shell yarn build ``` ```shell pnpm run build ``` ```shell bun run build ``` ### Build Artifacts By default, Analog comes with [Server-Side Rendering](/docs/features/server/server-side-rendering) enabled. Client artifacts are located in the `dist/analog/public` directory. The server for the API/SSR build artifacts is located in the `dist/analog/server` directory. ## Migrating an Existing Application You can also migrate an existing Angular application to Analog. See the [migration guide](/docs/guides/migrating) for migration steps. --- # Angular Compilation URL: https://analogjs.org/docs/guides/angular-compilation # Angular Compilation The AnalogJS Vite Plugin (`@analogjs/vite-plugin-angular`) compiles your Angular components, directives, and pipes to Ivy definitions during the Vite build. It provides two compilation paths: - **Default compilation**: Angular's own compiler (`ngtsc`), with full compile-time type checking. - **Fast compile**: an in-tree single-pass compiler that produces the same Ivy output much faster. Both emit identical Ivy code and identical runtime behavior, because both ultimately call the same `@angular/compiler` lowering APIs. :::tip For background on why compilation, type-checking, and build times are in tension, read [Angular Compilation, Type-Checking, and Build Bottlenecks](https://dev.to/brandontroberts/angular-compilation-type-checking-and-build-bottlenecks-4n2f). ::: ## Default compilation This is what you get out of the box with no configuration required: ```ts // vite.config.ts import { defineConfig } from 'vite'; import angular from '@analogjs/vite-plugin-angular'; export default defineConfig({ plugins: [angular()], }); ``` The default path runs Angular's compiler through the Angular Compilation API. It performs **full TypeScript and template type checking** (including `strictTemplates`), emits declaration files, and supports i18n message extraction. Use it when you want compile-time safety: wrong template bindings, missing inputs, and type mismatches fail the build. ### Editing multiple files During development, the default compilation path groups component and resource changes received within a 100 ms quiet window into one compilation. Reload and HMR notifications are coalesced after compilation, so saving several files at once avoids compiling separately for every file. Changes received during a compilation are queued for the next batch. This behavior requires no configuration. ## Fast compile Fast compile swaps Angular's compiler for an in-tree, single-pass transform. Instead of constructing a full `ts.Program`, it extracts each file's decorator metadata and generates Ivy definitions directly through `@angular/compiler`'s lowering APIs. It produces equivalent Ivy output and reduces cold-build and hot-rebuild times. ### Enabling Opt in via the `angular()` plugin: ```ts // vite.config.ts import { defineConfig } from 'vite'; import angular from '@analogjs/vite-plugin-angular'; export default defineConfig({ plugins: [ angular({ fastCompile: true, }), ], }); ``` :::note In an Analog application, configure the plugin through `@analogjs/platform` instead. Its `analog()` plugin forwards the same `fastCompile` option: ```ts import analog from '@analogjs/platform'; export default defineConfig({ plugins: [analog({ fastCompile: true })], }); ``` ::: ### What it supports | Capability | Fast compile | | -------------------------------------------- | --------------------------------------------------------------- | | AOT compilation (`Ι΅Ι΅defineComponent`) | βœ… | | JIT compilation | βœ… | | Inline `template:` / `styles:` | βœ… Inline SCSS / Sass / Less is preprocessed before compilation | | External `templateUrl` / `styleUrl` | βœ… Resolved and preprocessed via Vite | | Hot Module Replacement (HMR) | βœ… | | Style encapsulation | βœ… | | Cross-file selector resolution | βœ… Via an in-tree component registry | | Library / partial builds (`fastCompileMode`) | βœ… Emits partial declarations for library publishing | | Compile-time template type checking | ❌ Use the Angular Language Service in your editor | ### Boundary blocks Fast compilation supports `@boundary` and `@error` blocks when the installed `@angular/compiler` supports them (verified with `22.2.0-next.7`). Deferred components inside error blocks are loaded lazily, while components also used outside a `@defer` body remain eager dependencies. Older compilers, including Angular 22.1, do not support this syntax. ### Building a library For a library build, set `fastCompileMode: 'partial'` so the compiler emits partial `Ι΅Ι΅ngDeclare*` declarations instead of final Ivy definitions: ```ts angular({ fastCompile: true, fastCompileMode: 'partial', }); ``` See [Building an Angular library](/docs/guides/libraries) for the full library workflow. ## Type checking Fast compile does not type-check your templates or TypeScript. To catch those errors, run Angular's compiler as a separate verification step against your app's tsconfig. Pass `--noEmit` so it only reports errors without writing output: ```bash ngc -p tsconfig.app.json --noEmit ``` Add it as a script and run it alongside your build: ```json // package.json { "scripts": { "typecheck": "ngc -p tsconfig.app.json --noEmit" } } ``` This keeps fast builds during development while still failing on template and type errors before you ship, and the Angular Language Service surfaces the same errors live in your editor. ### Nx workspaces In an Nx workspace, [`angular-typechecker`](https://www.npmjs.com/package/angular-typechecker) provides an Nx executor that runs the complete Angular type-check: TypeScript checks, template type-checking, and the extended `NG8xxx` diagnostics. It emits nothing and runs separately from your build and tests. Nx caches the result per project. Add it with `nx add`, which installs the package and runs its `angular-typechecker:init` generator to seed a cacheable `angular-typechecker:typecheck` entry in `nx.json` under `targetDefaults`: ```bash nx add angular-typechecker ``` Then wire a `typecheck` target into the project you want to check: ```bash nx g angular-typechecker:configuration my-app ``` Run it: ```bash nx typecheck my-app ``` The generator writes this target. Add `includeDeps: true` to also report diagnostics from dependencies: ```jsonc // apps/my-app/project.json { "targets": { "typecheck": { "executor": "angular-typechecker:typecheck", "options": { "tsConfig": "apps/my-app/tsconfig.json", "includeDeps": true, }, }, }, } ``` ## Compatibility ### Angular versions Fast compile is validated against Angular `17`, `18`, `19`, `20`, `21`, `22`, and `next` on every PR. Components that use `@defer` at runtime require Angular 18+. For overall Angular/Analog/Vite version support, see [Version Compatibility](/docs/guides/compatibility). ### Default vs fast compile | | Default | Fast compile | | ----------------------------------- | ----------------------- | ------------------------------------- | | Ivy output | Full | Identical | | Cold build / hot rebuild | Standard | Significantly faster | | Compile-time template type checking | Yes (`strictTemplates`) | No (use the Angular Language Service) | | i18n message extraction | Yes | `$localize` emitted | | Library / partial builds | Yes | Yes (`fastCompileMode: 'partial'`) | ## Trade-offs :::caution Fast compile trades compile-time safety for speed. **No compile-time template type checking.** Fast compile emits identical Ivy output and identical runtime behavior, but it does not validate template bindings, inputs, or types at build time. Run the **Angular Language Service** in your editor to catch these errors as you type; they appear as editor squiggles instead of failing the build. If you need template type errors to fail your build (for example in CI), add the [type-checking step](#type-checking) or use the default compilation path. ::: --- # Version Compatibility URL: https://analogjs.org/docs/guides/compatibility # Version Compatibility Analog is compatible across multiple versions of Angular and Vite. Its always recommended to use the latest version of Analog because there are bug fixes and features continually being added. Below is a reference table that matches compatible versions of Angular with the Analog and Vite. # Angular Version Support | Angular Version | Analog Version | Vite Version | | --------------- | -------------- | ------------ | | ^21.0.0 | **latest** | ^7.0.0 | | ^20.0.0 | **latest** | ^7.0.0 | | ^19.1.0 | **latest** | ^6.0.0 | | ^18.0.0 | **latest** | ^5.0.0 | | ^17.0.0 | **latest** | ^5.0.0 | You can also [migrate and existing Angular Single Page Application](/docs/guides/migrating) to Analog. # Nx Version Support Using Nx is not a requirement to use Analog. Analog uses the Nx Devkit for supporting Nx workspaces and Angular CLI workspaces The table shows the minimum version of Nx, the supported Angular version, and the minimum supported version of Analog and Vite. | Nx Version _(min)_ | Angular Version | Analog Version | Vite Version | | ------------------ | --------------- | -------------- | ------------ | | 22.x.x | ^22.x.x | **latest** | ^8.0.0 | | 22.x.x | ^21.x.x | **latest** | ^7.0.0 | | 22.x.x | ^20.x.x | **latest** | ^7.0.0 | | 21.x.x | ^20.x.x | **latest** | ^7.0.0 | | 20.4.0 | ^19.1.0 | **latest** | ^6.0.0 | | 18.0.0 | ^17.0.0 | **latest** | ^5.0.0 | | 17.0.0 | ^17.0.0 | **latest** | ^5.0.0 | Additionally, you can check this [guide from Nx](https://nx.dev/packages/angular/documents/angular-nx-version-matrix) to learn more about Nx and Angular compatibility. --- # Form Server Actions URL: https://analogjs.org/docs/guides/forms # Form Server Actions Analog supports server-side handling of form submissions and validation.
## Setting up the Form To handle form submissions, use the `FormAction` directive from the `@analogjs/router` package. The directive collects `FormData` and handles GET navigation or POST submission to the current page. The directive emits after processing the form: - `onSuccess`: when the form is processing on the server and returns a success response. - `onError`: when the form returns an error response. - `state`: emits `submitting`, `success`, `error`, `redirect`, or `navigate` as the submission progresses. The example page below submits an email for a newsletter signup. ```ts // src/app/pages/newsletter.page.ts type FormErrors = | { email?: string; } | undefined; @Component({ selector: 'app-newsletter-page', imports: [FormAction], template: `

Newsletter Signup

@if (!signedUp()) {
@if (errors()?.email) {

{{ errors()?.email }}

} } @else {
Thanks for signing up!
} `, }) export default class NewsletterComponent { signedUp = signal(false); errors = signal(undefined); onSuccess() { this.signedUp.set(true); } onError(result?: FormErrors) { this.errors.set(result); } } ``` The `FormAction` directive submits the form data to the server, which is processed by its handler. ### Opting Into Enhanced Forms Set `[enhanceForm]="true"` to enable enhanced form handling. It defaults to `false` in v2, so existing forms keep their current behavior. Enhanced handling is planned to become the default in v3; `[enhanceForm]="false"` explicitly selects the existing behavior. ```html
``` Import `FormAction` in the component's `imports`, as with existing forms. The flag configures the same directive. It enables all of the following: - Use `action` or `[action]` as the submission destination. Without an action, POST uses the current page endpoint and GET uses the current route. - Preserve repeated GET fields as multiple query values. Submitted fields replace existing values with the same name; other destination query parameters and the fragment are retained. - Preserve complete redirect URLs, including query parameters and fragments. Same-origin GET destinations and redirects use Angular navigation with `onSameUrlNavigation: 'reload'`; external destinations use browser navigation. - Set `data-state="idle"` initially and update it with submission state. Set `aria-busy="true"` while submitting, then remove it when processing completes. - Emit `error` after network or response-parsing failures, clearing busy state. The directive manages `data-state` and `aria-busy` only while enhancement is enabled. Disabling it restores the previous attribute values. Use the `state` output to track progress in either mode; `FormActionState` is exported for typing state handlers. With the flag omitted or set to `false`, forms retain beta's page endpoint, GET query replacement and last-value handling for repeated fields, and pathname-only Angular redirects. The directive does not modify state attributes. ## Handling the Form Action To handle the form action, define the `.server.ts` alongside the `.page.ts` file that contains the async `action` function to process the form submission. In the server action, you can use access environment variables, read cookies, and perform other server-side only operations. ```ts // src/app/pages/newsletter.server.ts export async function action({ event }: PageServerAction) { const body = await readFormData(event); const email = body.get('email') as string; if (!email) { return fail(422, { email: 'Email is required' }); } if (email.length < 10) { return redirect('/'); } return json({ type: 'success' }); } ``` - The `json` function returns a JSON response. - The `redirect` function returns a redirect response to the client. This should be an absolute path. - The `fail` function is used for returning form validation errors. ### Validating a Form Action with a Schema Use `defineAction` to parse JSON or form data and validate it with a Standard Schema-compatible library, such as Zod 3.24+ or Valibot. The handler receives the schema's inferred output type, including transformed values. ```ts // src/app/pages/newsletter.server.ts import { defineAction, json } from '@analogjs/router/server/actions'; import { z } from 'zod'; export const action = defineAction({ schema: z.object({ email: z.string().email() }), handler: ({ data }) => json({ email: data.email }), }); ``` An optional `params` schema validates route parameters. The handler also receives `params`, `req`, `res`, `fetch`, and `event`, just like a regular `PageServerAction`. Both synchronous and asynchronous schemas are supported. Without a `schema`, `data` contains the parsed request body. Invalid input returns HTTP 422 with the `X-Analog-Errors` header and an array of Standard Schema issues. The handler is not called. The existing `FormAction` directive emits this array through `onError`; each issue includes a `message` and an optional `path`. Existing actions returning `fail()` keep their own error shape. Repeated form fields are preserved as arrays, including file fields. Empty or unparseable bodies fall back to `{}`, which is then validated by the schema. ### Displaying Validation Errors Use `issuesToFieldErrors` and `issuesToFormErrors` to display the issues returned by `defineAction`. Field paths become dot-separated names, and multiple messages for the same field remain in order. Issues without a path are form-level errors. ```ts import { signal } from '@angular/core'; import { issuesToFieldErrors, issuesToFormErrors, type ValidationFieldErrors, } from '@analogjs/router'; import type { StandardSchemaV1 } from '@analogjs/router/server/actions'; // Inside the component handling a defineAction form: fieldErrors = signal({}); formErrors = signal([]); onError(result: unknown) { // This form's defineAction handler returns Standard Schema issues. const issues = result as ReadonlyArray; this.fieldErrors.set(issuesToFieldErrors(issues)); this.formErrors.set(issuesToFormErrors(issues)); } ``` Bind `(onError)="onError($event)"` on the form and render the messages: ```html @for (message of fieldErrors()['email'] ?? []; track $index) {

{{ message }}

} @for (message of formErrors(); track $index) {

{{ message }}

} ``` `issuePathToFieldName(['profile', { key: 'name' }, 0])` returns `'profile.name.0'` when you need to normalize an individual issue path. These helpers accept Standard Schema issue arrays; existing actions that return custom error objects with `fail()` can keep their existing error handlers. Field names use dots to separate path segments without escaping. A literal key such as `['profile.name']` and a nested path `['profile', 'name']` both map to `'profile.name'`. If your schema distinguishes these keys, use the original issue paths to display their messages separately. ### Handling Multiple Forms To handle multiple forms on the same page, add a hidden input to distinguish each form. ```html
``` In the server action, use the `action` value. ```ts export async function action({ event }: PageServerAction) { const body = await readFormData(event); const action = body.get('action') as string; if (action === 'register') { // process register form } } ``` ## Handling GET Requests Forms with a `GET` action can be used to navigate to the same URL, with the form inputs passed as query parameters. The example below defines a search form with the `search` field as a query param. ```ts // src/app/pages/search.page.ts @Component({ selector: 'app-search-page', imports: [FormAction], template: `

Search

@if (searchTerm()) {

Search Term: {{ searchTerm() }}

} `, }) export default class NewsletterComponent { loader = toSignal(injectLoad(), { requireSync: true }); searchTerm = computed(() => this.loader().searchTerm); } ``` The query parameter can be accessed through the server form action. ```ts // src/app/pages/search.server.ts export async function load({ event }: PageServerLoad) { const query = getQuery(event); console.log('loaded search', query['search']); return { loaded: true, searchTerm: `${query['search']}`, }; } ``` --- # Building an Angular Library URL: https://analogjs.org/docs/guides/libraries # Building an Angular Library Angular libraries are built for supporting many different services and functionality. Angular libraries can be built using Vite that can be published to npm. ## Creating a Library If you are creating a new package, use the `library` schematic: ```sh ng generate lib my-lib ``` For an existing library, follow the setup instructions. ## Setup Install the `@analogjs/platform` package: ```shell npm install @analogjs/platform --save-dev ``` ```shell yarn add @analogjs/platform --dev ``` ```shell pnpm install -w @analogjs/platform ``` Next, create a `vite.config.ts` at the root of the project, and configure it to build the library. > Update the references to `my-lib` to match the library project name. ```ts export default defineConfig(({ mode }) => ({ root: __dirname, cacheDir: '../../node_modules/.vite/libs/my-lib', plugins: [angular()], resolve: { mainFields: ['module'], }, build: { target: ['esnext'], sourcemap: true, lib: { // Library entry point entry: 'src/public-api.ts', // Package output path, must contain fesm2022 fileName: `fesm2022/my-lib`, // Publish as ESM package formats: ['es'], }, rollupOptions: { // Add external libraries that should be excluded from the bundle external: [/^@angular\/.*/, 'rxjs', 'rxjs/operators'], output: { // Produce a single file bundle preserveModules: false, }, }, minify: false, }, })); ``` Next, update the project configuration to use the `@analogjs/platform:vite` builder to build the library. ```json { "name": "my-lib", "$schema": "../../node_modules/nx/schemas/project-schema.json", "sourceRoot": "projects/my-lib/src", "prefix": "lib", "projectType": "library", "architect": { "build": { "builder": "@analogjs/platform:vite", "options": { "configFile": "projects/my-lib/vite.config.ts", "outputPath": "dist/projects/my-lib" }, "defaultConfiguration": "production", "configurations": { "development": { "mode": "development" }, "production": { "sourcemap": true, "mode": "production" } } } } } ``` Adjust the `package.json` at the root of the project to point to the built output. Include any necessary `dependencies` or `peerDependencies` that are needed when installing the package. ```json { "name": "my-lib", "description": "A description of the Angular library", "type": "module", "peerDependencies": { "@angular/common": "^19.0.0", "@angular/core": "^19.0.0" }, "dependencies": { "tslib": "^2.0.0" }, "types": "./src/public-api.d.ts", "exports": { "./package.json": { "default": "./package.json" }, ".": { "import": "./fesm2022/my-lib.mjs", "require": "./fesm2022/my-lib.mjs", "default": "./fesm2022/my-lib.mjs" } }, "sideEffects": false, "publishConfig": { "access": "public" } } ``` ## Copying Assets Static assets in the `public` directory are copied to the build output directory by default. If you want to copy additional assets outside of that directory, use the `vite-plugin-static-copy` Vite plugin. First, install the `vite-plugin-static-copy` package. ```shell npm install vite-plugin-static-copy --save-dev ``` ```shell yarn add vite-plugin-static-copy --dev ``` ```shell pnpm install -w vite-plugin-static-copy --save-dev ``` Next, import the plugin and set it up: ```ts /// import { viteStaticCopy } from 'vite-plugin-static-copy'; // https://vitejs.dev/config/ export default defineConfig(({ mode }) => ({ // ... plugins: [ angular(), viteStaticCopy({ targets: [{ src: ['*.md', 'package.json'], dest: '.' }], }), ], })); ``` ## Building the Library Run the build command: ```sh ng build my-lib ``` ## Publishing the Library After logging using `npm login`, use the `npm publish` command to publish the package. To see the output without publishing, use the `--dry-run` flag. ```sh npm publish dist/projects/my-lib --dry-run ``` To publish the library to npm: ```sh npm publish dist/projects/my-lib ``` --- # Migrating an Angular app to Analog URL: https://analogjs.org/docs/guides/migrating # Migrating an Angular app to Analog An existing Angular Single Page Application can be configured to use Analog using a schematic/generator for Angular CLI or Nx workspaces. > Analog is compatible with Angular v17 and above. ## Using a Schematic/Generator First, install the `@analogjs/platform` package: ```shell npm install @analogjs/platform --save-dev ``` ```shell yarn add @analogjs/platform --dev ``` ```shell pnpm install -w @analogjs/platform ``` Next, run the command to set up the Vite config, update the build/serve targets in the project configuration, move necessary files, and optionally set up Vitest for unit testing. ```shell npx ng generate @analogjs/platform:migrate --project [your-project-name] ``` For Nx projects: ```shell npx nx generate @analogjs/platform:migrate --project [your-project-name] ``` ## Updating Global Styles and Scripts If you have any global scripts or styles configured in the `angular.json`, reference them inside the `head` tag in the `index.html`. ```html My Analog app ``` ## Setting Up Environments In an Angular application, `fileReplacements` are configured in the `angular.json` for different environments. ### Using Environment Variables In Analog, you can setup and use environment variables. This is the **recommended** approach. Add a `.env` file to the root of your application, and prefix any **public** environment variables with `VITE_`. **Do not** check this file into your source code repository. ```sh VITE_MY_API_KEY=development-key # Only available in the server build MY_SERVER_API_KEY=development-server-key ``` Import and use the environment variable in your code. ```ts @Injectable({ providedIn: 'root', }) export class AuthService { private readonly apiKey = import.meta.env['VITE_MY_API_KEY']; constructor(private http: HttpClient) {} } ``` When deploying, set up your environment variables to their production equivalents. ```sh VITE_MY_API_KEY=production-key # Only available in the server build MY_SERVER_API_KEY=production-server-key ``` Read [here](https://vitejs.dev/guide/env-and-mode.html) for about more information on environment variables. ### Using File Replacements You can also use the `fileReplacements` option to replace files. ```ts /// // https://vitejs.dev/config/ export default defineConfig(({ mode }) => ({ build: { target: ['es2020'], }, resolve: { mainFields: ['module'], }, plugins: [ analog({ fileReplacements: mode === 'production' ? [ { replace: 'src/environments/environment.ts', with: 'src/environments/environment.prod.ts', }, ] : [], }), ], test: { globals: true, environment: 'jsdom', setupFiles: ['src/test-setup.ts'], include: ['**/*.spec.ts'], reporters: ['default'], }, define: { 'import.meta.vitest': mode !== 'production', }, })); ``` ## Copying Assets Static assets in the `public` directory are copied to the build output directory by default. If you want to copy additional assets outside of that directory, use the `vite-plugin-static-copy` Vite plugin. First, install the `vite-plugin-static-copy` package. ```shell npm install vite-plugin-static-copy --save-dev ``` ```shell yarn add vite-plugin-static-copy --dev ``` ```shell pnpm install -w vite-plugin-static-copy --save-dev ``` Next, import the plugin and set it up: ```ts /// import { viteStaticCopy } from 'vite-plugin-static-copy'; // https://vitejs.dev/config/ export default defineConfig(({ mode }) => ({ // ... plugins: [ analog(), viteStaticCopy({ targets: [{ src: '*.md', dest: '.' }], }), ], })); ``` ## Enabling HMR Angular supports HMR/Live reload where in most cases components can be updated without a page reload. To enable it in Analog, use the `liveReload: true` option. ```ts /// // https://vitejs.dev/config/ export default defineConfig(({ mode }) => ({ // .. other configuration plugins: [ analog({ liveReload: true, }), ], })); ``` --- # Build with AI URL: https://analogjs.org/docs/integrations/ai/index # Build with AI Analog provides integrations to use AI-assisted development practics. ## LLMs Index files Analog's docs site publishes two AI-friendly index files at the site root: - `https://analogjs.org/llms.txt` - `https://analogjs.org/llms-full.txt` These files make it easier to feed the docs into AI-assisted workflows without crawling the full site manually. ### What's the difference? #### `llms.txt` `llms.txt` is a compact index of the docs. It contains the page titles, URLs, and short descriptions so an assistant or retrieval pipeline can discover the relevant docs pages quickly. Use it when you want: - a lightweight entry point for retrieval - a page index for custom RAG pipelines - a quick way to point an AI tool at the Analog docs corpus #### `llms-full.txt` `llms-full.txt` is the expanded version. It concatenates the full Markdown content for the docs pages into a single text file. Use it when you want: - a single file for local indexing - a fuller context window for long-form prompting - offline processing without fetching each docs page individually ## Per-page Markdown Every docs page is also published as raw Markdown at its `.md` URL. Append `.md` to any docs page URL to fetch a clean, chrome-free Markdown version of just that page: - `https://analogjs.org/docs/features/routing/overview`: the rendered page - `https://analogjs.org/docs/features/routing/overview.md`: the raw Markdown Use it when you want: - a single page as context instead of the full corpus - an addressable source an agent can fetch on demand - Markdown without the site navigation and other page chrome Every docs page also has a **Copy page** button next to its title that copies the page as Markdown to the clipboard β€” handy for pasting into an AI assistant with formatting intact β€” with a dropdown to view the raw Markdown at the page's `.md` URL. ## Section indexes In addition to the site-wide `llms.txt`, each multi-page docs section publishes its own scoped index at `llms.txt` under the section path, for example: - `https://analogjs.org/docs/features/llms.txt` - `https://analogjs.org/docs/integrations/llms.txt` - `https://analogjs.org/docs/packages/llms.txt` Use a section index when you only need part of the docs, for example pointing an assistant at the integrations docs without pulling in the entire corpus. ## How Analog generates these files The docs site generates these files automatically during its build: - `llms.txt`: an index of the docs pages under `src/content` - a scoped `llms.txt` for each multi-page section, from the same content - `llms-full.txt`: the Markdown bodies of those pages concatenated into one file - a per-page `.md` file emitted alongside each prerendered route, via Analog's `outputSourceFile` prerender option That means the files stay aligned with the published docs instead of requiring a separate export step. ## Framework conventions for coding agents `@analogjs/platform` ships its own guidance for AI coding assistants, so it tracks the version you have installed instead of drifting from a copy checked into your project. - `node_modules/@analogjs/platform/AGENTS.md` β€” Markdown guidance for any client that reads `AGENTS.md`. Generated apps include a root `AGENTS.md` that points here. - `node_modules/@analogjs/platform/plugin.json` β€” an [Agent Plugins v1.0.0](https://github.com/agentplugins/agent-plugins-spec) manifest, with the same guidance as a discoverable skill at `skills/analogjs/SKILL.md`. Both cover file-based routing, server and API routes, data fetching, content routes, and the modern Angular style Analog expects. Agent Plugins v1 has no discovery mechanism for plugins installed in `node_modules`, so register the plugin root with your client by hand: ```text node_modules/@analogjs/platform ``` The `AGENTS.md` pointer keeps working with no setup either way. ## Example workflows ### Point an assistant at the docs index Use `llms.txt` when your AI tool supports a remote docs index: ```text Use https://analogjs.org/llms.txt as the primary AnalogJS documentation index. ``` ### Build a local retrieval corpus Use `llms-full.txt` when you want one source file for embeddings or local search: ```shell curl -O https://analogjs.org/llms-full.txt ``` ### Combine with normal docs links The AI-oriented files are a supplement, not a replacement for the published docs UI. Keep linking users to the canonical docs pages when you want navigable documentation, and use the `llms` files when you want AI-friendly ingestion. --- # Angular Material Integration with Analog URL: https://analogjs.org/docs/integrations/angular-material/index # Angular Material Integration with Analog This tutorial will guide you through the process of integrating the [Angular Material](https://material.angular.io) component library into your Analog application. ## Step 1: Installing the Angular Material library To begin, you need to install the `@angular/cdk` and `@angular/material` packages. Depending on your preferred package manager, run one of the following commands: ```shell npm install @angular/cdk @angular/material ``` ```shell yarn add @angular/cdk @angular/material ``` ```shell pnpm install @angular/cdk @angular/material ``` ## Step 2: Configuring the Angular Material library 1. Rename the `src/styles.css` file to `src/styles.scss`. 2. If you're using `zone.js`, configure the `scss` preprocessorOptions to use the `legacy` api. 3. Set the `inlineStylesExtension` property to `'scss'` in the `vite.config.ts` file: ```ts export default defineConfig(({ mode }) => { return { css: { preprocessorOptions: { scss: { api: 'legacy', }, }, }, plugins: [ analog({ inlineStylesExtension: 'scss', }), ], }; }); ``` 4. Update the `index.html` file to reference the SCSS file: ```html ``` 5. Update the `styles.scss` file to import the Angular Material styles and define your custom theme: ```scss @use '@angular/material' as mat; html { color-scheme: light dark; @include mat.theme( ( color: mat.$violet-palette, typography: Roboto, density: 0, ) ); } body { font-family: Roboto, 'Helvetica Neue', sans-serif; margin: 0; padding: 30px; height: 100%; } html { height: 100%; } ``` ## Optional Step: Configuring Animations If you want to activate or deactivate animations where needed, follow the correspondent steps: 1. Open the `app.config.ts` file and add `provideAnimations()` as a provider ```ts providers: [ // other providers provideAnimations(), ], ``` 2. Open the `app.config.server.ts` file and add `provideNoopAnimations()` as a provider ```ts providers: [ // other providers provideNoopAnimations(), ], ``` ## Optional Step: Configuring Tailwind CSS If you use Tailwind CSS, set up PostCSS to make it work correctly with Angular Material: 1. **Install the Tailwind PostCSS plugin:** ```shell npm install @tailwindcss/postcss ``` 2. **Create a `.postcssrc.json` file:** ```json { "plugins": { "@tailwindcss/postcss": {} } } ``` > **Note:** Use `@use 'tailwindcss';` instead of the deprecated `@import 'tailwindcss';`. With these steps, you have configured animations to be enabled on the client and disabled on the server in your Analog application. That's it! You have successfully installed and configured the Angular Material library for your Analog application. You can now start utilizing the Angular Material components and styles in your project. For more information on theming with Angular Material, refer to the [Angular Material Theming Guide](https://material.angular.io/guide/theming). --- # Ionic Framework Integration with Analog URL: https://analogjs.org/docs/integrations/ionic/index # Ionic Framework Integration with Analog This tutorial guides you through the process of integrating Ionic Framework within your Analog application so you can leverage the power of Ionic's iOS and Android components in your applications. ## Step 1: Install Ionic Framework To begin, you need to install the `@ionic/angular@latest` package. Depending on your preferred package manager, run one of the following commands: ```shell npm install @ionic/angular@latest ``` ```shell yarn add @ionic/angular@latest ``` ```shell pnpm install @ionic/angular@latest ``` ### Optional: Install Ionic Angular Toolkit for schematics Ionic also offers a set of schematics that can help you create components following the Ionic structure. You can add them by installing the `@ionic/angular-toolkit` package to your devDependencies ```shell npm install -D @ionic/angular-toolkit ``` ```shell yarn add -D @ionic/angular-toolkit ``` ```shell pnpm install -D @ionic/angular-toolkit ``` ### Optional: Install Ionicons: Ionic custom icons library Ionic offers too, an icon library that brings more than 500 icons for most of your mobile application needs. You can install them by adding the `ionicons` package to your project: ```shell npm install ionicons ``` ```shell yarn add ionicons ``` ```shell pnpm install ionicons ``` ## Step 2: Configuring Ionic Framework in your application 1. Update your `vite.config.ts` file to include Ionic packages in the **SSR** process, adding them to the `noExternal` array. `ionicons` is required only if you installed the ionicons package. If you use Vitest, inline the `@ionic/angular` package to allow Vitest to build that package properly for Vitest. ```ts export default defineConfig(({ mode }) => { return { // ... // add these lines ssr: { noExternal: ['@ionic/**', '@stencil/**', 'ionicons'], }, // ... // add these lines if you use Vitest test: { server: { deps: { inline: ['@ionic/angular'], }, }, }, }; }); ``` 2. Add in your `app.config.ts` the `provideIonicAngular` method and `IonicRouteStrategy` provider. ```ts export const appConfig: ApplicationConfig = { providers: [ provideFileRouter(), provideClientHydration(), provideHttpClient(withFetch()), { provide: RouteReuseStrategy, useClass: IonicRouteStrategy }, provideIonicAngular(), ], }; ``` 3. Update your `app.component.ts` file to set in the template the required Ionic tags. You will need to look at [Server Side Rendering caveat](#server-side-rendering-caveat) as [Ionic doesn't yet support client hydration](https://github.com/ionic-team/ionic-framework/issues/28625#issuecomment-1843919548) ```ts @Component({ selector: 'demo-root', imports: [IonApp, IonRouterOutlet], template: ``, }) export class AppComponent {} ``` 4. Rename the file `styles.css` to `styles.scss`. 5. Set the `inlineStylesExtension` property to `'scss'` in the `vite.config.ts` file: ```ts export default defineConfig(({ mode }) => { return { plugins: [ analog({ inlineStylesExtension: 'scss', }), ], }; }); ``` 6. Update the `index.html` file to reference the SCSS file, and to include the required meta tags for Ionic apps: ```html ``` 7. Update the `styles.scss` file to import the Ionic styles and define your [custom theme](https://ionicframework.com/docs/theming/color-generator): ```scss /* Core CSS required for Ionic components to work properly */ @import '@ionic/angular/css/core.css'; /* Basic CSS for apps built with Ionic */ @import '@ionic/angular/css/normalize.css'; @import '@ionic/angular/css/structure.css'; @import '@ionic/angular/css/typography.css'; @import '@ionic/angular/css/display.css'; /* Optional CSS utils that can be commented out */ @import '@ionic/angular/css/padding.css'; @import '@ionic/angular/css/float-elements.css'; @import '@ionic/angular/css/text-alignment.css'; @import '@ionic/angular/css/text-transformation.css'; @import '@ionic/angular/css/flex-utils.css'; /** * Ionic Dark Mode * ----------------------------------------------------- * For more info, please see: * https://ionicframework.com/docs/theming/dark-mode */ /* @import "@ionic/angular/css/palettes/dark.always.css"; */ /* @import "@ionic/angular/css/palettes/dark.class.css"; */ @import '@ionic/angular/css/palettes/dark.system.css'; ``` ### Server Side Rendering Caveat Ionic Framework [doesn't support Angular's new Client Hydration](https://github.com/ionic-team/ionic-framework/issues/28625#issuecomment-1843919548), as Angular [doesn't support SSR with web components](https://github.com/angular/angular/issues/52275), and when they are supported, work has to be done on the Stencil components to enable it. So right now there are three options to handle this: 1. Remove `provideClientHydration()` from `app.config.ts` providers. - This removes the new client hydration mechanism from Angular and reverts to the previous one, which will cause a flicker when re-rendering the DOM from the client. ```ts export const appConfig: ApplicationConfig = { providers: [ provideFileRouter(), //provideClientHydration(), // remove this. provideHttpClient(withFetch()), { provide: RouteReuseStrategy, useClass: IonicRouteStrategy }, provideIonicAngular(), ], }; ``` 2. Add `ngSkipHydration` attribute to the `ion-app` tag. - This will disable the client hydration mechanism for the `ion-app` element and children, but will continue to use client hydration on other elements. This will also cause a flicker in the page for the Ionic components. This is not that helpful for other elements/components as, with Ionic apps, all your Ionic components exist inside the `ion-app` tag. ```ts @Component({ selector: 'demo-root', imports: [IonApp, IonRouterOutlet], template: ` `, }) export class AppComponent {} ``` 3. Disable SSR completely - Disable SSR in the `vite.config.ts` file. This **will eliminate the flickering** but you will lose all the benefits of having SSR in your app. ```ts plugins: [ analog({ ssr: false, }), ], ``` You **must** pick one of the previous options, as not configuring this will make your app throw errors on runtime, like the following: ```js ERROR Error: NG0500: During hydration Angular expected but found a comment node. Angular expected this DOM: … <-- AT THIS LOCATION … Actual DOM is: <-- AT THIS LOCATION … Note: attributes are only displayed to better represent the DOM but have no effect on hydration mismatches. To fix this problem: * check the "AppComponent" component for hydration-related issues * check to see if your template has valid HTML structure * or skip hydration by adding the `ngSkipHydration` attribute to its host node in a template ``` ## Step 3: Adding Capacitor (Optional) Capacitor allows you to create web native applications that can be run on iOS and Android devices with ease. ### Step 3.1 Install and configure your Capacitor app 1. First, you need to install the `@capacitor/core` and `@capacitor/cli` packages. Depending on your preferred package manager, run one of the following commands: ```shell npm install @capacitor/core npm install -D @capacitor/cli ``` ```shell yarn add @capacitor/core yarn add -D @capacitor/cli ``` ```shell pnpm install @capacitor/core pnpm install -D @capacitor/cli ``` 2. Then you have to initialize the Capacitor project with the following command. The CLI will ask you a few questions, starting with your app name, and the package ID you would like to use for your app. ```shell npx cap init ``` 3. Update `capacitor.config.ts` `webDir` property to point to the dist folder of analog build ```ts const config: CapacitorConfig = { appId: 'com.ionic.capacitor', appName: 'ionic-capacitor', webDir: 'dist/analog/public', }; export default config; ``` ### Step 3.2 Create your Android and iOS projects 1. Install the `@capacitor/android` and/or `@capacitor/ios` packages based on the platforms you want to support. ```shell npm install @capacitor/android npm install @capacitor/ios ``` ```shell yarn add @capacitor/android yarn add @capacitor/ios ``` ```shell pnpm install @capacitor/android pnpm install @capacitor/ios ``` 2. Add the Android and/or iOS project to your app ```shell npx cap add android npx cap add ios ``` 3. Sync the project files to the installed platforms ```shell npx cap sync ``` 4. You can run the app with the following commands ```shell npx cap run android npx cap run ios ``` --- That's it! You have successfully installed and configured Ionic Framework with (or without) Capacitor for your Analog application! --- # Nx URL: https://analogjs.org/docs/integrations/nx/index # Nx Analog provides integration with Nx monorepos and workspaces through a workspace preset and an application generator. An Analog application can be created as a standalone project or added to an existing Nx workspace. ## Overview [Nx](https://nx.dev) is a smart, fast, extensible build system with first class monorepo support and powerful integrations. ## Creating a Standalone Nx project To scaffold a standalone Nx project, use the `create-nx-workspace` command with the `@analogjs/platform` preset. Create a new Nx workspace with a preconfigured Analog application: ```shell npx create-nx-workspace@latest --preset=@analogjs/platform ``` The Analog preset prompts you to provide the name of your application. In this example, we simply use `analog-app`. Additionally, asks whether you would like to include [TailwindCSS](https://tailwindcss.com) in your new project. If you choose to include it, all the required dependencies are installed automatically, and any necessary configurations is added. ### Serving the application To start the development server for your application, run the `nx serve` command. ```shell npx nx serve analog-app ``` Navigate to `http://localhost:4200` in your browser to see the application running. ### Building the Application To build the application for deployment run: ```shell npx nx build analog-app ``` ### Build Artifacts The client build artifacts are located in the dist folder of your Nx workspace. In the standalone workspace layout, the `analog-app`'s client artifacts are located in the `dist/analog/public` directory. The server for the API/SSR build artifacts is located in the `dist/analog/server` directory. ## Adding to an existing Nx workspace An Analog application can be generated within an existing Nx workspace. To generate an application: First, install the `@analogjs/platform` package: ```shell npm install @analogjs/platform --save-dev ``` ```shell yarn add @analogjs/platform --dev ``` ```shell pnpm install @analogjs/platform --save-dev ``` Next, use the application generator to scaffold a new application: ```shell npx nx g @analogjs/platform:application analog-app ``` ### Generator Options Both the preset and the application generator accept the following options: | Option | Description | Default | | ------------- | ---------------------------------------------------------------------------------------------------- | -------- | | `addTailwind` | Adds [TailwindCSS](https://tailwindcss.com) and its configuration to the application. | `false` | | `linter` | The linter to set up for the application: `eslint`, `oxlint`, or `none`. `oxlint` requires Nx 23.2+. | `eslint` | | `tags` | Comma-separated Nx project tags, used with the `@nx/enforce-module-boundaries` lint rule. | | To generate an application linted with [oxlint](https://oxc.rs/docs/guide/usage/linter) through the `@nx/oxlint` plugin: ```shell npx nx g @analogjs/platform:application analog-app --linter=oxlint ``` ## Updating Nx Analog supports Nx 23.2 and newer inferred targets and migrations. Update the workspace with `nx migrate`, and run the migrations it generates: ```shell npx nx migrate latest npx nx migrate --run-migrations ``` Nx 23.2 adds the `--run-migration` flag to run a single migration by name, such as the `@nx/vitest` migration that replaces `__dirname` with `import.meta.dirname` in the `vite.config.ts` files: ```shell npx nx migrate --run-migration update-23-2-0-use-import-meta-dirname ``` ## Agent Context Both the preset and the application generator scaffold `AGENTS.md` and `CLAUDE.md` files that point AI coding assistants at the framework conventions shipped in `node_modules/@analogjs/platform/AGENTS.md`. The preset writes them at the workspace root, since the workspace is created for Analog. The application generator writes them in the application folder, such as `apps/analog-app`, so the guidance stays scoped to the Analog application in a workspace that may contain other projects. Existing `AGENTS.md` and `CLAUDE.md` files are never overwritten. --- # Using Storybook with Angular and Vite URL: https://analogjs.org/docs/integrations/storybook/index # Using Storybook with Angular and Vite [Storybook](https://storybook.js.org) is a frontend workshop for building UI components and pages in isolation. By default, Angular and Storybook uses Webpack to build and serve the Storybook application. This guides you through the process of switching to building and serving your Storybook with Angular using Vite using the AnalogJS Storybook integration. This package can be applied to _any_ Angular project using Storybook. > This is a community integration not maintained by the Storybook team. If you have issues, file an issue in our [GitHub repo](https://github.com/analogjs/analog/issues). ## Compatibility Guide The AnalogJS Storybook integration for using Angular and Vite supports multiple versions of Storybook. See the table below for which version to install based on the project dependencies. | Storybook Version | Analog Version | | ----------------- | -------------- | | ^10.0.0 | ^2.0.0 | | ^9.0.0 | ^1.22.0 | | ^8.6.0 | ^1.22.0 | ## Setting up Storybook If you don't have Storybook setup already, run the following command to initialize Storybook for your project: ```sh npx storybook@latest init ``` Follow the provided prompts, and commit your changes. ## Installing the Storybook package Install the Storybook Integration for Angular and Vite. Depending on your preferred package manager, run one of the following commands: ```shell npm install @analogjs/storybook-angular @analogjs/vite-plugin-angular --save-dev ``` ```shell yarn add @analogjs/storybook-angular @analogjs/vite-plugin-angular --dev ``` ```shell pnpm install @analogjs/storybook-angular @analogjs/vite-plugin-angular -w --save-dev ``` ```shell bun install @analogjs/storybook-angular @analogjs/vite-plugin-angular --save-dev ``` ## Configuring Storybook Update the `.storybook/main.ts` file to use the `StorybookConfig` type. Also update the `framework` to use the `@analogjs/storybook-angular` package. ```ts const config: StorybookConfig = { // other config, addons, etc. framework: { name: '@analogjs/storybook-angular', options: {}, }, }; export default config; ``` Remove the existing `webpackFinal` config function if present. Next, update the Storybook targets in the `angular.json` or `project.json` ```json "storybook": { "builder": "@analogjs/storybook-angular:start-storybook", }, "build-storybook": { "builder": "@analogjs/storybook-angular:build-storybook" } ``` Remove any `webpack` specific options and remove the `browserTarget` option. Add the `/storybook-static` folder to the `.gitignore` file. ## Setting up CSS To register global styles, add them to the `@analogjs/storybook-angular` builder options in the `angular.json` or `project.json`. ```json "storybook": { "builder": "@analogjs/storybook-angular:start-storybook", "options": { // ... other options "styles": [ "src/styles.css" ], "stylePreprocessorOptions": { "loadPaths": ["libs/my-lib/styles"] } } }, "build-storybook": { "builder": "@analogjs/storybook-angular:build-storybook", "options": { // ... other options "styles": [ "src/styles.css" ], "stylePreprocessorOptions": { "loadPaths": ["libs/my-lib/styles"] } } } ``` ## Enabling Zoneless Change Detection To use zoneless change detection for the Storybook, add the `experimentalZoneless` flag to the `@analogjs/storybook-angular` builder options in the `angular.json` or `project.json`. ```json "storybook": { "builder": "@analogjs/storybook-angular:start-storybook", "options": { // ... other options "experimentalZoneless": true } }, "build-storybook": { "builder": "@analogjs/storybook-angular:build-storybook", "options": { // ... other options "experimentalZoneless": true } } ``` ```json "storybook": { "executor": "@analogjs/storybook-angular:start-storybook", "options": { // ... other options "configDir": "path/to/.storybook", "experimentalZoneless": true, "compodoc": false } }, "build-storybook": { "executor": "@analogjs/storybook-angular:build-storybook", "options": { // ... other options "configDir": "path/to/.storybook", "experimentalZoneless": true, "compodoc": false } } ``` > Zoneless change detection is the default for new projects starting Angular v21. ## Setting up Static Assets Static assets are configured in the `.storybook/main.ts` file using the `staticDirs` array. The example below shows how to add the `public` directory from `src/public` relative to the `.storybook/main.ts` file. ```ts const config: StorybookConfig = { // other config, addons, etc. framework: { name: '@analogjs/storybook-angular', options: {}, }, staticDirs: ['../public'], }; export default config; ``` See the [Storybook docs on images and assets](https://storybook.js.org/docs/configure/integration/images-and-assets) for more information. ## Running Storybook Run the command for starting the development server. ```sh npm run storybook ``` ## Building Storybook Run the command for building the storybook. ```sh npm run build-storybook ``` ## Using TypeScript Config Path Aliases If you are using `paths` in your `tsconfig.json`, support for those aliases can be added to the `vite.config.ts`. ### With Angular CLI First, install the `vite-tsconfig-paths` package. ```shell npm install vite-tsconfig-paths --save-dev ``` ```shell yarn add vite-tsconfig-paths --dev ``` ```shell pnpm install -w vite-tsconfig-paths --save-dev ``` Next, add the plugin to the `plugins` array in the `.storybook/main.ts`. ```ts const config: StorybookConfig = { // ... other config, addons, etc. async viteFinal(config: UserConfig) { return mergeConfig(config, { plugins: [viteTsConfigPaths()], }); }, }; export default config; ``` ### With Nx Nx workspaces use the same `vite-tsconfig-paths` plugin in the `.storybook/main.ts`. The `nxViteTsPaths` plugin from `@nx/vite` is deprecated and is removed in Nx 24, so replace it with `viteTsConfigPaths()`. ## Using File Replacements You can also use the `replaceFiles()` plugin from Nx to replace files during your build. Import the plugin and set it up: ```ts const config: StorybookConfig = { // ... other config, addons, etc. async viteFinal(config: UserConfig) { return mergeConfig(config, { plugins: [ replaceFiles([ { replace: './src/one.ts', with: './src/two.ts', }, ]), ], }); }, }; export default config; ``` Adding the replacement files to `files` array in the `tsconfig.app.json` may also be necessary. ```json { "extends": "./tsconfig.json", // other config "files": ["src/main.ts", "src/main.server.ts", "src/two.ts"] } ``` ## Setting up Vitest for Interaction Testing Storybook also supports using Vitest for testing component interactions. ### Installing Packages Install the Vitest addon and dependencies: ```sh npm install @analogjs/vitest-angular @storybook/addon-vitest vitest @vitest/browser-playwright --save-dev ``` ### Add Vitest Add-on Add the addon to your `.storybook/main.ts`: ```ts const config: StorybookConfig = { stories: ['../src/**/*.mdx', '../src/**/*.stories.@(js|jsx|mjs|ts|tsx)'], addons: [ '@storybook/addon-essentials', '@storybook/addon-interactions', '@storybook/addon-vitest', ], framework: { name: '@analogjs/storybook-angular', options: {}, }, }; export default config; ``` ### Setup Vitest Configuration Create a `.storybook/vitest.setup.ts` file: ```ts const project = setProjectAnnotations([projectAnnotations]); beforeAll(project.beforeAll); ``` Update `.storybook/tsconfig.json` to include the setup file: ```json { "extends": "../tsconfig.app.json", "compilerOptions": { "types": ["node"], "allowSyntheticDefaultImports": true, "resolveJsonModule": true }, "exclude": ["../src/test.ts", "../src/**/*.spec.ts"], "include": ["../src/**/*.stories.*", "./preview.ts", "./vitest.setup.ts"], "files": ["./typings.d.ts"] } ``` Create a `vitest.config.ts` file in your project root, or add a `storybook` project to your existing `vite.config.ts`: ```ts /// const dirname = typeof __dirname !== 'undefined' ? __dirname : path.dirname(fileURLToPath(import.meta.url)); export default defineConfig({ test: { projects: [ { extends: true, plugins: [ storybookTest({ configDir: path.join(dirname, '.storybook'), }), ], test: { name: 'storybook', browser: { enabled: true, headless: true, provider: playwright(), instances: [{ browser: 'chromium' }], }, setupFiles: ['.storybook/vitest.setup.ts'], }, }, ], }, }); ``` ### Installing Playwright Install Playwright browser binaries: ```sh npx playwright install chromium ``` ### Running Component Tests Add the `test-storybook` target to your `angular.json`: ```json "test-storybook": { "builder": "@analogjs/vitest-angular:test", "options": { "configFile": "vitest.config.ts" } } ``` Add a test script to your `package.json`: ```json "scripts": { "test-storybook": "ng run your-app:test-storybook" } ``` Run your interaction tests with: ```sh npm run test-storybook ``` You can also run tests directly in the Storybook UI. Start Storybook and use the "Run Tests" button in the sidebar, or navigate to a story to see interaction tests run automatically in the Interactions panel. #### With Nx In an Nx workspace using the `@nx/storybook/plugin` inferred targets, Nx 23.2 and newer detects `@storybook/addon-vitest` in the project's `.storybook/main.ts` and infers the `test-storybook` target as `vitest run --project=storybook`, so no target needs to be added to the `project.json`. Run the interaction tests with: ```sh npx nx test-storybook your-app ``` ## Building with Angular in Development Mode `storybook build` produces a production build, compiling Angular with optimizations and without its development debug API (for example `window.ng.getComponent`). If tooling runs against the built Storybook and needs that API, such as Playwright component tests, opt the build into Angular's development compilation by setting the Vite `mode` in `viteFinal`: ```ts import { UserConfig } from 'vite'; import type { StorybookConfig } from '@analogjs/storybook-angular'; const config: StorybookConfig = { // ... other config, addons, etc. async viteFinal(config: UserConfig) { return { ...config, mode: 'development' }; }, }; export default config; ``` An explicit `mode` takes precedence over the production `NODE_ENV` that the Storybook CLI sets. ## Code Coverage To collect coverage while running your stories, enable the V8 provider on the `storybook` test project: ```ts test: { name: 'storybook', coverage: { provider: 'v8', include: ['src/**/*.ts'], }, browser: { enabled: true, headless: true, provider: playwright(), instances: [{ browser: 'chromium' }], }, setupFiles: ['.storybook/vitest.setup.ts'], }, ``` ### Monorepo configuration In a monorepo, a story often imports a shared library from another project through its package/barrel entry point (for example `@my-org/ui`). When coverage is configured to include those dependent-project sources, Vite pre-bundles the barrel and serves it from `node_modules/.vite/deps`. Browser-mode V8 coverage ignores any module served from `node_modules`, so the dependency is reported with **0 hits at its source path even though it executed**. Merging that report with your unit-test report double-counts the file and drags the overall percentage down. To collect real coverage for those dependencies, keep them out of Vite's dependency pre-bundling so they are served and instrumented from source. Add them to `optimizeDeps.exclude` in your `.storybook/main.ts` via `viteFinal`: ```ts import { UserConfig, mergeConfig } from 'vite'; import type { StorybookConfig } from '@analogjs/storybook-angular'; const config: StorybookConfig = { // ... other config, addons, etc. async viteFinal(config: UserConfig) { return mergeConfig(config, { // serve workspace libraries from source so browser coverage can instrument them optimizeDeps: { exclude: ['@my-org/ui'], }, }); }, }; export default config; ``` Alternatively, if you only want story-level coverage, scope `coverage.include` to the current project's files so dependent-project sources are not emitted as zero-hit records. --- # TanStack Query Integration with Analog URL: https://analogjs.org/docs/integrations/tanstack-query/index # TanStack Query Integration with Analog Analog provides a first-class [TanStack Query](https://tanstack.com/query/latest/docs/framework/angular/overview) integration for managing server state with SSR hydration support. Use route `load` functions when data should be resolved before the page component is created and the result is tied to navigation. Use TanStack Query when you need client-managed server state with query-key caching, invalidation, retries, or background refetching. ## Step 1: Install TanStack Query ```shell npm install @tanstack/angular-query-experimental ``` ```shell yarn add @tanstack/angular-query-experimental ``` ```shell pnpm add @tanstack/angular-query-experimental ``` ## Step 2: Configure the Client Provider Add TanStack Query and the Analog hydration provider to your application config. ```ts import { provideHttpClient, withFetch, withInterceptors, } from '@angular/common/http'; import { InjectionToken } from '@angular/core'; import type { ApplicationConfig } from '@angular/core'; import { requestContextInterceptor } from '@analogjs/router'; import { provideAnalogQuery } from '@analogjs/router/tanstack-query'; import { QueryClient, provideTanStackQuery, } from '@tanstack/angular-query-experimental'; // Per-injector `QueryClient` factory. `bootstrapApplication` creates a // fresh root injector per SSR request, so each request gets its own // `QueryClient` and request state can't leak across responses. On the // browser this still resolves to a single instance for the app. const QUERY_CLIENT = new InjectionToken('QueryClient', { factory: () => new QueryClient(), }); export const appConfig: ApplicationConfig = { providers: [ provideHttpClient( withFetch(), withInterceptors([requestContextInterceptor]), ), provideTanStackQuery(QUERY_CLIENT), provideAnalogQuery(), ], }; ``` `provideAnalogQuery()` rehydrates the TanStack Query cache from `TransferState` on the client, preventing duplicate fetches after SSR navigation. :::warning Pass a factory, not a `new QueryClient()` instance. `provideTanStackQuery(new QueryClient())` evaluates the constructor once at module-load time, so every SSR request on the same Node process shares the same cache and leaks query state between responses. Wrapping the client in an `InjectionToken` with `factory: () => new QueryClient()` gives each `bootstrapApplication` call its own client. ::: ## Step 3: Configure the Server Provider Add `provideServerAnalogQuery()` to the server application config so prefetched query state is transferred during hydration. ```ts import type { ApplicationConfig } from '@angular/core'; import { mergeApplicationConfig } from '@angular/core'; import { provideServerRendering } from '@angular/platform-server'; import { provideServerAnalogQuery } from '@analogjs/router/tanstack-query/server'; import { appConfig } from './app.config'; const serverConfig: ApplicationConfig = { providers: [provideServerRendering(), provideServerAnalogQuery()], }; export const config = mergeApplicationConfig(appConfig, serverConfig); ``` ## Step 4: Query Server Routes Use `injectQuery` and `injectMutation` from TanStack Query against Analog server routes in your components. ```ts import { HttpClient } from '@angular/common/http'; import { Component, inject } from '@angular/core'; import { lastValueFrom } from 'rxjs'; import { injectQuery } from '@tanstack/angular-query-experimental'; @Component({ template: ` @if (query.isPending()) { Loading... } @else if (query.data(); as data) {

{{ data.message }}

} `, }) export default class QueryPageComponent { private readonly http = inject(HttpClient); readonly query = injectQuery(() => ({ queryKey: ['echo'], queryFn: () => lastValueFrom(this.http.get<{ message: string }>('/api/v1/echo')), })); } ``` ## Typed Server Routes Use `serverQueryOptions` and `serverMutationOptions` from `@analogjs/router/tanstack-query` to get end-to-end type safety between server routes and client queries. Define the server route with `defineServerRoute` from `@analogjs/router/server/actions` and export it so its types can be referenced from the component. ```ts // src/server/routes/api/v1/todos.get.ts import { defineServerRoute } from '@analogjs/router/server/actions'; import * as v from 'valibot'; export const route = defineServerRoute({ query: v.object({ scope: v.optional(v.string(), 'default') }), handler: ({ query }) => getTodos(query.scope), }); export default route; ``` ```ts import { HttpClient } from '@angular/common/http'; import { Component, inject } from '@angular/core'; import { injectQuery } from '@tanstack/angular-query-experimental'; import { serverQueryOptions } from '@analogjs/router/tanstack-query'; import type { route } from '../../server/routes/api/v1/todos.get'; @Component({ template: ` @if (todosQuery.data(); as todos) { @for (todo of todos; track todo.id) {

{{ todo.title }}

} } `, }) export default class TodosComponent { private readonly http = inject(HttpClient); readonly todosQuery = injectQuery(() => serverQueryOptions(this.http, '/api/v1/todos', { queryKey: ['todos'], query: { scope: 'default' }, }), ); } ``` Query params, mutation bodies, and response shapes are all inferred from the server route definition with no manual type duplication. ## Prefetching Queries in `load()` Use `definePageLoadQueries` in a `.server.ts` file to prefetch TanStack Query queries during the Nitro `load()` handler. The dehydrated cache rides along on the route's load result and is merged into the active `QueryClient` on `ResolveEnd`, so components reading the same query options find a warm cache on first render β€” no SSR-to-client refetch, no in-component request waterfall. Define the query options in a shared module. Page `.server.ts` files are emptied in the client build, so the component cannot import runtime values from them. ```ts // src/app/pages/posts.query.ts import { queryOptions } from '@tanstack/angular-query-experimental'; export const postsQuery = queryOptions({ queryKey: ['posts'], queryFn: async ({ signal }) => fetch('https://api.example.com/posts', { signal }).then((r) => r.json()), }); ``` ```ts // src/app/pages/posts.server.ts import { definePageLoadQueries } from '@analogjs/router/tanstack-query/server'; import { postsQuery } from './posts.query'; export const load = definePageLoadQueries({ handler: async ({ client }) => { await client.prefetchQuery(postsQuery); }, }); ``` ```ts // src/app/pages/posts.page.ts import { Component } from '@angular/core'; import { injectQuery } from '@tanstack/angular-query-experimental'; import { postsQuery } from './posts.query'; @Component({ template: ` @if (posts.data(); as items) { @for (post of items; track post.id) {

{{ post.title }}

} } `, }) export default class PostsPage { readonly posts = injectQuery(() => postsQuery); } ``` `definePageLoadQueries` inherits `params` and `query` validation from `definePageLoad`; pass a Standard Schema and the handler is only invoked after validation succeeds. Any value you return from the handler is available as `data` on the load result for use with `injectLoad()`: ```ts export const load = definePageLoadQueries({ handler: async ({ client, params }) => { await client.prefetchQuery(postBySlugQuery(params['slug'])); return { renderedAt: Date.now() }; }, }); // in the component: readonly loadResult = toSignal(injectLoad()); // loadResult()?.data.renderedAt ``` A fresh `QueryClient` is constructed per request. Pass `client: () => new QueryClient({ defaultOptions: ... })` if you need to override the defaults used for prefetching. --- # Introduction URL: https://analogjs.org/docs/introduction # Introduction Analog is a fullstack meta-framework for building applications and websites with [Angular](https://angular.dev). Similar to other meta-frameworks such as Next.JS, Nuxt, SvelteKit, Qwik City, and others, Analog provides a similar experience, building on top of Angular. ## Features - Supports Vite/Vitest/Playwright - Server and deployment integrations powered by [Nitro](https://nitro.unjs.io) - [File-based routing](/docs/features/routing/overview) - [Server-side data fetching](/docs/features/data-fetching/server-side-data-fetching) - [Support for using Markdown as content routes](/docs/features/routing/content) - [API/server routes](/docs/features/api/overview) - Hybrid [SSR](/docs/features/server/server-side-rendering)/[SSG support](/docs/features/server/static-site-generation) - Supports Angular CLI/[Nx workspaces](/docs/integrations/nx) - Supports [Angular components](/docs/packages/astro-angular/overview) with Astro ## Support Us - Visit and Star the [GitHub Repo](https://github.com/analogjs/analog) - Join the [Discord](https://chat.analogjs.org) - Follow us on [Twitter](https://twitter.com/analogjs) - Become a [Sponsor](/docs/sponsoring) --- # Astro URL: https://analogjs.org/docs/packages/astro-angular/overview # @analogjs/astro-angular [Astro](https://astro.build) is a modern web framework designed for building fast, content-focused websites, compatible with all major frontend frameworks. Though primarily a static site generation (SSG) tool, it can also integrate dynamic components called "islands", which support partial hydration. This package allows rendering [Angular](https://angular.dev) components as islands in Astro. ## Setup ### Using the Astro CLI Use the `astro add` command to install the integration Using npm: ```sh npx astro add @analogjs/astro-angular ``` Using pnpm: ```sh pnpm astro add @analogjs/astro-angular ``` Using yarn: ```sh yarn astro add @analogjs/astro-angular ``` This command: - Installs the `@analogjs/astro-angular` package. - Adds the `@analogjs/astro-angular` integration to the `astro.config.mjs` file. - Installs the necessary dependencies to render Angular components on the server and client, and common Angular dependencies, such as `@angular/common`. ### Setting up the TypeScript config The integration needs a `tsconfig.app.json` at the root of the project for compilation. Create a `tsconfig.app.json` in the root of the project. ```json { "extends": "./tsconfig.json", "compileOnSave": false, "compilerOptions": { "baseUrl": "./", "outDir": "./dist/out-tsc", "forceConsistentCasingInFileNames": true, "strict": true, "noImplicitOverride": true, "noPropertyAccessFromIndexSignature": true, "noImplicitReturns": true, "noFallthroughCasesInSwitch": true, "sourceMap": true, "declaration": false, "downlevelIteration": true, "experimentalDecorators": true, "moduleResolution": "node", "importHelpers": true, "noEmit": false, "target": "es2020", "module": "es2020", "lib": ["es2020", "dom"], "skipLibCheck": true }, "angularCompilerOptions": { "enableI18nLegacyMessageIdFormat": false, "strictInjectionParameters": true, "strictInputAccessModifiers": true, "strictTemplates": true, "allowJs": false }, "files": [], "include": ["src/**/*.ts", "src/**/*.tsx"] } ``` Go to [Defining A Component](#defining-a-component) to set up an Angular component to use in an Astro component. ## Manual Installation The integration can also be installed manually ### Install the Astro Integration ```sh yarn add @analogjs/astro-angular ``` ### Install the necessary Angular dependencies ```sh npm install @angular/build @angular/{animations,common,compiler-cli,compiler,core,language-service,forms,platform-browser,platform-server} rxjs tslib --save ``` ### Adding the integration Add the integration to the `astro.config.mjs` ```js import { defineConfig } from 'astro/config'; import angular from '@analogjs/astro-angular'; export default defineConfig({ integrations: [angular()], }); ``` Go to [Defining A Component](#defining-a-component) ## Configuration ### Configure Vite Angular Plugin Provide an option object to configure the `@analogjs/vite-plugin-angular` powering this plugin. ```js import { defineConfig } from 'astro/config'; import angular from '@analogjs/astro-angular'; export default defineConfig({ integrations: [ angular({ vite: { inlineStylesExtension: 'scss|sass|less', }, }), ], }); ``` ### Filtering File Transforms For better compatibility when integrating with other plugins such as [Starlight](https://starlight.astro.build), put the Angular components in a specific folder and use the `transformFilter` callback function to only transform those files. ```js import { defineConfig } from 'astro/config'; import angular from '@analogjs/astro-angular'; export default defineConfig({ integrations: [ angular({ vite: { transformFilter: (_code, id) => { return id.includes('src/components'); // <- only transform Angular TypeScript files }, }, }), ], }); ``` ### Transforming Packages for SSR Compatibility To ensure Angular libraries are transformed during Astro's SSR process, add them to the `ssr.noExternal` array in the Vite config. ```js import { defineConfig } from 'astro/config'; import angular from '@analogjs/astro-angular'; export default defineConfig({ integrations: [angular()], vite: { ssr: { // transform these packages during SSR. Globs supported noExternal: ['@rx-angular/**'], }, }, }); ``` ### Strict Style Placement Angular components can use style scoped to each component instance. By default, these style tags will be inserted adjacent to the component's HTML by `@analogjs/astro-angular`. While this will typically work for modern browsers, it is technically invalid HTML. To force these component styles to the document head, enable the `strictStylePlacement` option in the integration config. **Warning:** enabling this option will disable Astro's [streaming](https://docs.astro.build/en/recipes/streaming-improve-page-performance/) mode under SSR. ```js import { defineConfig } from 'astro/config'; import angular from '@analogjs/astro-angular'; export default defineConfig({ integrations: [angular({ strictStylePlacement: true })], }); ``` ### Angular Client Hydration (Experimental) By default, `@analogjs/astro-angular` performs hydration by bootstrapping the component on the client, replacing the DOM that was rendered on the server. To opt-in to Angular's client hydration, enable the `experimental.useAngularHydration` option in the integration config. This will switch the hydration strategy to use [provideClientHydration](https://angular.dev/api/platform-browser/provideClientHydration). ```js import { defineConfig } from 'astro/config'; import angular from '@analogjs/astro-angular'; export default defineConfig({ integrations: [angular({ useAngularHydration: true })], }); ``` #### Skip Hydration Use the `ngSkipHydration` attribute on any components which do not work properly with hydration enabled. Read more [here](https://angular.dev/guide/hydration#how-to-skip-hydration-for-particular-components). ## Defining A Component The Astro Angular integration **only** supports rendering standalone components: ```ts import { Component, Input } from '@angular/core'; @Component({ selector: 'app-hello', template: `

Hello from Angular!!

@if (show()) {

{{ helpText() }}

} `, }) export class HelloComponent { helpText = input('help'); show = signal(false); toggle() { this.show.update((show) => !show); } } ``` Add the Angular component to the Astro component template. This only renders the HTML from the Angular component. ```tsx --- import { HelloComponent } from '../components/hello.component'; const helpText = "Helping binding"; --- ``` To hydrate the component on the client, use one of the Astro [client directives](https://docs.astro.build/en/reference/directives-reference/#client-directives): ```tsx --- import { HelloComponent } from '../components/hello.component'; --- ``` Find more information about [Client Directives](https://docs.astro.build/en/reference/directives-reference/#client-directives) in the Astro documentation. ### Listening to Component Outputs Outputs can be emitted by the Angular component are forwarded as HTML events to the Astro island. To enable this feature, add a client directive and a unique `[data-analog-id]` property to each Angular component: ```tsx --- import { HelloComponent } from '../components/hello.component'; --- ``` Then, listen to the event in the Astro component using the `addOutputListener` function: ```tsx --- import { HelloComponent } from '../components/hello.component'; --- ``` ## Adding Component Providers Additional providers can be added to a component for static rendering and client hydration. These are `renderProviders` and `clientProviders` respectively. These providers are defined as static arrays on the Component class, and are registered when the component is rendered, and hydrated on the client. ```ts import { Component, OnInit, inject } from '@angular/core'; import { provideHttpClient, HttpClient } from '@angular/common/http'; interface Todo { id: number; title: string; completed: boolean; } @Component({ selector: 'app-todos', template: `

Todos

    @for (todo of todos(); track todo.id) {
  • {{ todo.title }}
  • }
`, }) export class TodosComponent implements OnInit { static clientProviders = [provideHttpClient()]; static renderProviders = [TodosComponent.clientProviders]; http = inject(HttpClient); todos = signal([]); ngOnInit() { this.http .get('https://jsonplaceholder.typicode.com/todos') .subscribe((todos) => this.todos.set(todos)); } } ``` ### Client Hydration Features (Experimental) First, make sure the experimental Angular client hydration option is enabled in the integration config. Read more [here](#angular-client-hydration-experimental). To add Angular hydration features, add a static property to the component class named `hydrationFeatures`. This should be a function that returns an array of hydration features to enable. The example below adds the [event replay](https://angular.dev/guide/hydration#how-event-replay-works) feature to the component. ```ts import { Component, input, signal } from '@angular/core'; import { withEventReplay } from '@angular/platform-browser'; @Component({ selector: 'app-hello', template: `

Hello from Angular!!

@if (show()) {

{{ helpText() }}

} `, }) export class HelloComponent { static hydrationFeatures = () => [withEventReplay()]; helpText = input('help'); show = signal(false); toggle() { this.show.update((show) => !show); } } ``` ## Using Components in MDX pages To use components with MDX pages, you must install and configure MDX support by following the Astro integration of [@astrojs/mdx](https://docs.astro.build/en/guides/integrations-guide/mdx/). Your `astro.config.mjs` should now include the `@astrojs/mdx` integration. ```js import { defineConfig } from 'astro/config'; import mdx from '@astrojs/mdx'; import angular from '@analogjs/astro-angular'; export default defineConfig({ integrations: [mdx(), angular()], }); ``` Create an `.mdx` file inside the `src/pages` directory and add the Angular component import below the frontmatter. ```md --- layout: '../../layouts/BlogPost.astro' title: 'Using Angular in MDX' description: 'Lorem ipsum dolor sit amet' pubDate: 'Sep 22 2022' --- import { HelloComponent } from "../../components/hello.component.ts"; ``` To hydrate the component on the client, use one of the Astro [client directives](https://docs.astro.build/en/reference/directives-reference/#client-directives): ```md --- layout: '../../layouts/BlogPost.astro' title: 'Using Angular in MDX' description: 'Lorem ipsum dolor sit amet' pubDate: 'Sep 22 2022' --- import { HelloComponent } from "../../components/hello.component.ts"; ``` > Important: In `.mdx` files the component import must end with the `.ts` suffix. Otherwise the dynamic import of the component will fail and the component won't be hydrated. ## Content Projection Children passed to an Angular component in an Astro file are projected into the component's `ng-content` slots. Angular's own `select` semantics apply, so existing components work unchanged. ```ts import { Component } from '@angular/core'; @Component({ selector: 'app-card', template: `
`, }) export class CardComponent {} ``` ```html --- import { CardComponent } from '../components/card.component'; ---

Is content projection cool?

Let's learn about content projection!

``` Content that does not match any `select` is projected into the default ``. Without a default slot it is dropped, as in Angular. `ngProjectAs` is honored as well. Astro's `slot` attribute is not needed to target a slot. Astro removes the attribute before rendering, so use Angular selectors such as attributes, classes, or element names instead. > Note: for hydrated islands, Astro also emits the projected content in an inert `