Skip to content

Commit a7926bb

Browse files
authored
Article API: Add title/intro consistency and refactor transformers (#59103)
1 parent 4b2d104 commit a7926bb

20 files changed

Lines changed: 424 additions & 339 deletions
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import type { Context, Page } from '@/types'
2+
import { renderContent } from '@/content-render/index'
3+
import matter from '@gr2m/gray-matter'
4+
5+
/**
6+
* Extract manual content from page markdown
7+
* Used by GraphQL transformers to get content before the auto-generated marker
8+
*/
9+
export async function extractManualContent(page: Page, context: Context): Promise<string> {
10+
if (!page.markdown) return ''
11+
12+
const markerIndex = page.markdown.indexOf(
13+
'<!-- Content after this section is automatically generated -->',
14+
)
15+
16+
if (markerIndex <= 0) return ''
17+
18+
const { content } = matter(page.markdown)
19+
const manualContentMarkerIndex = content.indexOf(
20+
'<!-- Content after this section is automatically generated -->',
21+
)
22+
23+
if (manualContentMarkerIndex <= 0) return ''
24+
25+
const rawManualContent = content.substring(0, manualContentMarkerIndex).trim()
26+
if (!rawManualContent) return ''
27+
28+
return await renderContent(rawManualContent, {
29+
...context,
30+
markdownRequested: true,
31+
})
32+
}

src/article-api/middleware/article-body.ts

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,5 +74,20 @@ export async function getArticleBody(req: ExtendedRequestWithPageInfo) {
7474
// these parts allow us to render the page
7575
const renderingReq = await createContextualizedRenderingRequest(pathname, page)
7676
renderingReq.context.markdownRequested = true
77-
return await page.render(renderingReq.context)
77+
const content = await page.render(renderingReq.context)
78+
79+
// Get title and intro for consistency with transformer-based pages
80+
const title = page.title
81+
const intro = page.intro
82+
? await page.renderProp('intro', renderingReq.context, { textOnly: true })
83+
: ''
84+
85+
// Prepend title and intro to the content
86+
let result = `# ${title}\n\n`
87+
if (intro) {
88+
result += `${intro}\n\n`
89+
}
90+
result += content
91+
92+
return result
7893
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
# {{ page.title }}
2+
3+
{% if page.intro %}
4+
{{ page.intro }}
5+
{% endif %}
6+
7+
{{ content }}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
# {{ page.title }}
2+
3+
{% if page.intro %}
4+
{{ page.intro }}
5+
{% endif %}
6+
7+
{{ content }}

src/article-api/templates/webhooks-page.template.md

Lines changed: 34 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,42 @@
1111
{% for webhook in webhooks %}
1212
## {{ webhook.name }}
1313

14-
**Available actions:** {% for actionType in webhook.actionTypes %}{% if forloop.last and forloop.length > 1 %}and {% endif %}`{{ actionType }}`{% unless forloop.last %}{% if forloop.length > 2 %}, {% else %} {% endif %}{% endunless %}{% endfor %}
14+
{% if webhook.summary %}
15+
{{ webhook.summary }}
1516

16-
{% if webhook.data.descriptionHtml %}
17-
{{ webhook.data.descriptionHtml }}
1817
{% endif %}
18+
{% if webhook.availability.size > 0 %}
19+
### Availability
1920

20-
**Availability:** {% for availability in webhook.data.availability %}{% if forloop.last and forloop.length > 1 %}and {% endif %}`{{ availability }}`{% unless forloop.last %}{% if forloop.length > 2 %}, {% else %} {% endif %}{% endunless %}{% endfor %}
21+
{% for avail in webhook.availability %}- `{{ avail }}`
22+
{% endfor %}
23+
24+
{% endif %}
25+
### Webhook payload object
26+
27+
{% if webhook.actionTypes.size > 1 %}
28+
**Action type:** {% for actionType in webhook.actionTypes %}`{{ actionType }}`{% unless forloop.last %}, {% endunless %}{% endfor %}
29+
30+
{% endif %}
31+
{% if webhook.description %}
32+
{{ webhook.description }}
33+
34+
{% endif %}
35+
{% if webhook.bodyParameters.size > 0 %}
36+
#### Webhook payload object parameters
37+
38+
| Name | Type | Description |
39+
|------|------|-------------|
40+
{% for param in webhook.bodyParameters %}| `{{ param.name }}` | `{{ param.type }}` | {% if param.isRequired %}**Required.** {% endif %}{{ param.description }} |
41+
{% endfor %}
2142

43+
{% endif %}
44+
{% if webhook.payloadExample %}
45+
### Webhook payload example
46+
47+
```json
48+
{{ webhook.payloadExample }}
49+
```
50+
51+
{% endif %}
2252
{% endfor %}

src/article-api/tests/article-body.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,15 @@ describe('article body api', () => {
2525
expect(res.headers['content-type']).toContain('text/markdown')
2626
})
2727

28+
test('body includes title and intro', async () => {
29+
const res = await get(makeURL('/en/get-started/start-your-journey/hello-world'))
30+
expect(res.statusCode).toBe(200)
31+
// Body should start with the page title as H1
32+
expect(res.body).toMatch(/^# Hello World/)
33+
// Body should include the intro after the title
34+
expect(res.body).toContain('Follow this Hello World exercise to get started with')
35+
})
36+
2837
test('octicons auto-generate aria-labels', async () => {
2938
const res = await get(makeURL('/en/get-started/start-your-journey/hello-world'))
3039
expect(res.statusCode).toBe(200)

src/article-api/tests/webhooks-transformer.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -93,8 +93,8 @@ describe('Webhooks transformer', () => {
9393
// Should show payload object parameters section
9494
expect(res.body).toContain('### Webhook payload object')
9595
expect(res.body).toContain('#### Webhook payload object parameters')
96-
// Should have a markdown table with parameter columns
97-
expect(res.body).toContain('| Name | Type | Description |')
96+
// Should have a markdown table with parameter columns (may have extra spacing from formatting)
97+
expect(res.body).toMatch(/\|\s*Name\s*\|\s*Type\s*\|\s*Description\s*\|/)
9898
})
9999

100100
test('webhooks show descriptions', async () => {

src/article-api/transformers/audit-logs-transformer.ts

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,19 +2,16 @@ import type { Context, Page } from '@/types'
22
import type { PageTransformer } from './types'
33
import type { CategorizedEvents } from '@/audit-logs/types'
44
import { renderContent } from '@/content-render/index'
5+
import { loadTemplate } from '@/article-api/lib/load-template'
56
import matter from '@gr2m/gray-matter'
6-
import { readFileSync } from 'fs'
7-
import { join, dirname } from 'path'
8-
import { fileURLToPath } from 'url'
9-
10-
const __filename = fileURLToPath(import.meta.url)
11-
const __dirname = dirname(__filename)
127

138
/**
149
* Transformer for Audit Logs pages
1510
* Converts audit log events and their data into markdown format using a Liquid template
1611
*/
1712
export class AuditLogsTransformer implements PageTransformer {
13+
templateName = 'audit-logs-page.template.md'
14+
1815
canTransform(page: Page): boolean {
1916
return page.autogenerated === 'audit-logs'
2017
}
@@ -76,8 +73,7 @@ export class AuditLogsTransformer implements PageTransformer {
7673
)
7774

7875
// Load and render template
79-
const templatePath = join(__dirname, '../templates/audit-logs-page.template.md')
80-
const templateContent = readFileSync(templatePath, 'utf8')
76+
const templateContent = loadTemplate(this.templateName)
8177

8278
// Render the template with Liquid
8379
const rendered = await renderContent(templateContent, {

src/article-api/transformers/codeql-cli-transformer.ts

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,47 @@
11
import type { Context, Page } from '@/types'
22
import type { PageTransformer } from './types'
3+
import { renderContent } from '@/content-render/index'
4+
import { loadTemplate } from '@/article-api/lib/load-template'
35
import { stripHtmlCommentsAndNormalizeWhitespace } from '@/article-api/lib/strip-html-comments'
46

57
/**
68
* Transformer for CodeQL CLI reference pages.
7-
* Renders autogenerated CodeQL CLI documentation pages as markdown.
9+
* Renders autogenerated CodeQL CLI documentation pages as markdown using a Liquid template.
810
* Sets `markdownRequested` to true in the context to ensure the page is rendered as markdown,
911
* bypassing the default article type check.
1012
*/
1113
export class CodeQLCliTransformer implements PageTransformer {
14+
templateName = 'codeql-cli-page.template.md'
15+
1216
canTransform(page: Page): boolean {
1317
return page.autogenerated === 'codeql-cli'
1418
}
1519

1620
async transform(page: Page, _pathname: string, context: Context): Promise<string> {
1721
// CodeQL CLI pages are fully generated markdown files in the repo.
18-
// We render them with markdownRequested=true to get the markdown output,
19-
// similar to how regular articles are rendered but through the transformer pattern.
22+
// We render them with markdownRequested=true to get the markdown output.
2023
context.markdownRequested = true
2124
const content = await page.render(context)
2225

2326
const intro = page.intro ? await page.renderProp('intro', context, { textOnly: true }) : ''
2427

25-
const result = `# ${page.title}\n\n${intro}\n\n${content}`
28+
// Prepare template data
29+
const templateData: Record<string, unknown> = {
30+
page: {
31+
title: page.title,
32+
intro,
33+
},
34+
content,
35+
}
36+
37+
// Load and render template
38+
const templateContent = loadTemplate(this.templateName)
39+
40+
const result = await renderContent(templateContent, {
41+
...context,
42+
...templateData,
43+
markdownRequested: true,
44+
})
2645

2746
// Strip HTML comments (e.g., markdownlint-disable comments) from the output
2847
return stripHtmlCommentsAndNormalizeWhitespace(result)

src/article-api/transformers/github-apps-transformer.ts

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,9 @@
11
import type { Context, Page } from '@/types'
22
import type { PageTransformer } from './types'
33
import { renderContent } from '@/content-render/index'
4+
import { loadTemplate } from '@/article-api/lib/load-template'
45
import matter from '@gr2m/gray-matter'
5-
import { readFileSync } from 'fs'
6-
import { join, dirname } from 'path'
7-
import { fileURLToPath } from 'url'
86

9-
const __filename = fileURLToPath(import.meta.url)
10-
const __dirname = dirname(__filename)
117
const DEBUG = process.env.RUNNER_DEBUG === '1' || process.env.DEBUG === '1'
128

139
// GitHub Apps data types
@@ -91,6 +87,8 @@ const PERMISSIONS_PAGE_TYPES = new Set([
9187
* in TypeScript for permissions pages to avoid Liquid escaping issues.
9288
*/
9389
export class GithubAppsTransformer implements PageTransformer {
90+
templateName = 'github-apps-page.template.md'
91+
9492
canTransform(page: Page): boolean {
9593
return page.autogenerated === 'github-apps'
9694
}
@@ -160,9 +158,8 @@ export class GithubAppsTransformer implements PageTransformer {
160158
isPermissionsPage,
161159
)
162160

163-
// Load and render template
164-
const templatePath = join(__dirname, '../templates/github-apps-page.template.md')
165-
const templateContent = readFileSync(templatePath, 'utf8')
161+
// Load template
162+
const templateContent = loadTemplate(this.templateName)
166163

167164
// For permissions pages, we need to construct the tables manually to avoid Liquid escaping
168165
let finalContent: string

0 commit comments

Comments
 (0)