fix(docs-infra): make absolute angular.dev hrefs relative in CLI option descriptions

CLI option descriptions are sourced from `@angular/cli` schema JSON
files, several of which contain absolute `https://angular.dev/...` URLs
in their `description` text. Those URLs render with the external-link
icon and push preview users out to production when viewed on
`next.angular.dev` or other dev previews. The path bypasses the existing
`link.mts` ban on absolute angular.dev links because option descriptions
go through `marked.parse` directly, without `AdevDocsRenderer`. Rewrite
the rendered hrefs whose values begin with `https://angular.dev/` (or
the `http:` variant) to root-relative paths so the resulting anchors
route through Angular's Router and resolve against the active
deployment. Subdomains such as `next.angular.dev/...` are intentionally
not rewritten because they refer to genuinely different deployments.

Closes #68795
This commit is contained in:
Kam
2026-05-19 20:05:02 +03:00
committed by Leon Senft
parent 2953ca7b0d
commit 745ee71c25
3 changed files with 61 additions and 131 deletions
@@ -43,4 +43,17 @@ describe('CLI docs to html', () => {
expect(cliTocs[0].textContent).toContain('ng component [name] [options]');
expect(cliTocs[1].textContent).toContain('ng c [name] [options]');
});
it('should rewrite absolute angular.dev hrefs in option descriptions to root-relative', async () => {
const renderableJson = await getRenderable(entryJson, '', 'angular/cli');
const localFragment = JSDOM.fragment(renderEntry(renderableJson));
const hrefs = Array.from(localFragment.querySelectorAll('a')).map((a) =>
a.getAttribute('href'),
);
// Absolute angular.dev URLs are rewritten to root-relative.
expect(hrefs).toContain('/cli-test-rewrite');
expect(hrefs).not.toContain('https://angular.dev/cli-test-rewrite');
// Subdomains are intentionally left as external.
expect(hrefs).toContain('https://next.angular.dev/cli-test-preview');
});
});
@@ -2,9 +2,7 @@
"name": "generate",
"command": "ng generate <schematic>",
"shortDescription": "Generates and/or modifies files based on a schematic.",
"aliases": [
"g"
],
"aliases": ["g"],
"deprecated": false,
"options": [
{
@@ -16,9 +14,7 @@
{
"name": "dry-run",
"type": "boolean",
"aliases": [
"d"
],
"aliases": ["d"],
"default": false,
"description": "Run through and reports activity without writing out results."
},
@@ -44,6 +40,12 @@
"type": "string",
"description": "The [collection:schematic] to run.",
"positional": 0
},
{
"name": "test-angular-dev-href-rewrite",
"type": "boolean",
"default": false,
"description": "See [angular.dev test](https://angular.dev/cli-test-rewrite) and [preview test](https://next.angular.dev/cli-test-preview)."
}
],
"subcommands": [
@@ -75,17 +77,13 @@
{
"name": "inline-style",
"type": "boolean",
"aliases": [
"s"
],
"aliases": ["s"],
"description": "Include styles inline in the root component.ts file. Only CSS styles can be included inline. Default is false, meaning that an external styles file is created and referenced in the root component.ts file."
},
{
"name": "inline-template",
"type": "boolean",
"aliases": [
"t"
],
"aliases": ["t"],
"description": "Include template inline in the root component.ts file. Default is false, meaning that an external template file is created and referenced in the root component.ts file. "
},
{
@@ -103,9 +101,7 @@
{
"name": "prefix",
"type": "string",
"aliases": [
"p"
],
"aliases": ["p"],
"default": "app",
"description": "A prefix to apply to generated selectors."
},
@@ -135,9 +131,7 @@
{
"name": "skip-tests",
"type": "boolean",
"aliases": [
"S"
],
"aliases": ["S"],
"default": false,
"description": "Do not create \"spec.ts\" test files for the application."
},
@@ -163,29 +157,17 @@
"name": "style",
"type": "string",
"default": "css",
"enum": [
"css",
"scss",
"sass",
"less"
],
"enum": ["css", "scss", "sass", "less"],
"description": "The file extension or preprocessor to use for style files."
},
{
"name": "view-encapsulation",
"type": "string",
"enum": [
"Emulated",
"None",
"ShadowDom",
"ExperimentalIsolatedShadowDom"
],
"enum": ["Emulated", "None", "ShadowDom", "ExperimentalIsolatedShadowDom"],
"description": "The view encapsulation strategy to use in the new application."
}
],
"aliases": [
"app"
],
"aliases": ["app"],
"deprecated": false
},
{
@@ -216,9 +198,7 @@
"description": "Adds a developer-defined type to the filename, in the format \"name.type.ts\"."
}
],
"aliases": [
"cl"
],
"aliases": ["cl"],
"deprecated": false
},
{
@@ -229,22 +209,15 @@
{
"name": "change-detection",
"type": "string",
"aliases": [
"c"
],
"aliases": ["c"],
"default": "Default",
"enum": [
"Default",
"OnPush"
],
"enum": ["Default", "OnPush"],
"description": "The change detection strategy to use in the new component."
},
{
"name": "display-block",
"type": "boolean",
"aliases": [
"b"
],
"aliases": ["b"],
"default": false,
"description": "Specifies if the style will contain `:host { display: block; }`."
},
@@ -269,27 +242,21 @@
{
"name": "inline-style",
"type": "boolean",
"aliases": [
"s"
],
"aliases": ["s"],
"default": false,
"description": "Include styles inline in the component.ts file. Only CSS styles can be included inline. By default, an external styles file is created and referenced in the component.ts file."
},
{
"name": "inline-template",
"type": "boolean",
"aliases": [
"t"
],
"aliases": ["t"],
"default": false,
"description": "Include template inline in the component.ts file. By default, an external template file is created and referenced in the component.ts file."
},
{
"name": "module",
"type": "string",
"aliases": [
"m"
],
"aliases": ["m"],
"description": "The declaring NgModule."
},
{
@@ -301,9 +268,7 @@
{
"name": "prefix",
"type": "string",
"aliases": [
"p"
],
"aliases": ["p"],
"description": "The prefix to apply to the generated component selector."
},
{
@@ -344,13 +309,7 @@
"name": "style",
"type": "string",
"default": "css",
"enum": [
"css",
"scss",
"sass",
"less",
"none"
],
"enum": ["css", "scss", "sass", "less", "none"],
"description": "The file extension or preprocessor to use for style files, or 'none' to skip generating the style file."
},
{
@@ -362,21 +321,12 @@
{
"name": "view-encapsulation",
"type": "string",
"aliases": [
"v"
],
"enum": [
"Emulated",
"None",
"ShadowDom",
"ExperimentalIsolatedShadowDom"
],
"aliases": ["v"],
"enum": ["Emulated", "None", "ShadowDom", "ExperimentalIsolatedShadowDom"],
"description": "The view encapsulation strategy to use in the new component."
}
],
"aliases": [
"c"
],
"aliases": ["c"],
"deprecated": false
},
{
@@ -392,10 +342,7 @@
{
"name": "type",
"type": "string",
"enum": [
"karma",
"browserslist"
],
"enum": ["karma", "browserslist"],
"description": "Specifies which type of configuration file to create.",
"positional": 0
}
@@ -423,9 +370,7 @@
{
"name": "module",
"type": "string",
"aliases": [
"m"
],
"aliases": ["m"],
"description": "The declaring NgModule."
},
{
@@ -437,9 +382,7 @@
{
"name": "prefix",
"type": "string",
"aliases": [
"p"
],
"aliases": ["p"],
"description": "A prefix to apply to generated selectors."
},
{
@@ -471,9 +414,7 @@
"description": "Whether the generated directive is standalone."
}
],
"aliases": [
"d"
],
"aliases": ["d"],
"deprecated": false
},
{
@@ -498,9 +439,7 @@
"description": "Adds a developer-defined type to the filename, in the format \"name.type.ts\"."
}
],
"aliases": [
"e"
],
"aliases": ["e"],
"deprecated": false
},
{
@@ -537,9 +476,7 @@
{
"name": "implements",
"type": "array",
"aliases": [
"guardType"
],
"aliases": ["guardType"],
"description": "Specifies which type of guard to create."
},
{
@@ -560,9 +497,7 @@
"description": "Do not create \"spec.ts\" test files for the new guard."
}
],
"aliases": [
"g"
],
"aliases": ["g"],
"deprecated": false
},
{
@@ -631,9 +566,7 @@
"positional": 1
}
],
"aliases": [
"i"
],
"aliases": ["i"],
"deprecated": false
},
{
@@ -656,9 +589,7 @@
{
"name": "prefix",
"type": "string",
"aliases": [
"p"
],
"aliases": ["p"],
"default": "lib",
"description": "A prefix to apply to generated selectors."
},
@@ -692,9 +623,7 @@
"description": "Creates a library based upon the standalone API, without NgModules."
}
],
"aliases": [
"lib"
],
"aliases": ["lib"],
"deprecated": false
},
{
@@ -711,9 +640,7 @@
{
"name": "module",
"type": "string",
"aliases": [
"m"
],
"aliases": ["m"],
"description": "The declaring NgModule."
},
{
@@ -742,16 +669,11 @@
"name": "routing-scope",
"type": "string",
"default": "Child",
"enum": [
"Child",
"Root"
],
"enum": ["Child", "Root"],
"description": "The scope for the new routing module."
}
],
"aliases": [
"m"
],
"aliases": ["m"],
"deprecated": false
},
{
@@ -774,9 +696,7 @@
{
"name": "module",
"type": "string",
"aliases": [
"m"
],
"aliases": ["m"],
"description": "The declaring NgModule."
},
{
@@ -809,9 +729,7 @@
"description": "Whether the generated pipe is standalone."
}
],
"aliases": [
"p"
],
"aliases": ["p"],
"deprecated": false
},
{
@@ -849,9 +767,7 @@
"description": "Do not create \"spec.ts\" test files for the new resolver."
}
],
"aliases": [
"r"
],
"aliases": ["r"],
"deprecated": false
},
{
@@ -883,9 +799,7 @@
"description": "Do not create \"spec.ts\" test files for the new service."
}
],
"aliases": [
"s"
],
"aliases": ["s"],
"deprecated": false
},
{
@@ -53,11 +53,14 @@ export function getCliCardsRenderable(command: CliCommand): CliCardRenderable[]
return cards;
}
// Rewrite absolute angular.dev hrefs to root-relative so links stay in-site.
const angularDevHrefRegex = /(href=["'])https?:\/\/angular\.dev\//g;
function getRenderableOptions(items: CliOption[]): CliOptionRenderable[] {
return items.map((option) => ({
...option,
deprecated: option.deprecated ? {version: undefined} : undefined,
description: marked.parse(option.description) as string,
description: (marked.parse(option.description) as string).replace(angularDevHrefRegex, '$1/'),
}));
}