Current setup#

This blog now runs Angular 21 with AnalogJS 2. The migration notes below describe the original move from Scully. For a new project, use the current Analog routing guide and static generation documentation.

I spent the best two days migrating my blog from Scully to AnalogJS. Why? Because Scully, once a strong choice for static site generation with Angular, is no longer actively maintained. Unfortunately, the official repo hasn't seen an update in over 2 years!

After researching, AnalogJS emerged as the clear winner due to its alignment with Angular's ecosystem and its simplicity for static site generation and server-side rendering. In this post, I'll detail some of the gotchas I encountered while building my blog with AnalogJS.


Gotcha 1: Page Routing#

Adapting to path-based routing is one of the biggest changes when moving from Scully (or Angular's traditional routing) to AnalogJS. Instead of defining routes in a module, AnalogJS relies on the file system to generate routes automatically. While this simplifies routing, it can be tricky to get used to.

Blog Pages with Slugs#

For example, if you have a blog structure where individual posts are accessed via a slug (e.g., /blog/my-first-post), your routing file would look like this:

typescript
/app/pages/blog/[slug].page.ts

This handles routes like /blog/my-first-post perfectly. However, what happens when a user tries to access /blog without a slug? By default, this will result in a blank or broken page. To handle this, you need to create a catch-all route inside the /blog folder, such as [...blog].page.ts. This file can either:

  1. Redirect users to another page (e.g., the homepage):

    typescript
    import { RouteMeta } from '@analogjs/router';
    
    export const routeMeta: RouteMeta = {
      redirectTo: '/',
      pathMatch: 'full',
    };
    
  2. Or display custom content like a 404 message or a landing page.

Catch-All for Non-Existent Routes#

To handle all undefined routes in your app (e.g., /random-page-that-doesnt-exist), create a wildcard route:

typescript
/app/pages/[...page-not-found].page.ts

Here's an example of a custom 404 component:

typescript
import { Component } from '@angular/core';
import { RouterLink } from '@angular/router';

@Component({
  standalone: true,
  imports: [RouterLink],
  template: `
    <main id="main" class="text-center space-y-8">
      <div>
        <pre class="font-mono text-sm sm:text-lg leading-tight text-gray-500">
          <!-- ASCII art or a message -->
        </pre>
      </div>
      <div>
        <h1 id="title" class="text-2xl sm:text-4xl font-bold text-gray-500">
          Oops, this page does not exist;
          <a href="/" class="text-black hover:text-teal-700">go home</a>
        </h1>
      </div>
    </main>
  `,
})
export default class PageNotFoundComponent {}

This ensures that any invalid route renders a friendly message instead of a blank or broken page.


Gotcha 2: Catch-All Routing with Static Content Using GitHub Pages#

Deploying your AnalogJS blog to a static host like GitHub Pages introduces another challenge: static file servers don't handle client-side routing natively. For example, navigating to /blog/my-first-post will result in a 404 error because the server looks for a file at /blog/my-first-post/index.html, which doesn't exist.

Why Does GitHub Pages Require a 404.html?#

GitHub Pages has a unique mechanism for fallback routing. It uses 404.html as the fallback file for any undefined route. By serving a 404.html that contains your app's content, you can let the AnalogJS router take over and render the appropriate page.

Solution: Post-Build Script for 404 Fallback#

To create a 404.html during the build process, I wrote a simple Node.js script (copy-404.cjs) that copies index.html to 404.html. Here's the script:

javascript
const fs = require('fs');
const path = require('path');

const src = path.resolve(__dirname, 'dist/analog/public/index.html');
const dest = path.resolve(__dirname, 'dist/analog/public/404.html');

// Copy index.html to 404.html
fs.copyFileSync(src, dest);
console.log('404.html created successfully!');

To automate this step, I updated my package.json as follows:

json
{
  "scripts": {
    "build": "vite build",
    "postbuild": "node copy-404.cjs"
  }
}

Now, whenever I run npm run build, the postbuild script ensures that 404.html is created alongside index.html.


Gotcha 3: Hardcoded Prerender Routes#

By default, the routes to prerender in AnalogJS are hardcoded. This is fine if you only plan to have a handful of posts, but it can turn into a maintenance nightmare as your blog grows in size or you forget to update the routes. For example:

typescript
prerender: {
  routes: ['/blog', '/blog/2022-12-27-my-first-post'],
},

Solution: Automate Route Generation#

To address this, you can dynamically generate prerendered routes by transforming files in your content directory. Here's an updated configuration that I used:

typescript
prerender: {
  routes: async () => [
    {
      contentDir: 'src/content/',
      transform: (file: PrerenderContentFile) => {
        if (file.attributes.draft) {
          return false;
        }
        const slug = file.attributes.slug || file.name;
        return `/blog/${slug}`;
      },
    },
  ],
},

Using this approach dynamically processes your content files during prerendering, skipping drafts and ensuring each route is based on the slug or file name.


After working through these migration challenges myself, I wanted to share what I learned about moving from Scully to AnalogJS. The transition involves some unexpected quirks - particularly around routing and prerendering - but I hope these real-world solutions help make your own migration smoother.

If you have questions, feel free to reach out using my BlueSky handle tomling.bsky.social.

If any of my blog posts have been helpful to you, and you’re feeling generous, don’t hesitate to buy me a coffee ☕.