Migrating from Scully + Angular 16 to Angular 20 Prerender (Full Guide)

March 25, 2026

I recently migrated my blog from Angular 16 + Scully to Angular 20 using the built-in prerenderer (SSR).

This wasnโ€™t just a version bump โ€” it was a full architectural shift:

  • removing Scully
  • upgrading Angular across multiple major versions
  • rebuilding the content pipeline
  • configuring SSR + prerender
  • fixing several tricky runtime and build issues

In this post, Iโ€™ll walk through the exact strategy, the problems I hit, and how to solve them.


Step 1: Upgrade Angular Incrementally

The safest way to upgrade Angular across major versions is step-by-step:

npx @angular/cli@17 update @angular/core@17 @angular/cli@17
npx @angular/cli@19 update @angular/core@19 @angular/cli@19
npx @angular/cli@20 update @angular/core@20 @angular/cli@20
Problem

Scully depends on older Angular and TypeScript versions, which leads to errors like:

  • incompatible peer dependencies
  • TypeScript conflicts
  • failed installs
Tip

๐Ÿ‘‰ Let Scully break during the upgrade
๐Ÿ‘‰ Focus on getting Angular upgraded first
๐Ÿ‘‰ Plan to remove Scully afterward


Step 2: Accept That Scully Is the Bottleneck

At some point, it becomes clear:

Scully is preventing modernization.

Common symptoms:

  • peer dependency conflicts
  • SSR build failures
  • outdated TypeScript requirements
Decision

๐Ÿ‘‰ Stop trying to fix Scully
๐Ÿ‘‰ Replace it with Angularโ€™s built-in prerender


Step 3: Replace Scully with a Custom Content Pipeline

Scully previously handled:

  • markdown parsing
  • route generation
  • static HTML injection

We replaced it with a custom pipeline:

Markdown โ†’ Node script โ†’ Angular TypeScript โ†’ runtime rendering
Tools Used
  • gray-matter (frontmatter parsing)
  • markdown-it (markdown โ†’ HTML)
Result

A generated file like:

src/app/content/generated/posts.generated.ts

Which contains:

  • all blog posts
  • metadata
  • HTML content

Step 4: Introduce Angular SSR

Add SSR support:

ng add @angular/ssr

This generates:

  • server.ts
  • main.server.ts
  • app.routes.server.ts
Key Concept

Angular now supports route-level rendering:

RenderMode.Prerender;
RenderMode.Client;
RenderMode.Server;

Step 5: Solve Prerender Route Extraction Failures

One of the most confusing errors:

O4.match is not a function
Cause

Angular fails when trying to automatically expand dynamic routes like:

/posts/:id
Fix

Do not prerender dynamic routes:

{ path: 'posts/:id', renderMode: RenderMode.Client }

Only prerender static routes:

{ path: 'blog', renderMode: RenderMode.Prerender }

Step 6: Fix โ€œdocument is not definedโ€ (SSR Crash)

Error:

ReferenceError: document is not defined
Why It Happens

SSR runs in Node.js, not the browser.

Browser-only APIs like:

  • document
  • window
  • localStorage

are not available.

Fix

Guard all DOM access:

import { isPlatformBrowser } from '@angular/common';
import { Inject, PLATFORM_ID } from '@angular/core';

constructor(@Inject(PLATFORM_ID) private platformId: Object) {}

if (isPlatformBrowser(this.platformId)) {
  const el = document.getElementById('photo-top');
}

Step 7: Fix Angular Build Config Conflict

Warning encountered:

The "prerender" option is not considered when "outputMode" is specified.
Root Cause

Angular ignores prerender settings when outputMode is set.

Fix

Remove:

"outputMode": "static"

This allows Angular to properly execute prerendering.


Step 8: Switch to Static Output

Final build output:

dist/<your-project>/browser

Preview locally:

npx http-server dist/<your-project>/browser

This replaces the old Scully docs/ directory entirely (I had Scully configured to generate into โ€˜docsโ€™ directory).


Step 9: Update GitHub Pages Deployment

Old deployment:

docs/

New deployment:

dist/<your-project>/browser
Key Step
cp CNAME dist/<your-project>/browser/CNAME

Step 10: Clean Up Scully Completely

Delete leftover files:

  • .scully/
  • docs/
  • scully.config.ts
  • scully-routes.json

Remove:

  • Scully dependencies
  • Scully scripts

Common Pitfalls (and How to Avoid Them)

1. Trying to Keep Scully During Upgrade

๐Ÿ‘‰ Donโ€™t. It will slow everything down.


2. Letting Angular Auto-Discover Routes

๐Ÿ‘‰ Can cause cryptic SSR crashes
๐Ÿ‘‰ Prefer explicit or hybrid routing


3. Using Browser APIs in SSR

๐Ÿ‘‰ Always guard with isPlatformBrowser


4. Mismatched Node Version

Keep engines aligned:

"node": ">=20 <23"

5. Old Deployment Assumptions

Scully โ†’ docs/
Angular prerender โ†’ dist/โ€ฆ/browser


Final Architecture

Markdown โ†’ Generator โ†’ Angular โ†’ SSR โ†’ Prerender โ†’ Static Site

Final Thoughts

This migration was not trivial, but it was absolutely worth it.

You now have:

  • modern Angular
  • no third-party static generator
  • faster builds
  • full control over content
  • a future-proof architecture

If youโ€™re still on Scully:

Start planning your exit now.