Skip to content

Commit d01e267

Browse files
authored
add content linter rule for carousel recommended articles (#57216)
1 parent a292bb3 commit d01e267

File tree

14 files changed

+306
-0
lines changed

14 files changed

+306
-0
lines changed

data/reusables/contributing/content-linter-rules.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@
7171
| GHD053 | header-content-requirement | Headers must have content between them, such as an introduction | warning | headers, structure, content |
7272
| GHD054 | third-party-actions-reusable | Code examples with third-party actions must include disclaimer reusable | warning | actions, reusable, third-party |
7373
| GHD055 | frontmatter-validation | Frontmatter properties must meet character limits and required property requirements | warning | frontmatter, character-limits, required-properties |
74+
| GHD056 | frontmatter-landing-recommended | Only landing pages can have recommended articles, there should be no duplicate recommended articles, and all recommended articles must exist | error | frontmatter, landing, recommended |
7475
| [search-replace](https://github.com/OnkarRuikar/markdownlint-rule-search-replace) | deprecated liquid syntax: octicon-<icon-name> | The octicon liquid syntax used is deprecated. Use this format instead `octicon "<octicon-name>" aria-label="<Octicon aria label>"` | error | |
7576
| [search-replace](https://github.com/OnkarRuikar/markdownlint-rule-search-replace) | deprecated liquid syntax: site.data | Catch occurrences of deprecated liquid data syntax. | error | |
7677
| [search-replace](https://github.com/OnkarRuikar/markdownlint-rule-search-replace) | developer-domain | Catch occurrences of developer.github.com domain. | error | |
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
import fs from 'fs'
2+
import path from 'path'
3+
import { addError } from 'markdownlint-rule-helpers'
4+
5+
import { getFrontmatter } from '../helpers/utils'
6+
7+
function isValidArticlePath(articlePath, currentFilePath) {
8+
// Article paths in recommended are relative to the current page's directory
9+
const relativePath = articlePath.startsWith('/') ? articlePath.substring(1) : articlePath
10+
const currentDir = path.dirname(currentFilePath)
11+
const fullPath = path.join(currentDir, `${relativePath}.md`)
12+
13+
try {
14+
return fs.existsSync(fullPath) && fs.statSync(fullPath).isFile()
15+
} catch {
16+
return false
17+
}
18+
}
19+
20+
export const frontmatterLandingRecommended = {
21+
names: ['GHD056', 'frontmatter-landing-recommended'],
22+
description:
23+
'Only landing pages can have recommended articles, there should be no duplicate recommended articles, and all recommended articles must exist',
24+
tags: ['frontmatter', 'landing', 'recommended'],
25+
function: (params, onError) => {
26+
const fm = getFrontmatter(params.lines)
27+
if (!fm || !fm.recommended) return
28+
29+
const recommendedLine = params.lines.find((line) => line.startsWith('recommended:'))
30+
31+
if (!recommendedLine) return
32+
33+
const lineNumber = params.lines.indexOf(recommendedLine) + 1
34+
35+
if (!fm.layout || !fm.layout.includes('landing')) {
36+
addError(
37+
onError,
38+
lineNumber,
39+
'recommended frontmatter key is only valid for landing pages',
40+
recommendedLine,
41+
[1, recommendedLine.length],
42+
null,
43+
)
44+
}
45+
46+
// Check for duplicate recommended items and invalid paths
47+
if (Array.isArray(fm.recommended)) {
48+
const seen = new Set()
49+
const duplicates = []
50+
const invalidPaths = []
51+
52+
fm.recommended.forEach((item) => {
53+
if (seen.has(item)) {
54+
duplicates.push(item)
55+
} else {
56+
seen.add(item)
57+
}
58+
59+
// Validate that the article path exists
60+
if (!isValidArticlePath(item, params.name)) {
61+
invalidPaths.push(item)
62+
}
63+
})
64+
65+
if (duplicates.length > 0) {
66+
addError(
67+
onError,
68+
lineNumber,
69+
`Found duplicate recommended articles: ${duplicates.join(', ')}`,
70+
recommendedLine,
71+
[1, recommendedLine.length],
72+
null,
73+
)
74+
}
75+
76+
if (invalidPaths.length > 0) {
77+
addError(
78+
onError,
79+
lineNumber,
80+
`Found invalid recommended article paths: ${invalidPaths.join(', ')}`,
81+
recommendedLine,
82+
[1, recommendedLine.length],
83+
null,
84+
)
85+
}
86+
}
87+
},
88+
}

src/content-linter/lib/linting-rules/index.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ import { frontmatterVersionsWhitespace } from '@/content-linter/lib/linting-rule
5454
import { frontmatterValidation } from '@/content-linter/lib/linting-rules/frontmatter-validation'
5555
import { headerContentRequirement } from '@/content-linter/lib/linting-rules/header-content-requirement'
5656
import { thirdPartyActionsReusable } from '@/content-linter/lib/linting-rules/third-party-actions-reusable'
57+
import { frontmatterLandingRecommended } from '@/content-linter/lib/linting-rules/frontmatter-landing-recommended'
5758

5859
const noDefaultAltText = markdownlintGitHub.find((elem) =>
5960
elem.names.includes('no-default-alt-text'),
@@ -115,6 +116,7 @@ export const gitHubDocsMarkdownlint = {
115116
headerContentRequirement, // GHD053
116117
thirdPartyActionsReusable, // GHD054
117118
frontmatterValidation, // GHD055
119+
frontmatterLandingRecommended, // GHD056
118120

119121
// Search-replace rules
120122
searchReplace, // Open-source plugin

src/content-linter/style/github-docs.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -310,6 +310,12 @@ export const githubDocsFrontmatterConfig = {
310310
'partial-markdown-files': false,
311311
'yml-files': false,
312312
},
313+
'frontmatter-landing-recommended': {
314+
// GHD056
315+
severity: 'error',
316+
'partial-markdown-files': false,
317+
'yml-files': false,
318+
},
313319
}
314320

315321
// Configures rules from the `github/markdownlint-github` repo
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
---
2+
title: Article One
3+
layout: inline
4+
versions:
5+
fpt: '*'
6+
ghec: '*'
7+
ghes: '*'
8+
topics:
9+
- Testing
10+
---
11+
12+
# Article One
13+
14+
This is a supporting article for testing.
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
---
2+
title: Article Two
3+
layout: inline
4+
versions:
5+
fpt: '*'
6+
ghec: '*'
7+
ghes: '*'
8+
topics:
9+
- Testing
10+
---
11+
12+
# Article Two
13+
14+
This is another supporting article for testing.
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
---
2+
title: Landing with Duplicates
3+
layout: product-landing
4+
versions:
5+
fpt: '*'
6+
ghec: '*'
7+
ghes: '*'
8+
topics:
9+
- Testing
10+
recommended:
11+
- /article-one
12+
- /article-two
13+
- /article-one
14+
- /subdir/article-three
15+
---
16+
17+
# Landing with Duplicates
18+
19+
This landing page has duplicate recommended articles.
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
---
2+
title: Not a Landing Page
3+
layout: inline
4+
versions:
5+
fpt: '*'
6+
ghec: '*'
7+
ghes: '*'
8+
topics:
9+
- Testing
10+
recommended:
11+
- /article-one
12+
- /article-two
13+
- /subdir/article-three
14+
---
15+
16+
# Not a Landing Page
17+
18+
This page has a recommended property but is not a landing page.
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
---
2+
title: Landing with Invalid Paths
3+
layout: product-landing
4+
versions:
5+
fpt: '*'
6+
ghec: '*'
7+
ghes: '*'
8+
topics:
9+
- Testing
10+
recommended:
11+
- /article-one
12+
- /nonexistent/path
13+
- /another/invalid/path
14+
---
15+
16+
# Landing with Invalid Paths
17+
18+
This landing page has some invalid recommended article paths.
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
---
2+
title: Landing without Recommended
3+
layout: product-landing
4+
versions:
5+
fpt: '*'
6+
ghec: '*'
7+
ghes: '*'
8+
topics:
9+
- Testing
10+
---
11+
12+
# Landing without Recommended
13+
14+
This is a landing page without any recommended articles.

0 commit comments

Comments
 (0)