Skip to content

Commit 9021845

Browse files
heiskrCopilot
andauthored
Add redirectedFrom metadata to article API responses (#60536)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 75ec4f8 commit 9021845

File tree

5 files changed

+17
-3
lines changed

5 files changed

+17
-3
lines changed

src/article-api/README.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,11 @@ The `/api/article` endpoints return information about a page by `pathname`.
2424

2525
`api/article/meta` is highly cached, in JSON format.
2626

27-
### Autogenerated Content Transformers
27+
### Redirects and the pagelist
28+
29+
The pagelist (`/api/pagelist/:lang/:version`) returns only **canonical permalinks**. The article API (`/api/article`, `/api/article/body`, `/api/article/meta`) transparently follows redirects—so URLs that don't appear in the pagelist (such as `redirect_from` aliases or old paths) may still return content.
30+
31+
When the article API resolves a redirect through the redirect table, the JSON response includes a `redirectedFrom` field containing the normalized pathname that was looked up (after trailing-slash removal and other standard normalization, not the raw originally-requested pathname). This field is only set for redirect-table lookups; it is not set for the bare `/` to `/<lang>` language rewrite. This lets consumers detect that the URL they requested is not canonical. The `/api/article/body` endpoint returns plain text, so `redirectedFrom` is not included in its response.
2832

2933
For autogenerated pages (REST, GraphQL, webhooks, landing pages, audit logs, etc), the Article API uses specialized transformers to convert the rendered content into markdown format. These transformers are located in `src/article-api/transformers/` and use an extensible architecture.
3034

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

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,7 @@ export async function getMetadata(req: ExtendedRequestWithPageInfo) {
134134
// /articles or '/en/enterprise-server@latest/foo/bar)
135135
// So by the time we get here, the pathname should be one of the
136136
// page's valid permalinks.
137-
const { page, pathname, archived } = req.pageinfo
137+
const { page, pathname, archived, redirectedFrom } = req.pageinfo
138138
const documentType = page?.documentType ?? null
139139

140140
if (archived && archived.isArchived) {
@@ -157,5 +157,8 @@ export async function getMetadata(req: ExtendedRequestWithPageInfo) {
157157
const fromCache = await getPageInfoFromCache(page, pathname)
158158
const { cacheInfo, ...meta } = fromCache
159159

160-
return { meta: { ...meta, documentType }, cacheInfo }
160+
return {
161+
meta: { ...meta, documentType, ...(redirectedFrom && { redirectedFrom }) },
162+
cacheInfo,
163+
}
161164
}

src/article-api/middleware/validation.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,7 @@ export const pageValidationMiddleware = (
109109
if (!req.pageinfo.archived.isArchived) {
110110
const redirect = getRedirect(pathname, redirectsContext)
111111
if (redirect) {
112+
req.pageinfo.redirectedFrom = pathname
112113
pathname = redirect
113114
}
114115
}

src/article-api/tests/pageinfo.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ interface PageMetadata {
1212
title: string
1313
intro: string
1414
documentType: string | null
15+
redirectedFrom?: string
1516
}
1617

1718
interface ErrorResponse {
@@ -46,6 +47,8 @@ describe('pageinfo api', () => {
4647
'Get started using HubGit to manage Git repositories and collaborate with others.',
4748
)
4849
expect(meta.documentType).toBe('category')
50+
// Canonical URLs should not have redirectedFrom
51+
expect(meta.redirectedFrom).toBeUndefined()
4952
// Check that it can be cached at the CDN
5053
expect(res.headers['set-cookie']).toBeUndefined()
5154
expect(res.headers['cache-control']).toContain('public')
@@ -90,13 +93,15 @@ describe('pageinfo api', () => {
9093
expect(res.statusCode).toBe(200)
9194
const meta = JSON.parse(res.body) as PageMetadata
9295
expect(meta.title).toBe('HubGit.com Fixture Documentation')
96+
expect(meta.redirectedFrom).toBe('/en/olden-days')
9397
}
9498
// Trailing slashes are always removed
9599
{
96100
const res = await get(makeURL('/en/olden-days/'))
97101
expect(res.statusCode).toBe(200)
98102
const meta = JSON.parse(res.body) as PageMetadata
99103
expect(meta.title).toBe('HubGit.com Fixture Documentation')
104+
expect(meta.redirectedFrom).toBe('/en/olden-days')
100105
}
101106
// Short code for latest version
102107
{

src/article-api/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,5 +10,6 @@ export type ExtendedRequestWithPageInfo = ExtendedRequest & {
1010
pathname: string
1111
page: Page
1212
archived?: ArchivedVersion
13+
redirectedFrom?: string
1314
}
1415
}

0 commit comments

Comments
 (0)