Visitar URL original
feat(@angular/build): add `prerender.format` option to prerender routes as `<route>.html` by JohannesHoppe · Pull Request #34180 · angular/angular-cli · GitHub
Skip to content

feat(@angular/build): add prerender.format option to prerender routes as <route>.html - #34180

Open
JohannesHoppe wants to merge 3 commits into
angular:mainfrom
JohannesHoppe:feat/prerender-format
Open

JohannesHoppe wants to merge 3 commits into
angular:mainfrom
JohannesHoppe:feat/prerender-format

Conversation

@JohannesHoppe

@JohannesHoppe JohannesHoppe commented Sep 25, 2026 •

Copy link
Copy Markdown
Contributor

PR Checklist

Please check to confirm your PR fulfills the following requirements:

PR Type

What kind of change does this PR introduce?

  • Bugfix
  • Feature
  • Code style update (formatting, local variables)
  • Refactoring (no functional changes, no api changes)
  • Build related changes
  • CI related changes
  • Documentation content changes
  • Other... Please describe:

What is the current behavior?

Prerendering always writes a route to <route>/index.html. On static hosts, this forces a choice between clean URLs and no redirects. You can't have both:

  • Clean URLs, but redirects: links use /<route>. Every direct request (search engines, bookmarks, shared links) is first redirected (301/308) to /<route>/, and the Angular router then removes the trailing slash again.
  • No redirects, but trailing slashes everywhere: links have to use /<route>/, and the app needs TrailingSlashPathLocationStrategy to keep the slash in the address bar.

Issue Number: closes #29173

What is the new behavior?

A new format property of the prerender option makes both possible: clean URLs without a trailing slash, served directly with a status code 200.

"prerender": {
  "format": "file"
}
  • "directory" (default): /foo/bar is written to foo/bar/index.html, unchanged.
  • "file": /foo/bar is written to foo/bar.html, so hosts that serve <route>.html for /<route> respond to /foo/bar without a redirect.

Mirrors Astro's build.format ('directory' | 'file'); Next.js, SvelteKit and Hugo offer the same choice via trailingSlash/uglyURLs.

  • The root route (after removing the baseHref option) stays index.html, so / and /<locale>/ keep being served by the host's directory index.
  • "file" is only considered when the build does not produce a server. The @angular/ssr runtime looks up prerendered pages as <route>/index.html (AngularServerApp.buildServerAssetPathFromRequest, CommonEngine.retrieveSSGPage), and with a server there is no redirect to avoid, since the generated server.ts serves static files with redirect: false. With outputMode: "server", or ssr without outputMode, the build warns that prerender.format is not considered and prerenders to <route>/index.html. The dev server does not prerender and shows no warning.
  • prerender.format is also considered when outputMode is set. The other prerender settings (routesFile, discoverRoutes) keep being ignored there, and the warning now names them.

As suggested by @SanderElias in the issue, the option description states that not all hosting services support this.

The API golden changes in one line: the generated enum for prerender.format is named Format, so the existing Format of the extract-i18n builder is listed as Format_2.

Does this PR introduce a breaking change?

  • Yes
  • No

Other information

As a stopgap, I built a builder (@angular-schule/prerender-format) that wraps @angular/build:application. It only works by replacing the internal prerenderPages() export of @angular/build at runtime to rename the output files, which is fragile and can break with any internal refactoring. A built-in option is the clean solution.

As a nested property, prerender.format does not show up in ng build --help. If this lands, I'm happy to follow up with a short section in the SSR guide ("Generate a fully static application") in angular/angular.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces the "prerenderFormat" option to the Angular application builder, allowing prerendered pages to be output as ".html" files (using the "file" format) instead of the default "/index.html" structure. It includes robust conflict resolution logic for routes that cannot be safely written to ".html" (such as routes named "index" or matching the application's index file), falling back to the directory format with a warning. The review feedback suggests a performance optimization in "prerender.ts" to pre-lowercase the "indexOutput" variable once outside the route rendering loop, rather than performing redundant string operations inside "getFileFormatConflict" for every route.

Comment thread packages/angular/build/src/utils/server-rendering/prerender.ts
Comment thread packages/angular/build/src/utils/server-rendering/prerender.ts Outdated
Comment thread packages/angular/build/src/utils/server-rendering/prerender.ts Outdated
@alan-agius4
alan-agius4 self-requested a review September 28, 2026 11:37
@alan-agius4 alan-agius4 added the action: review The PR is still awaiting reviews from at least one requested reviewer label Oct 1, 2026

@alan-agius4 alan-agius4 left a comment •

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for working on this, couple of comments.

@alan-agius4 alan-agius4 removed the action: review The PR is still awaiting reviews from at least one requested reviewer label Oct 8, 2026
}
]
},
"prerenderFormat": {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rather than introducing a separate top-level option (prerenderFormat), let's make format an optional property of the prerender object:

"prerender": {
  "format": "file"
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done, it is now prerender.format. Since the prerender option is otherwise ignored when outputMode is set, options.ts now reads format first. The existing warning stays where prerender is ignored, and a new one names routesFile and discoverRoutes when only those are ignored.

lowerIndexOutput,
usedFiles,
);
if (reason) {

@alan-agius4 alan-agius4 Oct 8, 2026 •

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's revert this conflict detection and fallback logic. We should keep this PR focused strictly on introducing the file format output without expanding into route collision detection and fallbacks.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done, reverted.

if (prerenderFormat === PrerenderFormat.File && !outputOptions.ignoreServer) {
// The server runtime of '@angular/ssr' looks up prerendered pages as '<route>/index.html'.
// The warning is only relevant when pages are actually prerendered, which the dev-server skips.
if ((prerenderOptions || appShellOptions) && !(options.partialSSRBuild || usePartialSsrBuild)) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When using outputMode: OutputMode.Server with modern server routing (app.routes.server.ts), routes can be marked as RenderMode.Prerender without configuring prerender in angular.json.

In that scenario, prerenderOptions is undefined, so (prerenderOptions || appShellOptions) evaluates to undefined, and this warning is silently skipped even though the user explicitly set format: 'file'. We should log the warning whenever format === 'file' and a server is produced, without gating on prerenderOptions.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done, the warning is now logged whenever format is file and the build produces a server.

await expectFileNotToExist(join('dist/test-project/browser/**/index.html'));

// Write each route to '<route>.html'
await noSilentNg('build', '--output-mode=static', '--prerender-format=file');

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This addition to the e2e test can be reverted. The static build behavior, file output layout, and manifests are already thoroughly covered in packages/angular/build/src/builders/application/tests/options/prerender-format_spec.ts#L114, and adding another build to the e2e suite noticeably increases CI run times.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done, reverted.

harness.expectFile('dist/browser/foo/index.html').toNotExist();
});

it(`should keep '<route>/index.html' with a warning for a route named like the index file when set to 'file'`, async () => {

@alan-agius4 alan-agius4 Oct 8, 2026 •

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using index: { output: '404.html' } is an unexpected configuration—index is intended to configure the application's primary entry file.

Adding specialized logic and threading indexOutput through prerender.ts and execute-post-bundle.ts to detect and divert routes matching index.output is unnecessary. This test and the associated indexOutput conflict logic should be removed.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done, removed together with the indexOutput plumbing.

);
}

describe('Option: "prerenderFormat"', () => {

@alan-agius4 alan-agius4 Oct 8, 2026 •

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A lot of these tests represent unrequested behavior changes and over-testing rather than verifying the option itself:

  • Tests like the index, 404, and case-collision scenarios invent and lock down speculative behavior rules and fallback diversions that shouldn't be part of this feature.
  • Scenarios like full multi-locale i18n builds with XLIFF translation files and baseHref stripping over-test other subsystems that already have their own dedicated test suites.

Once format is moved under prerender and the conflict detection logic is removed, let's keep this spec focused strictly on the option itself:

  1. Emits <route>.html when format: 'file' (and root / as index.html).
  2. Emits <route>/index.html when format: 'directory' (default).
  3. Warns when configured alongside a server build.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done, the spec now covers exactly these three cases.

@@ -0,0 +1,368 @@
/**

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A lot of these tests represent unrequested behavior changes and over-testing rather than verifying the option itself.

Tests like the index, 404, and case-collision scenarios invent and lock down speculative behavior rules and fallback diversions that shouldn't be part of this feature.

Scenarios like full multi-locale i18n builds with XLIFF translation files and baseHref stripping over-test other subsystems that already have their own dedicated test suites.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done, see the reply above: the spec now covers exactly the three cases.

@alan-agius4 alan-agius4 added the action: cleanup The PR is in need of cleanup, either due to needing a rebase or in response to comments from reviews label Oct 8, 2026
…es as `<route>.html`

The new `format` property of the `prerender` option accepts `directory` (default, unchanged
behavior) and `file`, like the `build.format` option of Astro. With `file`, routes are written
to `<route>.html` instead of `<route>/index.html`. Static hosts serve `<route>/index.html` under
`/<route>/`, so a request to `/<route>` is first redirected to the URL with a trailing slash,
which the Angular router then removes again. Hosts that serve `<route>.html` for `/<route>`
respond without that redirect.

The root route (after removing the `baseHref` option) is still written to `index.html`. Static
redirect pages follow the same layout. `prerender.format` is also considered when `outputMode`
is set, while `prerender.routesFile` and `prerender.discoverRoutes` are not.

`file` is only considered when the build does not produce a server, because the `@angular/ssr`
runtime looks up prerendered pages as `<route>/index.html`. In that case, a warning is reported
and routes are written to `<route>/index.html`.

Closes angular#29173
JohannesHoppe added a commit to angular-schule/prerender-format that referenced this pull request Oct 8, 2026
…0.3.0)

The "file" format is configured as `"prerender": { "format": "file" }`,
the same option as in the pull request for the Angular CLI. The builder
takes `format` out of the prerender option, so it also works with
`"outputMode": "static"`. Routes are renamed from `<route>/index.html`
to `<route>.html` without special rules for individual routes.

BREAKING CHANGE: the top-level option `prerenderFormat` is replaced by
`prerender.format`. Run `ng add @angular-schule/prerender-format` again
or move the value into the `prerender` object.
@JohannesHoppe
JohannesHoppe force-pushed the feat/prerender-format branch from 7a9034f to 13f8dea Compare October 8, 2026 16:25
@JohannesHoppe JohannesHoppe changed the title feat(@angular/build): add prerenderFormat option to prerender routes as <route>.html feat(@angular/build): add prerender.format option to prerender routes as <route>.html Oct 8, 2026
@JohannesHoppe

Copy link
Copy Markdown
Contributor Author

Thanks for the thorough review, Alan! All points are addressed. I also reworded the original commit message for the new option name, which needed a force-push. The review changes themselves are in the fixup commits.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

action: cleanup The PR is in need of cleanup, either due to needing a rebase or in response to comments from reviews area: @angular/build detected: feature PR contains a feature commit

Projects

None yet

Development

Successfully merging this pull request may close these issues.

SSG/Prerendering: Allow generating foo.html instead of foo/index.html

2 participants