Nice looking URLs and flawless SEO for your prerendered Angular app: blog/my-article.html instead of blog/my-article/index.html, no more trailing slash redirects! π
Table of contents:
- π‘ Why?
β οΈ Prerequisites- π Quick Start
- βοΈ Installation
- π¦ Options
- π Other frameworks
- π Hosts
- π§ How it works
- π Known limitations
- π License
Angular's prerendering (SSG) writes every route into its own folder: the route blog/my-article becomes blog/my-article/index.html.
Static hosts see a folder and redirect /blog/my-article to /blog/my-article/.
So you have to choose:
- Nice looking URLs, but redirects: links have to use
/blog/my-article. Every direct visit (search engine, bookmark, shared link) starts with a 301/308 redirect to/blog/my-article/, and the Angular router then removes the trailing slash again. - No redirects, but trailing slashes everywhere: links have to use
/blog/my-article/. Pages answer with a status code 200, but every URL ends with a slash, and Angular needs an extra provider to keep it in the address bar:{ provide: LocationStrategy, useClass: TrailingSlashPathLocationStrategy }(seeTrailingSlashPathLocationStrategy).
The same route becomes blog/my-article.html, and hosts like Cloudflare Pages serve it under /blog/my-article directly, with a status code 200.
- Nice looking URLs:
/blog/my-article, without a trailing slash, in links, in the address bar and in the server response alike. - Flawless SEO: every page answers directly with a status code 200. Search engines see no redirect, and the URL they crawl is the same one your links point to.
- Old links keep working: addresses with a trailing slash redirect to the address without it (measured on Cloudflare Pages, see Hosts).
The idea comes from angular/angular-cli#29173, which asks for an option to write <route>.html instead of <route>/index.html.
The pull request angular/angular-cli#34180 adds such an option to Angular itself, with the same name and values as this builder: prerenderFormat, 'directory' or 'file'.
The builder is a stopgap: developed and tested for Angular 22, until Angular has a built-in option and this package is no longer needed.
If you want the option in Angular, please give the issue and the pull request a π.
Read more: Static Angular SSR: Nice URLs and good SEO - you can't have both (but here's a fix!)
- Angular 22 with the application builder (
@angular/build:application) "outputMode": "static"(with a server, the option is not considered, see Known limitations)- A host that serves
blog/my-article.htmlunder/blog/my-articlewithout a redirect (see Hosts)
ng add @angular-schule/prerender-format
ng buildng add @angular-schule/prerender-format installs the package and changes the builder of your build target in angular.json and sets prerenderFormat.
prerenderFormat: "file" only applies to static builds. If the build target (or one of its configurations) produces an SSR server, ng add still sets it up and shows a warning.
"build": {
"builder": "@angular-schule/prerender-format:application",
"options": {
"outputMode": "static",
"prerenderFormat": "file"
}
}All other options stay as they are: the builder accepts every option of @angular/build:application and passes it on.
Use --project to choose the project in a workspace with several projects.
- optional
- Default:
directory
prerenderFormat |
Route blog/my-article |
Start page |
|---|---|---|
directory |
blog/my-article/index.html |
index.html |
file |
blog/my-article.html |
index.html |
The name and the values follow Astro's build.format.
Parent and child routes live side by side: blog.html next to the folder blog/.
The start page of each locale (for example with base href /en/) stays index.html.
If blog/my-article.html is not safe for a route, that route keeps blog/my-article/index.html and the build shows a warning. This applies to a route whose last segment is index in any letter case (/index, /blog/index), to a file name the build uses itself (such as index.csr.html), and to a file name another route already uses.
Static site generators have offered this choice for a long time. We decided to borrow the terminology from Astro: build.format with 'directory' and 'file' became prerenderFormat with the same values.
| Framework | Option | blog/my-article/index.html |
blog/my-article.html |
|---|---|---|---|
| Astro | build.format |
'directory' (default) |
'file' |
| Next.js (static export) | trailingSlash |
true |
false (default) |
| SvelteKit | trailingSlash |
'always' |
'never' (default) |
| Nuxt 2 | generate.subFolders |
true (default) |
false |
| Hugo | uglyURLs |
false (default) |
true |
| Angular | NEW: prerenderFormat |
'directory' (default) |
'file' |
Astro's documentation recommends build.format: 'file' together with trailingSlash: 'never', which is exactly the combination this builder enables for Angular.
Measured on Cloudflare Pages with prerenderFormat: "file":
| Request | Response |
|---|---|
/blog/my-article |
200, blog/my-article.html |
/blog/my-article/ |
308 β /blog/my-article |
/blog/my-article.html |
308 β /blog/my-article |
/blog with blog.html next to the folder blog/ |
200, blog.html |
Old addresses with a trailing slash keep working, they redirect to the address without it.
GitHub Pages, Firebase Hosting, Vercel and Netlify are listed in the Angular issue as supporting this, some of them behind a setting. Check your host before switching.
The builder calls buildApplication from @angular/build and wraps its internal prerenderPages() function, which returns the prerendered pages as a record of output paths.
The wrapper renames blog/my-article/index.html to blog/my-article.html before anything is written, so the service worker manifest and all later build steps see the final file names.
prerenderPages() is internal API, so this package supports Angular 22 only.
If the prerendered pages did not go through the wrapper (nothing was prerendered, or a version of @angular/build with different internals), the build shows a warning and keeps the directory layout.
The option never makes your build fail.
-
Static builds only, by design.
prerenderFormat: "file"solves a problem of static hosting and makes no sense in other setups. Anssrentry is fine as long as"outputMode"is"static": Angular then uses it only duringng buildto prerender the pages, and no server is deployed. If a server is deployed, the option is not considered and the build shows a warning: a server needs no.htmlfiles, it answers/blog/my-articledirectly without redirecting to/blog/my-article/, and the Angular SSR server looks up prerendered pages asindex.html.β Works: static output, the
ssrentry only renders at build time"options": { "outputMode": "static", "server": "src/main.server.ts", "ssr": { "entry": "src/server.ts" }, "prerenderFormat": "file" }
β Works: prerendering without SSR
"options": { "server": "src/main.server.ts", "prerender": true, "prerenderFormat": "file" }
β οΈ Not considered, with a warning: a server is deployed"options": { "outputMode": "server", "server": "src/main.server.ts", "ssr": { "entry": "src/server.ts" }, "prerenderFormat": "file" }
β οΈ Not considered, with a warning: SSR withoutoutputModealso deploys a server"options": { "server": "src/main.server.ts", "ssr": { "entry": "src/server.ts" }, "prerenderFormat": "file" }
-
ng testwarning. The@angular/build:unit-testbuilder logs a warning when itsbuildTargetuses a builder other than@angular/build:application. Tests run normally.
Code released under the MIT license.