diff --git a/.agent/skills/adev-writing-guide/SKILL.md b/.agent/skills/adev-writing-guide/SKILL.md
index 4855ec6b95d..250afcbfad1 100644
--- a/.agent/skills/adev-writing-guide/SKILL.md
+++ b/.agent/skills/adev-writing-guide/SKILL.md
@@ -1,6 +1,6 @@
---
name: adev-writing-guide
-description: Comprehensive writing guide for Angular documentation (adev). Covers Google Technical Writing standards, Angular-specific markdown extensions, code blocks, and components. Use when authoring or reviewing content in adev/src/content.
+description: Comprehensive writing guide for Angular documentation (adev). Covers Google Technical Writing standards, Angular-specific markdown extensions, code blocks, and components. You MUST use this skill any time you plan to create, edit, or review documentation files in `adev/` or `adev/src/content`.
---
# Angular Documentation (adev) Writing Guide
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 8abb9e9bc09..ec9bebf0cb6 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,48 @@
+
+# 22.0.0-next.9 (2026-04-22)
+## Breaking Changes
+### router
+- paramsInheritanceStrategy now defaults to 'always'
+
+ The default value of paramsInheritanceStrategy has been changed from 'emptyOnly' to 'always'. This means that route parameters are inherited from all parent routes by default. To restore the previous behavior, set paramsInheritanceStrategy to 'emptyOnly' in your router configuration.
+### core
+| Commit | Type | Description |
+| -- | -- | -- |
+| [8f3d0b9d97](https://github.com/angular/angular/commit/8f3d0b9d97424e058eb7bce57d80833fb68dec4a) | feat | introduce `@Service` decorator |
+| [9f479ae964](https://github.com/angular/angular/commit/9f479ae9641a5c928f8eeab9c7846245002b3eff) | feat | Update Testability to use PendingTasks for stability indicator |
+### docs
+| Commit | Type | Description |
+| -- | -- | -- |
+| [b24b4cb699](https://github.com/angular/angular/commit/b24b4cb699c325fc2ce40681724341baaabf277b) | fix | link formatting in "Animating your Application with CSS" |
+### migrations
+| Commit | Type | Description |
+| -- | -- | -- |
+| [b395173cf2](https://github.com/angular/angular/commit/b395173cf206b8c04c5ab74298e640c9086d0bac) | fix | fix NgClass leaving trailing comma after removal |
+### router
+| Commit | Type | Description |
+| -- | -- | -- |
+| [6eff439546](https://github.com/angular/angular/commit/6eff4395467de51a46656d79d957b448b32dde0c) | fix | restore internal URL on popstate when `browserUrl` is used |
+| [17d10f7a99](https://github.com/angular/angular/commit/17d10f7a9921429d0192df6925d20d7236425c9a) | fix | set default paramsInheritanceStrategy to 'always' |
+
+
+
+
+# 21.2.10 (2026-04-22)
+### docs
+| Commit | Type | Description |
+| -- | -- | -- |
+| [0d5ee9ae1b](https://github.com/angular/angular/commit/0d5ee9ae1ba4b7acd8f27a059a778f0b4bd8a5bd) | fix | link formatting in "Animating your Application with CSS" |
+### migrations
+| Commit | Type | Description |
+| -- | -- | -- |
+| [5533ab4f56](https://github.com/angular/angular/commit/5533ab4f56f574bc9365cf0573c4a34a3ab5aaf1) | fix | fix NgClass leaving trailing comma after removal |
+### router
+| Commit | Type | Description |
+| -- | -- | -- |
+| [580212c995](https://github.com/angular/angular/commit/580212c995751c4bf4ce8a49df4167498743e0ea) | fix | restore internal URL on popstate when `browserUrl` is used |
+
+
+
# 19.2.21 (2026-04-15)
### platform-server
diff --git a/MODULE.bazel b/MODULE.bazel
index c05d9345fd1..a9821292046 100644
--- a/MODULE.bazel
+++ b/MODULE.bazel
@@ -18,14 +18,14 @@ bazel_dep(name = "yq.bzl", version = "0.3.6")
bazel_dep(name = "rules_angular")
git_override(
module_name = "rules_angular",
- commit = "03dade2ea0ea355e13ca88c550eaa633191b16ec",
+ commit = "72fb243de23a8352f79c034827a4f413cbbc379b",
remote = "https://github.com/angular/rules_angular.git",
)
bazel_dep(name = "devinfra")
git_override(
module_name = "devinfra",
- commit = "e04d90adad1a125b29fbc4d97f425798768a8cb1",
+ commit = "2c2d7e68e6634e9ac92eaa5f12c137a96725d216",
remote = "https://github.com/angular/dev-infra.git",
)
@@ -71,8 +71,8 @@ use_repo(node, "nodejs_windows_amd64")
pnpm = use_extension("@aspect_rules_js//npm:extensions.bzl", "pnpm")
pnpm.pnpm(
name = "pnpm",
- pnpm_version = "10.33.0",
- pnpm_version_integrity = "sha512-EFaLtKavtYyes2MNqQzJUWQXq+vT+rvmc58K55VyjaFJHp21pUTHatjrdXD1xLs9bGN7LLQb/c20f6gjyGSTGQ==",
+ pnpm_version = "10.33.2",
+ pnpm_version_integrity = "sha512-qQ+vb+6rca1sblf5Tg/hoS9dzCLNdU20CulZPraj4LaxLjVAIYuzeuCDQEsfLObbKkEh6XmCm0r/lLmfSdoc+A==",
)
use_repo(pnpm, "pnpm")
diff --git a/README.md b/README.md
index baf7d7a7d94..c1528944d08 100644
--- a/README.md
+++ b/README.md
@@ -67,7 +67,7 @@ Install the Angular CLI globally:
npm install -g @angular/cli
```
-Create workspace:
+Create a workspace:
```
ng new [PROJECT NAME]
diff --git a/adev/package.json b/adev/package.json
index f6fa8dbb32a..16cb0c03e4d 100644
--- a/adev/package.json
+++ b/adev/package.json
@@ -5,21 +5,21 @@
"@algolia/requester-browser-xhr": "5.50.2",
"@algolia/requester-node-http": "5.50.2",
"@angular/animations": "workspace:*",
- "@angular/aria": "22.0.0-next.5",
- "@angular/build": "22.0.0-next.5",
- "@angular/cdk": "22.0.0-next.5",
- "@angular/cli": "22.0.0-next.5",
+ "@angular/aria": "22.0.0-next.6",
+ "@angular/build": "22.0.0-next.6",
+ "@angular/cdk": "22.0.0-next.6",
+ "@angular/cli": "22.0.0-next.6",
"@angular/common": "workspace:*",
"@angular/compiler": "workspace:*",
"@angular/compiler-cli": "workspace:*",
"@angular/core": "workspace:*",
"@angular/docs": "workspace:*",
"@angular/forms": "workspace:*",
- "@angular/material": "22.0.0-next.5",
+ "@angular/material": "22.0.0-next.6",
"@angular/platform-browser": "workspace:*",
"@angular/platform-server": "workspace:*",
"@angular/router": "workspace:*",
- "@angular/ssr": "22.0.0-next.5",
+ "@angular/ssr": "22.0.0-next.6",
"@codemirror/autocomplete": "6.20.1",
"@codemirror/commands": "6.10.3",
"@codemirror/lang-angular": "0.1.4",
diff --git a/adev/shared-docs/pipeline/api-gen/rendering/test/transforms/jsdoc-transforms.spec.mts b/adev/shared-docs/pipeline/api-gen/rendering/test/transforms/jsdoc-transforms.spec.mts
index 285345badc6..fd57b323d22 100644
--- a/adev/shared-docs/pipeline/api-gen/rendering/test/transforms/jsdoc-transforms.spec.mts
+++ b/adev/shared-docs/pipeline/api-gen/rendering/test/transforms/jsdoc-transforms.spec.mts
@@ -199,6 +199,23 @@ describe('jsdoc transforms', () => {
expect(entryFn).toThrowError(/Forbidden relative link: cli\/build ng build/);
});
+
+ it('should throw on a miscased absolute @link to a known API symbol', () => {
+ setSymbols({RouterModule: 'router'});
+
+ const entryFn = () =>
+ addHtmlAdditionalLinks({
+ jsdocTags: [
+ {
+ name: 'see',
+ comment: '{@link /api/router/routerModule#forRoot forRoot}',
+ },
+ ],
+ moduleName: 'test',
+ });
+
+ expect(entryFn).toThrowError(/Broken @link.*Did you mean \/api\/router\/RouterModule/);
+ });
});
describe('addHtmlDescription', () => {
diff --git a/adev/shared-docs/pipeline/api-gen/rendering/transforms/jsdoc-transforms.mts b/adev/shared-docs/pipeline/api-gen/rendering/transforms/jsdoc-transforms.mts
index 38fbe90b85d..5f067da7bb0 100644
--- a/adev/shared-docs/pipeline/api-gen/rendering/transforms/jsdoc-transforms.mts
+++ b/adev/shared-docs/pipeline/api-gen/rendering/transforms/jsdoc-transforms.mts
@@ -223,6 +223,27 @@ function parseAtLink(link: string): {label: string; url: string} | undefined {
);
}
+ // Validate absolute `/api/...` links against the known symbol registry. This catches
+ // miscased symbol names (e.g. `/api/router/routerModule` instead of
+ // `/api/router/RouterModule`) at build time.
+ if (rawSymbol.startsWith('/api/')) {
+ const [pathPart] = rawSymbol.split('#');
+ const segments = pathPart.split('/').filter((s) => s.length > 0);
+ const symbolName = segments[segments.length - 1];
+ // Case-insensitive lookup: find the canonical symbol name in the registry.
+ const knownSymbols = Object.keys(getSymbolsAsApiEntries());
+ const canonicalSymbol = knownSymbols.find(
+ (s) => s.toLowerCase() === symbolName.toLowerCase(),
+ );
+ if (canonicalSymbol && canonicalSymbol !== symbolName) {
+ const expectedUrl = getSymbolUrl(canonicalSymbol);
+ throw Error(
+ `Broken @link: ${link}. Did you mean ${expectedUrl}? ` +
+ `Symbol names in API URLs are case-sensitive.`,
+ );
+ }
+ }
+
return {
url: rawSymbol,
label: description ?? rawSymbol.split('/').pop()!,
diff --git a/adev/shared-docs/pipeline/shared/marked/extensions/docs-card/docs-card.mts b/adev/shared-docs/pipeline/shared/marked/extensions/docs-card/docs-card.mts
index 3642e813c6b..ebd74453da1 100644
--- a/adev/shared-docs/pipeline/shared/marked/extensions/docs-card/docs-card.mts
+++ b/adev/shared-docs/pipeline/shared/marked/extensions/docs-card/docs-card.mts
@@ -18,6 +18,7 @@ interface DocsCardToken extends Tokens.Generic {
href?: string;
imgSrc?: string;
iconImgSrc?: string; // Need image since icons are custom
+ titleInline?: boolean;
tokens: Token[];
}
@@ -30,6 +31,7 @@ const linkRule = /link="([^"]*)"/;
const hrefRule = /href="([^"]*)"/;
const imgSrcRule = /imgSrc="([^"]*)"/;
const iconImgSrcRule = /iconImgSrc="([^"]*)"/;
+const titleInlineRule = /(?:^|\s)titleInline(?=\s|$|=)/;
export const docsCardExtension = {
name: 'docs-card',
@@ -47,6 +49,7 @@ export const docsCardExtension = {
const href = hrefRule.exec(attr);
const imgSrc = imgSrcRule.exec(attr);
const iconImgSrc = iconImgSrcRule.exec(attr);
+ const titleInline = titleInlineRule.test(attr);
const body = match[2].trim();
@@ -59,6 +62,7 @@ export const docsCardExtension = {
link: link ? link[1] : undefined,
imgSrc: imgSrc ? imgSrc[1] : undefined,
iconImgSrc: iconImgSrc ? iconImgSrc[1] : undefined,
+ titleInline,
tokens: [],
};
this.lexer.blockTokens(token.body, token.tokens);
@@ -79,12 +83,14 @@ function getStandardCard(renderer: AdevDocsRenderer, token: DocsCardToken) {
// We need to read svg content, instead of renering svg with `img`,
// cause we would like to use CSS variables to support dark and light mode.
const icon = loadWorkspaceRelativeFile(token.iconImgSrc);
+ const header = token.titleInline
+ ? `
`
+ : `${icon}
- ${icon}
-
${token.title}
+ ${header}
${renderer.parser.parse(token.tokens)}
${token.link ? token.link : 'Learn more'}
diff --git a/adev/shared-docs/services/search-history.service.ts b/adev/shared-docs/services/search-history.service.ts
index 03f3e6d059e..3a387414601 100644
--- a/adev/shared-docs/services/search-history.service.ts
+++ b/adev/shared-docs/services/search-history.service.ts
@@ -6,7 +6,7 @@
* found in the LICENSE file at https://angular.dev/license
*/
-import {computed, inject, Injectable, signal} from '@angular/core';
+import {computed, inject, signal, Service} from '@angular/core';
import {LOCAL_STORAGE} from '../providers';
import {SearchResultItem} from '../interfaces';
@@ -29,7 +29,7 @@ function cleanUpHtml(label: string | null): string {
return (label || '').replace(/<\/?mark>/g, '');
}
-@Injectable({providedIn: 'root'})
+@Service()
export class SearchHistory {
private readonly localStorage = inject(LOCAL_STORAGE);
private readonly history = signal