Skip to content

Fix: Analog Not Working — Routes Not Loading, API Endpoints Failing, or Vite Build Errors

FixDevs ·

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 Found

Or API routes don’t respond:

GET /api/users → 502 Bad Gateway

Or 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.ts file becomes a route. The .page suffix is required — regular .component.ts files aren’t auto-routed.
  • API routes go in src/server/routes/ — Analog uses Nitro (same as Nuxt) for server endpoints. API files need defineEventHandler exports.
  • Analog needs the Vite plugin@analogjs/platform must be installed and configured in vite.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.

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.ts

Fix 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.

For related Angular issues, see Fix: Angular SSR Not Working and Fix: Angular Signals Not Updating.

F

FixDevs

Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.

Was this article helpful?

Related Articles