diff --git a/src/article-api/README-pageinfo.md b/src/article-api/README-pageinfo.md deleted file mode 100644 index ec2fece8a2..0000000000 --- a/src/article-api/README-pageinfo.md +++ /dev/null @@ -1,17 +0,0 @@ -# Pageinfo endpoint - -This subject folder contains the code for the `/api/pageinfo` endpoint. - -## What it does - -The `/api/pageinfo` endpoint powers the hover preview for internal links on . - -## How it works - -The `/api/pageinfo` endpoint returns information about a page by `pathname`. It is highly cached, in JSON format. - -## How to get help - -For internal folks ask in the Docs Engineering slack channel. - -For open source folks, please open a discussion in the public repository. diff --git a/src/article-api/README.md b/src/article-api/README.md index 413273733e..3ba4328224 100644 --- a/src/article-api/README.md +++ b/src/article-api/README.md @@ -8,3 +8,17 @@ This subject folder contains the code for the Article API endpoints: ## What it does Article API endpoints allow consumers to query GitHub Docs for listings of current articles, and for specific article information. + +The `/api/article/meta` endpoint powers hovercards, which provide a preview for internal links on . + +## How it works + +The `/api/article` endpoints return information about a page by `pathname`. + +`api/article/meta` is highly cached, in JSON format. + +## How to get help + +For internal folks ask in the Docs Engineering slack channel. + +For open source folks, please open a discussion in the public repository. diff --git a/src/article-api/middleware/article-body.ts b/src/article-api/middleware/article-body.ts new file mode 100644 index 0000000000..d2dd3b8f05 --- /dev/null +++ b/src/article-api/middleware/article-body.ts @@ -0,0 +1,37 @@ +import type { Response } from 'express' + +import { Context } from '#src/types.js' +import { ExtendedRequestWithPageInfo } from '../types' +import contextualize from '#src/frame/middleware/context/context.js' + +export async function getArticleBody(req: ExtendedRequestWithPageInfo) { + // req.pageinfo is set from pageValidationMiddleware and pathValidationMiddleware + // and is in the ExtendedRequestWithPageInfo + const { page, pathname, archived } = req.pageinfo + + if (archived?.isArchived) + throw new Error(`Page ${pathname} is archived and can't be rendered in markdown.`) + // for anything that's not an article (like index pages), don't try to render and + // tell the user what's going on + if (page.documentType !== 'article') { + throw new Error(`Page ${pathname} isn't yet available in markdown.`) + } + // these parts allow us to render the page + const mockedContext: Context = {} + const renderingReq = { + path: pathname, + language: page.languageCode, + pagePath: pathname, + cookies: {}, + context: mockedContext, + headers: { + 'content-type': 'text/markdown', + }, + } + + // contextualize and render the page + await contextualize(renderingReq as ExtendedRequestWithPageInfo, {} as Response, () => {}) + renderingReq.context.page = page + renderingReq.context.markdownRequested = true + return await page.render(renderingReq.context) +} diff --git a/src/article-api/middleware/pageinfo.ts b/src/article-api/middleware/article-pageinfo.ts similarity index 55% rename from src/article-api/middleware/pageinfo.ts rename to src/article-api/middleware/article-pageinfo.ts index 8d1c3ef253..cb500b211f 100644 --- a/src/article-api/middleware/pageinfo.ts +++ b/src/article-api/middleware/article-pageinfo.ts @@ -1,23 +1,11 @@ -import express from 'express' -import type { RequestHandler, Response } from 'express' +import type { Response } from 'express' import type { ExtendedRequestWithPageInfo } from '../types' import type { ExtendedRequest, Page, Context, Permalink } from '@/types' -import statsd from '@/observability/lib/statsd.js' -import { defaultCacheControl } from '@/frame/middleware/cache-control.js' -import catchMiddlewareError from '@/observability/middleware/catch-middleware-error.js' -import { - SURROGATE_ENUMS, - setFastlySurrogateKey, - makeLanguageSurrogateKey, -} from '@/frame/middleware/set-fastly-surrogate-key.js' import shortVersions from '@/versions/middleware/short-versions.js' import contextualize from '@/frame/middleware/context/context' import features from '@/versions/middleware/features.js' import { readCompressedJsonFile } from '@/frame/lib/read-json-file.js' -import { pathValidationMiddleware, pageValidationMiddleware } from './validation' - -const router = express.Router() // If you have pre-computed page info into a JSON file on disk, this is // where it would be expected to be found. @@ -96,7 +84,7 @@ type CachedPageInfo = { } let _cache: CachedPageInfo | null = null -async function getPageInfoFromCache(page: Page, pathname: string) { +export async function getPageInfoFromCache(page: Page, pathname: string) { let cacheInfo = '' if (_cache === null) { try { @@ -111,12 +99,12 @@ async function getPageInfoFromCache(page: Page, pathname: string) { } } - let info = _cache[pathname] + let meta = _cache[pathname] if (!cacheInfo) { - cacheInfo = info ? 'hit' : 'miss' + cacheInfo = meta ? 'hit' : 'miss' } - if (!info) { - info = await getPageInfo(page, pathname) + if (!meta) { + meta = await getPageInfo(page, pathname) // You might wonder; why do we not store this compute information // into the `_cache` from here? // The short answer is; it won't be used again. @@ -128,74 +116,37 @@ async function getPageInfoFromCache(page: Page, pathname: string) { // In CI, we use the caching because the CI runs // `npm run precompute-pageinfo` right before it runs vitest tests. } - info.cacheInfo = cacheInfo - return info + meta.cacheInfo = cacheInfo + return meta } -router.get( - '/v1', - pathValidationMiddleware as RequestHandler, - pageValidationMiddleware as RequestHandler, - catchMiddlewareError(async function pageInfo(req: ExtendedRequestWithPageInfo, res: Response) { - // Remember, the `validationMiddleware` will use redirects if the - // `pathname` used is a redirect (e.g. /en/articles/foo or - // /articles or '/en/enterprise-server@latest/foo/bar) - // So by the time we get here, the pathname should be one of the - // page's valid permalinks. - const { page, pathname, archived } = req.pageinfo +export async function getMetadata(req: ExtendedRequestWithPageInfo) { + // Remember, the `validationMiddleware` will use redirects if the + // `pathname` used is a redirect (e.g. /en/articles/foo or + // /articles or '/en/enterprise-server@latest/foo/bar) + // So by the time we get here, the pathname should be one of the + // page's valid permalinks. + const { page, pathname, archived } = req.pageinfo - if (archived && archived.isArchived) { - const { requestedVersion } = archived - const title = `GitHub Enterprise Server ${requestedVersion} Help Documentation` - const intro = '' - const product = 'GitHub Enterprise Server' - defaultCacheControl(res) - return res.json({ info: { intro, title, product } }) - } + if (archived && archived.isArchived) { + const { requestedVersion } = archived + const title = `GitHub Enterprise Server ${requestedVersion} Help Documentation` + const intro = '' + const product = 'GitHub Enterprise Server' + return { meta: { intro, title, product } } + } - if (!page) { - return res.status(400).json({ error: `No page found for '${pathname}'` }) - } + if (!page) { + throw new Error(`No page found for '${pathname}'`) + } - const pagePermalinks = page.permalinks.map((p: Permalink) => p.href) - if (!pagePermalinks.includes(pathname)) { - throw new Error(`pathname '${pathname}' not one of the page's permalinks`) - } + const pagePermalinks = page.permalinks.map((p: Permalink) => p.href) + if (!pagePermalinks.includes(pathname)) { + throw new Error(`pathname '${pathname}' not one of the page's permalinks`) + } - const fromCache = await getPageInfoFromCache(page, pathname) - const { cacheInfo, ...info } = fromCache + const fromCache = await getPageInfoFromCache(page, pathname) + const { cacheInfo, ...meta } = fromCache - const tags = [ - // According to https://docs.datadoghq.com/getting_started/tagging/#define-tags - // the max length of a tag is 200 characters. Most of ours are less than - // that but we truncate just to be safe. - `pathname:${pathname}`.slice(0, 200), - `language:${page.languageCode}`, - `cache:${cacheInfo}`, - ] - statsd.increment('pageinfo.lookup', 1, tags) - - defaultCacheControl(res) - - // This is necessary so that the `Surrogate-Key` header is set with - // the correct language surrogate key bit. By default, it's set - // from the pathname but `/api/**` URLs don't have a language - // (other than the default 'en'). - // We do this so that all of these URLs are cached in Fastly by language - // which we need for the staggered purge. - - setFastlySurrogateKey( - res, - `${SURROGATE_ENUMS.DEFAULT} ${makeLanguageSurrogateKey(page.languageCode)}`, - true, - ) - res.status(200).json({ info }) - }), -) - -// Alias for the latest version -router.get('/', (req, res) => { - res.redirect(307, req.originalUrl.replace('/pageinfo', '/pageinfo/v1')) -}) - -export default router + return { meta, cacheInfo } +} diff --git a/src/article-api/middleware/article.ts b/src/article-api/middleware/article.ts index 0670466235..642420b737 100644 --- a/src/article-api/middleware/article.ts +++ b/src/article-api/middleware/article.ts @@ -2,87 +2,21 @@ import type { RequestHandler, Response } from 'express' import express from 'express' import { defaultCacheControl } from '@/frame/middleware/cache-control.js' -import { Context } from '#src/types.js' import catchMiddlewareError from '@/observability/middleware/catch-middleware-error.js' import { ExtendedRequestWithPageInfo } from '../types' import { pageValidationMiddleware, pathValidationMiddleware } from './validation' -import contextualize from '#src/frame/middleware/context/context.js' +import { getArticleBody } from './article-body' +import { getMetadata } from './article-pageinfo' +import { + makeLanguageSurrogateKey, + setFastlySurrogateKey, + SURROGATE_ENUMS, +} from '#src/frame/middleware/set-fastly-surrogate-key.js' import statsd from '#src/observability/lib/statsd.js' -/** START helper functions */ - -// for now, we're just querying pageinfo, we'll likely replace /api/pageinfo -// with /api/meta and move or reference that code here -async function getArticleMetadata(req: ExtendedRequestWithPageInfo) { - const host = req.get('x-host') || req.get('x-forwarded-host') || req.get('host') - const queryString = new URLSearchParams(req.query as Record).toString() - const apiUrl = `${req.protocol}://${host}/api/pageinfo${queryString ? `?${queryString}` : ''}` - - // Fetch the data from the pageinfo API - const response = await fetch(apiUrl) - - // Check if the response is OK - if (!response.ok) { - const errorText = await response.text() - throw new Error(`Failed to fetch metadata: ${response.status} ${errorText}`) - } - - return await response.json() -} - -async function getArticleBody(req: ExtendedRequestWithPageInfo) { - // req.pageinfo is set from pageValidationMiddleware and pathValidationMiddleware - // and is in the ExtendedRequestWithPageInfo - const { page, pathname } = req.pageinfo - - // for anything that's not an article (like index pages), don't try to render and - // tell the user what's going on - if (page.documentType !== 'article') { - throw new Error(`Page ${pathname} isn't yet available in markdown.`) - } - // these parts allow us to render the page - const mockedContext: Context = {} - const renderingReq = { - path: pathname, - language: page.languageCode, - pagePath: pathname, - cookies: {}, - context: mockedContext, - headers: { - 'content-type': 'text/markdown', - }, - } - - // contextualize and render the page - await contextualize(renderingReq as ExtendedRequestWithPageInfo, {} as Response, () => {}) - renderingReq.context.page = page - renderingReq.context.markdownRequested = true - return await page.render(renderingReq.context) -} - -function incrementArticleLookup( - pathname: string, - language: string, - type: 'full' | 'body' | 'meta', -) { - const tags = [ - // According to https://docs.datadoghq.com/getting_started/tagging/#define-tags - // the max length of a tag is 200 characters. Most of ours are less than - // that but we truncate just to be safe. - `pathname:${pathname}`.slice(0, 200), - `language:${language}`, - `type:${type}`, - ] - - statsd.increment('api.article.lookup', 1, tags) -} - -/** END helper functions */ - -/** START routes */ const router = express.Router() -// For all these routes: +// For all these routes in `/api/article`: // - pathValidationMiddleware ensures the path is properly structured and handles errors when it's not // - pageValidationMiddleware fetches the page from the pagelist, returns 404 to the user if not found @@ -91,8 +25,7 @@ router.get( pathValidationMiddleware as RequestHandler, pageValidationMiddleware as RequestHandler, catchMiddlewareError(async function (req: ExtendedRequestWithPageInfo, res: Response) { - // First, fetch metadata - const metaData = await getArticleMetadata(req) + const { meta, cacheInfo } = await getMetadata(req) let bodyContent try { bodyContent = await getArticleBody(req) @@ -100,11 +33,11 @@ router.get( return res.status(403).json({ error: (error as Error).message }) } - incrementArticleLookup(req.pageinfo.pathname, req.pageinfo.page.languageCode, 'full') + incrementArticleLookup(req, 'full', cacheInfo) defaultCacheControl(res) return res.json({ - meta: metaData, + meta, body: bodyContent, }) }), @@ -122,7 +55,7 @@ router.get( return res.status(403).json({ error: (error as Error).message }) } - incrementArticleLookup(req.pageinfo.pathname, req.pageinfo.page.languageCode, 'body') + incrementArticleLookup(req, 'body') defaultCacheControl(res) return res.type('text/markdown').send(bodyContent) @@ -133,16 +66,59 @@ router.get( '/meta', pathValidationMiddleware as RequestHandler, pageValidationMiddleware as RequestHandler, - catchMiddlewareError(async function (req: ExtendedRequestWithPageInfo, res: Response) { - const metaData = await getArticleMetadata(req) - - incrementArticleLookup(req.pageinfo.pathname, req.pageinfo.page.languageCode, 'meta') + catchMiddlewareError(async function pageInfo(req: ExtendedRequestWithPageInfo, res: Response) { + const { meta, cacheInfo } = await getMetadata(req) + incrementArticleLookup(req, 'meta', cacheInfo) defaultCacheControl(res) - return res.json(metaData) + + // This is necessary so that the `Surrogate-Key` header is set with + // the correct language surrogate key bit. By default, it's set + // from the pathname but `/api/**` URLs don't have a language + // (other than the default 'en'). + // We do this so that all of these URLs are cached in Fastly by language + // which we need for the staggered purge. + + setFastlySurrogateKey( + res, + `${SURROGATE_ENUMS.DEFAULT} ${makeLanguageSurrogateKey(req.pageinfo?.page?.languageCode || 'en')}`, + true, + ) + return res.json(meta) }), ) -/** END routes */ +// this helps us standardize calls to our datadog agent for article api purposes +function incrementArticleLookup( + req: ExtendedRequestWithPageInfo, + type: 'full' | 'body' | 'meta', + cacheInfo?: string, +) { + const pathname = req.pageinfo.pathname + const language = req.pageinfo.page?.languageCode || 'en' + + // logs the source of the request, if it's for hovercards it'll have the header X-Request-Source. + // see src/links/components/LinkPreviewPopover.tsx + const source = + req.get('X-Request-Source') || + (req.get('Referer') + ? 'external-' + (new URL(req.get('Referer') || '').hostname || 'unknown') + : 'external') + + const tags = [ + // According to https://docs.datadoghq.com/getting_started/tagging/#define-tags + // the max length of a tag is 200 characters. Most of ours are less than + // that but we truncate just to be safe. + `pathname:${pathname}`.slice(0, 200), + `language:${language}`, + `type:${type}`, + `source:${source}`.slice(0, 200), + ] + + // the /article/meta endpoint uses a cache + if (cacheInfo) tags.push(`cache:${cacheInfo}`) + + statsd.increment('api.article.lookup', 1, tags) +} export default router diff --git a/src/article-api/scripts/precompute-pageinfo.ts b/src/article-api/scripts/precompute-pageinfo.ts index 2decd71a02..cd46fbe4ec 100644 --- a/src/article-api/scripts/precompute-pageinfo.ts +++ b/src/article-api/scripts/precompute-pageinfo.ts @@ -34,7 +34,7 @@ import { program, Option } from 'commander' import { languageKeys } from 'src/languages/lib/languages.js' import { loadPages, loadUnversionedTree } from 'src/frame/lib/page-data.js' -import { CACHE_FILE_PATH, getPageInfo } from '../middleware/pageinfo' +import { CACHE_FILE_PATH, getPageInfo } from '../middleware/article-pageinfo' program .description('Generates a JSON file with precompute pageinfo data by pathname') diff --git a/src/article-api/tests/pageinfo.js b/src/article-api/tests/pageinfo.js index abf3fc6e48..a412da2efa 100644 --- a/src/article-api/tests/pageinfo.js +++ b/src/article-api/tests/pageinfo.js @@ -4,7 +4,7 @@ import { get } from '#src/tests/helpers/e2etest.js' import { SURROGATE_ENUMS } from '#src/frame/middleware/set-fastly-surrogate-key.js' import { latest } from '#src/versions/lib/enterprise-server-releases.js' -const makeURL = (pathname) => `/api/pageinfo/v1?${new URLSearchParams({ pathname })}` +const makeURL = (pathname) => `/api/article/meta?${new URLSearchParams({ pathname })}` describe('pageinfo api', () => { beforeAll(() => { @@ -24,19 +24,13 @@ describe('pageinfo api', () => { } }) - test('redirects without version suffix', async () => { - const res = await get('/api/pageinfo') - expect(res.statusCode).toBe(307) - expect(res.headers.location).toBe('/api/pageinfo/v1') - }) - test('happy path', async () => { const res = await get(makeURL('/en/get-started/start-your-journey')) expect(res.statusCode).toBe(200) - const { info } = JSON.parse(res.body) - expect(info.product).toBe('Get started') - expect(info.title).toBe('Start your journey') - expect(info.intro).toBe( + const meta = JSON.parse(res.body) + expect(meta.product).toBe('Get started') + expect(meta.title).toBe('Start your journey') + expect(meta.intro).toBe( 'Get started using HubGit to manage Git repositories and collaborate with others.', ) // Check that it can be cached at the CDN @@ -56,21 +50,21 @@ describe('pageinfo api', () => { }) test("no 'pathname' query string at all", async () => { - const res = await get('/api/pageinfo/v1') + const res = await get('/api/article/meta') expect(res.statusCode).toBe(400) const { error } = JSON.parse(res.body) expect(error).toBe("No 'pathname' query") }) test("empty 'pathname' query string", async () => { - const res = await get('/api/pageinfo/v1?pathname=%20') + const res = await get('/api/article/meta?pathname=%20') expect(res.statusCode).toBe(400) const { error } = JSON.parse(res.body) expect(error).toBe("'pathname' query empty") }) test('repeated pathname query string key', async () => { - const res = await get('/api/pageinfo/v1?pathname=a&pathname=b') + const res = await get('/api/article/meta?pathname=a&pathname=b') expect(res.statusCode).toBe(400) const { error } = JSON.parse(res.body) expect(error).toBe("Multiple 'pathname' keys") @@ -81,29 +75,29 @@ describe('pageinfo api', () => { { const res = await get(makeURL('/en/olden-days')) expect(res.statusCode).toBe(200) - const { info } = JSON.parse(res.body) - expect(info.title).toBe('HubGit.com Fixture Documentation') + const meta = JSON.parse(res.body) + expect(meta.title).toBe('HubGit.com Fixture Documentation') } // Trailing slashes are always removed { const res = await get(makeURL('/en/olden-days/')) expect(res.statusCode).toBe(200) - const { info } = JSON.parse(res.body) - expect(info.title).toBe('HubGit.com Fixture Documentation') + const meta = JSON.parse(res.body) + expect(meta.title).toBe('HubGit.com Fixture Documentation') } // Short code for latest version { const res = await get(makeURL('/en/enterprise-server@latest/get-started/liquid/ifversion')) expect(res.statusCode).toBe(200) - const { info } = JSON.parse(res.body) - expect(info.intro).toMatch(/\(not on fpt\)/) + const meta = JSON.parse(res.body) + expect(meta.intro).toMatch(/\(not on fpt\)/) } // A URL that doesn't have fpt as an available version { const res = await get(makeURL('/en/get-started/versioning/only-ghec-and-ghes')) expect(res.statusCode).toBe(200) - const { info } = JSON.parse(res.body) - expect(info.title).toBe('Only in Enterprise Cloud and Enterprise Server') + const meta = JSON.parse(res.body) + expect(meta.title).toBe('Only in Enterprise Cloud and Enterprise Server') } }) @@ -114,15 +108,15 @@ describe('pageinfo api', () => { { const res = await get(makeURL('/en/get-started/liquid/ifversion')) expect(res.statusCode).toBe(200) - const { info } = JSON.parse(res.body) - expect(info.intro).toMatch(/\(on fpt\)/) + const meta = JSON.parse(res.body) + expect(meta.intro).toMatch(/\(on fpt\)/) } // Second on any other version { const res = await get(makeURL('/en/enterprise-server@latest/get-started/liquid/ifversion')) expect(res.statusCode).toBe(200) - const { info } = JSON.parse(res.body) - expect(info.intro).toMatch(/\(not on fpt\)/) + const meta = JSON.parse(res.body) + expect(meta.intro).toMatch(/\(not on fpt\)/) } }) @@ -131,8 +125,8 @@ describe('pageinfo api', () => { { const res = await get(makeURL('/en')) expect(res.statusCode).toBe(200) - const { info } = JSON.parse(res.body) - expect(info.title).toMatch('HubGit.com Fixture Documentation') + const meta = JSON.parse(res.body) + expect(meta.title).toMatch('HubGit.com Fixture Documentation') } // enterprise-server with language specified // This is important because it tests that we check for a page @@ -143,8 +137,8 @@ describe('pageinfo api', () => { { const res = await get(makeURL(`/en/enterprise-server@${latest}`)) expect(res.statusCode).toBe(200) - const { info } = JSON.parse(res.body) - expect(info.title).toMatch('HubGit Enterprise Server Fixture Documentation') + const meta = JSON.parse(res.body) + expect(meta.title).toMatch('HubGit Enterprise Server Fixture Documentation') } }) @@ -153,15 +147,15 @@ describe('pageinfo api', () => { { const res = await get(makeURL('/')) expect(res.statusCode).toBe(200) - const { info } = JSON.parse(res.body) - expect(info.title).toMatch('HubGit.com Fixture Documentation') + const meta = JSON.parse(res.body) + expect(meta.title).toMatch('HubGit.com Fixture Documentation') } // enterprise-server without language specified { const res = await get(makeURL('/enterprise-server@latest')) expect(res.statusCode).toBe(200) - const { info } = JSON.parse(res.body) - expect(info.title).toMatch('HubGit Enterprise Server Fixture Documentation') + const meta = JSON.parse(res.body) + expect(meta.title).toMatch('HubGit Enterprise Server Fixture Documentation') } }) @@ -175,16 +169,16 @@ describe('pageinfo api', () => { { const res = await get(makeURL('/en/enterprise-server@3.2')) expect(res.statusCode).toBe(200) - const { info } = JSON.parse(res.body) - expect(info.title).toMatch('GitHub Enterprise Server 3.2 Help Documentation') + const meta = JSON.parse(res.body) + expect(meta.title).toMatch('GitHub Enterprise Server 3.2 Help Documentation') } // The oldest known archived version that we proxy { const res = await get(makeURL('/en/enterprise/11.10.340')) expect(res.statusCode).toBe(200) - const { info } = JSON.parse(res.body) - expect(info.title).toMatch('GitHub Enterprise Server 11.10.340 Help Documentation') + const meta = JSON.parse(res.body) + expect(meta.title).toMatch('GitHub Enterprise Server 11.10.340 Help Documentation') } }) @@ -206,10 +200,10 @@ describe('pageinfo api', () => { test('Japanese page', async () => { const res = await get(makeURL('/ja/get-started/start-your-journey/hello-world')) expect(res.statusCode).toBe(200) - const { info } = JSON.parse(res.body) - expect(info.product).toBe('はじめに') - expect(info.title).toBe('こんにちは World') - expect(info.intro).toBe('この Hello World 演習に従って、HubGit の使用を開始します。') + const meta = JSON.parse(res.body) + expect(meta.product).toBe('はじめに') + expect(meta.title).toBe('こんにちは World') + expect(meta.intro).toBe('この Hello World 演習に従って、HubGit の使用を開始します。') }) test('falls back to English if translation is not present', async () => { @@ -223,7 +217,6 @@ describe('pageinfo api', () => { const translation = JSON.parse(translationRes.body) expect(en.title).toBe(translation.title) expect(en.intro).toBe(translation.intro) - expect(en.product).toBe(translation.product) }) }) }) diff --git a/src/frame/middleware/api.ts b/src/frame/middleware/api.ts index 4d3137f408..5fa5f05d22 100644 --- a/src/frame/middleware/api.ts +++ b/src/frame/middleware/api.ts @@ -5,7 +5,6 @@ import events from '@/events/middleware.js' import anchorRedirect from '@/rest/api/anchor-redirect.js' import aiSearch from '@/search/middleware/ai-search' import search from '@/search/middleware/search-routes.js' -import pageInfo from '@/article-api/middleware/pageinfo' import pageList from '@/article-api/middleware/pagelist' import article from '@/article-api/middleware/article' import webhooks from '@/webhooks/middleware/webhooks.js' @@ -29,7 +28,6 @@ if (process.env.NODE_ENV === 'test') { router.use('/events', createAPIRateLimiter(eventsRouteRateLimit), events) router.use('/webhooks', createAPIRateLimiter(internalRoutesRateLimit), webhooks) router.use('/anchor-redirect', createAPIRateLimiter(internalRoutesRateLimit), anchorRedirect) -router.use('/pageinfo', createAPIRateLimiter(3), pageInfo) router.use('/pagelist', createAPIRateLimiter(publicRoutesRateLimit), pageList) router.use('/article', createAPIRateLimiter(publicRoutesRateLimit), article) diff --git a/src/links/components/LinkPreviewPopover.tsx b/src/links/components/LinkPreviewPopover.tsx index 00c0d628f4..67bb9770e4 100644 --- a/src/links/components/LinkPreviewPopover.tsx +++ b/src/links/components/LinkPreviewPopover.tsx @@ -38,14 +38,12 @@ const BOUNDING_TOP_MARGIN = 300 const FIRST_LINK_ID = '_hc_first_focusable' const TITLE_ID = '_hc_title' -type Info = { +type PageMetadata = { product: string title: string intro: string anchor?: string -} -type APIInfo = { - info: Info + cacheInfo?: string } function getOrCreatePopoverGlobal() { @@ -250,17 +248,21 @@ function popoverWrap(element: HTMLLinkElement, filledCallback?: (popover: HTMLDi const { pathname } = new URL(element.href) - fetch(`/api/pageinfo/v1?${new URLSearchParams({ pathname })}`).then(async (response) => { + fetch(`/api/article/meta?${new URLSearchParams({ pathname })}`, { + headers: { + 'X-Request-Source': 'hovercards', + }, + }).then(async (response) => { if (response.ok) { - const { info } = (await response.json()) as APIInfo - fillPopover(element, info, filledCallback) + const meta = (await response.json()) as PageMetadata + fillPopover(element, meta, filledCallback) } }) } function fillPopover( element: HTMLLinkElement, - info: Info, + info: PageMetadata, callback?: (popover: HTMLDivElement) => void, ) { const { product, title, intro, anchor } = info