Fix: Analog Not Working — Routes Not Loading, API Endpoints Failing, or Vite Build Errors
Part of: React & Frontend Errors
Quick Answer
How to fix Analog (Angular meta-framework) issues — file-based routing, API routes with Nitro, content collections, server-side rendering, markdown pages, and deployment.
The Problem
Analog routes return 404:
GET /about → 404 Not FoundOr API routes don’t respond:
GET /api/users → 502 Bad GatewayOr the Vite dev server crashes:
Error: [vite] Cannot find module '@analogjs/vite-plugin-angular'Why This Happens
Analog is the Angular meta-framework built on Vite and Nitro. It brings file-based routing, API routes, and SSR to Angular:
- Routes use file-based convention in
src/app/pages/— each.page.tsfile becomes a route. The.pagesuffix is required — regular.component.tsfiles aren’t auto-routed. - API routes go in
src/server/routes/— Analog uses Nitro (same as Nuxt) for server endpoints. API files needdefineEventHandlerexports. - Analog needs the Vite plugin —
@analogjs/platformmust be installed and configured invite.config.ts. Without it, Angular components don’t compile. - Content/markdown requires
@analogjs/content— for MDX/markdown pages, the content plugin must be explicitly installed and configured.
Analog occupies a unique slot in the Angular ecosystem. Where Angular CLI projects historically used Webpack and Angular Universal for SSR, Analog replaces that stack with Vite (for dev/build) and Nitro (for the server runtime). That swap means most “not working” errors are actually Vite or Nitro errors surfacing through Analog’s plugin layer. A missing dependency in vite.config.ts looks like an Analog bug, but it’s almost always a plugin order or peer-dep mismatch.
The second source of confusion is the dual identity of files. A component file (.component.ts) is still a standard Angular standalone component. A page file (.page.ts) is an Analog convention that the file-based router treats as a route. They look almost identical, but only .page.ts files in src/app/pages/ register URLs. New users routinely copy a working component, rename it, and wonder why the route 404s. The default export requirement is the third trap: Angular components are typically named exports, but Analog’s router only picks up export default class. Mix these conventions up and the framework falls back to “no route here,” not a clear error message.
Analog Version History
The version timeline matters because Analog moves fast and breaking changes are tied to specific releases:
- Pre-1.0 (2023) — Brandon Roberts shipped early previews with
provideFileRouter()and basic Nitro integration, but the API churned monthly. Tutorials from this era often referencedefineRoutehelpers that no longer exist. - Analog 1.0 (early 2024) — first stable release. Locked in
@analogjs/platformas the umbrella package, standardized.page.tsfilename suffix, and adopted Nitro for API routes. From this point forward, the routing/server contract has been stable. - Analog 1.2 — 1.5 (mid 2024) — added the
@analogjs/contentplugin for markdown/MDX, expanded the content schema API, and started shipping the@analogjs/routerstandalone package so projects could opt into file-based routing without the full platform. - Analog 1.6+ (late 2024) — aligned Vite peer ranges to Vite 5, added zoneless change detection support, and introduced first-class Angular 18+ feature flags. Older 1.0–1.2 tutorials assume Vite 4 and Zone.js, which still works but gets noisy warnings.
The practical takeaway is that any Analog answer you find online needs a version stamp. A 2023 example that uses pre-1.0 APIs will not run on current Analog. Pin @analogjs/platform and @analogjs/content to matching minor versions, and check the changelog before upgrading across a minor boundary.
Another version-related trap is the Angular peer-dep range. Each Analog minor supports a specific Angular major (Analog 1.0 was Angular 17, Analog 1.5+ targets Angular 18, later releases track Angular 19). If you upgrade Angular ahead of Analog, the Vite plugin can fail to compile templates or signal control flow, and the error message points at your component, not the version skew. When in doubt, downgrade Angular to the version Analog’s package.json lists in peerDependencies and retry. Nitro versions follow a similar lockstep — Analog vendors a known-good Nitro range and overriding it (via npm overrides or pnpm resolutions) usually breaks server route handling. Treat Analog, Angular, Vite, and Nitro as one coordinated stack rather than four independently-upgradable packages.
Comparing Analog to Angular Universal clarifies its niche. Angular Universal (the historical SSR solution) bolts SSR onto an existing Angular CLI project. The build still goes through Webpack, the dev server still runs Angular CLI, and SSR is a separate Node entry that pre-renders the application. Analog replaces that whole flow: Vite is the dev server, Nitro is the SSR runtime, and file-based routing replaces the manual RouterModule.forRoot([...]) declarations. The migration cost is non-trivial — most third-party Angular libraries assume the CLI/Webpack pipeline — but the dev experience and feature set (API routes, content collections, edge deployment presets) is closer to Nuxt or SvelteKit than to classic Angular.
Fix 1: Project Setup
# Create new Analog project
npm create analog@latest my-app
cd my-app && npm install && npm run dev
# Or add to existing Angular project
ng add @analogjs/platform// vite.config.ts
import { defineConfig } from 'vite';
import analog from '@analogjs/platform';
export default defineConfig({
plugins: [
analog({
ssr: true,
static: false,
prerender: {
routes: ['/', '/about', '/blog'],
},
}),
],
});// src/app/app.config.ts
import { ApplicationConfig, provideZoneChangeDetection } from '@angular/core';
import { provideFileRouter } from '@analogjs/router';
import { provideHttpClient, withFetch } from '@angular/common/http';
import { provideClientHydration } from '@angular/platform-browser';
export const appConfig: ApplicationConfig = {
providers: [
provideZoneChangeDetection({ eventCoalescing: true }),
provideFileRouter(), // File-based routing
provideHttpClient(withFetch()),
provideClientHydration(),
],
};Fix 2: File-Based Routing
src/app/pages/
├── index.page.ts # /
├── about.page.ts # /about
├── (auth)/
│ ├── login.page.ts # /login
│ └── register.page.ts # /register
├── blog/
│ ├── index.page.ts # /blog
│ └── [slug].page.ts # /blog/:slug
├── dashboard/
│ ├── index.page.ts # /dashboard
│ └── settings.page.ts # /dashboard/settings
└── [...not-found].page.ts # Catch-all 404// src/app/pages/index.page.ts — home page
import { Component } from '@angular/core';
import { RouterLink } from '@angular/router';
@Component({
standalone: true,
imports: [RouterLink],
template: `
<h1>Welcome to Analog</h1>
<nav>
<a routerLink="/about">About</a>
<a routerLink="/blog">Blog</a>
<a routerLink="/dashboard">Dashboard</a>
</nav>
`,
})
export default class HomePage {}
// MUST be default export// src/app/pages/blog/[slug].page.ts — dynamic route
import { Component, inject } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { AsyncPipe } from '@angular/common';
import { toSignal } from '@angular/core/rxjs-interop';
import { switchMap } from 'rxjs';
import { HttpClient } from '@angular/common/http';
@Component({
standalone: true,
imports: [AsyncPipe],
template: `
@if (post(); as p) {
<article>
<h1>{{ p.title }}</h1>
<p>{{ p.body }}</p>
</article>
} @else {
<p>Loading...</p>
}
`,
})
export default class BlogPostPage {
private route = inject(ActivatedRoute);
private http = inject(HttpClient);
post = toSignal(
this.route.paramMap.pipe(
switchMap(params => this.http.get<Post>(`/api/posts/${params.get('slug')}`))
)
);
}Fix 3: API Routes (Nitro)
src/server/routes/
├── v1/
│ ├── users.ts # /api/v1/users
│ ├── users/
│ │ └── [id].ts # /api/v1/users/:id
│ └── posts.ts # /api/v1/posts
└── health.ts # /api/health// src/server/routes/v1/users.ts
import { defineEventHandler, readBody, getQuery } from 'h3';
// GET /api/v1/users
export default defineEventHandler(async (event) => {
const query = getQuery(event);
const limit = Number(query.limit) || 20;
const users = await db.query.users.findMany({ limit });
return users;
});
// src/server/routes/v1/users/[id].ts
// Multiple HTTP methods in one file
export default defineEventHandler(async (event) => {
const id = getRouterParam(event, 'id');
if (event.method === 'GET') {
const user = await db.query.users.findFirst({ where: eq(users.id, id!) });
if (!user) throw createError({ statusCode: 404, message: 'User not found' });
return user;
}
if (event.method === 'PATCH') {
const body = await readBody(event);
const [updated] = await db.update(users).set(body).where(eq(users.id, id!)).returning();
return updated;
}
if (event.method === 'DELETE') {
await db.delete(users).where(eq(users.id, id!));
return { deleted: true };
}
});
// Or use specific method files:
// src/server/routes/v1/users.get.ts
// src/server/routes/v1/users.post.tsFix 4: Layouts
// src/app/pages/dashboard.page.ts — layout route
// Files named the same as a directory create a layout
import { Component } from '@angular/core';
import { RouterOutlet, RouterLink } from '@angular/router';
@Component({
standalone: true,
imports: [RouterOutlet, RouterLink],
template: `
<div class="flex min-h-screen">
<aside class="w-64 bg-gray-900 text-white p-4">
<h2 class="text-xl font-bold mb-4">Dashboard</h2>
<nav>
<a routerLink="/dashboard" routerLinkActive="text-blue-400" [routerLinkActiveOptions]="{ exact: true }">
Overview
</a>
<a routerLink="/dashboard/settings" routerLinkActive="text-blue-400">
Settings
</a>
</nav>
</aside>
<main class="flex-1 p-8">
<router-outlet />
</main>
</div>
`,
})
export default class DashboardLayout {}
// src/app/pages/dashboard/index.page.ts — /dashboard
@Component({
standalone: true,
template: `<h1>Dashboard Overview</h1>`,
})
export default class DashboardIndexPage {}
// src/app/pages/dashboard/settings.page.ts — /dashboard/settings
@Component({
standalone: true,
template: `<h1>Settings</h1>`,
})
export default class DashboardSettingsPage {}Fix 5: Markdown/Content Pages
npm install @analogjs/content marked prismjs// vite.config.ts — enable content plugin
import { defineConfig } from 'vite';
import analog, { type PrerenderContentFile } from '@analogjs/platform';
export default defineConfig({
plugins: [
analog({
content: {
highlighter: 'prism',
},
prerender: {
routes: async () => {
const contentFiles = await import.meta.glob<PrerenderContentFile>(
'/src/content/blog/*.md'
);
return [
'/',
'/blog',
...Object.keys(contentFiles).map(file => {
const slug = file.replace('/src/content/blog/', '').replace('.md', '');
return `/blog/${slug}`;
}),
];
},
},
}),
],
});<!-- src/content/blog/getting-started.md -->
---
title: Getting Started with Analog
date: 2026-03-30
slug: getting-started
description: Learn how to build apps with Analog
---
# Getting Started
This is a markdown blog post rendered by Analog.
```typescript
console.log('Hello from Analog!');
```typescript
// src/app/pages/blog/[slug].page.ts — render markdown content
import { Component } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { injectContent, MarkdownComponent } from '@analogjs/content';
import { AsyncPipe } from '@angular/common';
interface PostAttributes {
title: string;
date: string;
slug: string;
description: string;
}
@Component({
standalone: true,
imports: [MarkdownComponent, AsyncPipe],
template: `
@if (post$ | async; as post) {
<article>
<h1>{{ post.attributes.title }}</h1>
<time>{{ post.attributes.date }}</time>
<analog-markdown [content]="post.content" />
</article>
}
`,
})
export default class BlogPostPage {
post$ = injectContent<PostAttributes>({
customFilename: this.route.snapshot.paramMap.get('slug')!,
});
constructor(private route: ActivatedRoute) {}
}Fix 6: Deployment
// vite.config.ts — deployment presets
export default defineConfig({
plugins: [
analog({
// Vercel
nitro: { preset: 'vercel' },
// Cloudflare Pages
// nitro: { preset: 'cloudflare-pages' },
// Netlify
// nitro: { preset: 'netlify' },
// Node.js server
// nitro: { preset: 'node-server' },
// Static site generation
// static: true,
// prerender: { routes: ['/', '/about', '/blog'] },
}),
],
});# Build
npm run build
# Preview production build locally
npm run serve
# Deploy
# Vercel: connect repo, auto-detected
# Netlify: set build command to `npm run build`, publish dir to `dist/analog/public`Still Not Working?
Routes return 404 — page files must use the .page.ts extension and export a default component. about.component.ts won’t work — it must be about.page.ts. Also check the file is in src/app/pages/.
API routes don’t respond — server routes must be in src/server/routes/ and export defineEventHandler. The /api/ prefix is added automatically. Also check the dev server is running with SSR enabled.
Vite build fails — ensure @analogjs/platform is installed and configured in vite.config.ts. Angular components need the Vite plugin to compile — without it, Angular decorators and templates aren’t processed.
Content/markdown not rendering — install @analogjs/content and marked. The content files must be in src/content/. The MarkdownComponent must be imported in the page component.
Hydration errors after enabling SSR — Angular’s provideClientHydration() must be present in app.config.ts and the component output must be deterministic. Code that calls Date.now(), Math.random(), or reads localStorage during construction will render different HTML on server and client. Move that logic into ngAfterViewInit or guard with isPlatformBrowser(this.platformId).
@analogjs/platform peer warning on install — Analog 1.x has strict peer ranges on Vite and Angular. If you upgrade Angular major but not Analog (or vice versa), npm install emits ERESOLVE warnings and Vite’s plugin order silently breaks. Bump both packages in lockstep and delete node_modules and package-lock.json before reinstalling.
Nitro preset doesn’t deploy — each preset (vercel, cloudflare-pages, netlify) emits a different output directory and entry file. If Vercel returns a function-not-found error, double-check that nitro.preset matches the target platform exactly. Generic builds default to node-server, which will not run on edge runtimes.
For related Angular issues, see Fix: Angular SSR Not Working and Fix: Angular Signals Not Updating. For the underlying server runtime, see Fix: Nitro Not Working. If your Vite dev server itself is failing before Analog can boot, start with Fix: Vite Failed to Resolve Import.
Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.
Was this article helpful?
Related Articles
Fix: Angular SSR Not Working — Hydration Failing, Window Not Defined, or Build Errors
How to fix Angular Server-Side Rendering issues — @angular/ssr setup, hydration, platform detection, transfer state, route-level rendering, and deployment configuration.
Fix: Astro Actions Not Working — Form Submission Failing, Validation Errors Missing, or Return Type Wrong
How to fix Astro Actions issues — action definition, Zod validation, form handling, progressive enhancement, error handling, file uploads, and calling actions from client scripts.
Fix: SolidStart Not Working — Routes Not Rendering, Server Functions Failing, or Hydration Errors
How to fix SolidStart issues — file-based routing, server functions, createAsync data loading, middleware, sessions, and deployment configuration.
Fix: TanStack Start Not Working — Server Functions Failing, Routes Not Loading, or SSR Errors
How to fix TanStack Start issues — project setup, file-based routing, server functions with createServerFn, data loading, middleware, SSR hydration, and deployment configuration.