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.
