diff --git a/config/kubernetes/production/deployments/webapp.yaml b/config/kubernetes/production/deployments/webapp.yaml index a0dac38bf5a6..49f8da5cc6b7 100644 --- a/config/kubernetes/production/deployments/webapp.yaml +++ b/config/kubernetes/production/deployments/webapp.yaml @@ -23,11 +23,11 @@ spec: image: docs-internal resources: requests: - cpu: 8000m - memory: 10Gi + cpu: 4000m + memory: 8Gi limits: - cpu: 16000m - memory: 14Gi + cpu: 8000m + memory: 16Gi ports: - name: http containerPort: 4000 diff --git a/content/billing/using-the-new-billing-platform/charging-business-units.md b/content/billing/using-the-new-billing-platform/charging-business-units.md index dfe65cab8924..d76162766467 100644 --- a/content/billing/using-the-new-billing-platform/charging-business-units.md +++ b/content/billing/using-the-new-billing-platform/charging-business-units.md @@ -103,7 +103,7 @@ To ensure your cost centers reflect spending as intended, it's important to unde ### Cost center allocation for {% data variables.product.prodname_GH_advanced_security %} -* If a user belongs to a cost center, all charges associated with the user is billed to the cost center. +* If a user belongs to a cost center, all charges associated with the user are billed to the cost center. * If a user does not belong to any cost center, usage is charged to the enterprise's default payment method and grouped under "Enterprise Only" spending on the usage page. ### Cost center allocation for {% data variables.product.prodname_enterprise %} diff --git a/package-lock.json b/package-lock.json index 021d8bc4e7cf..bac3df34a230 100644 --- a/package-lock.json +++ b/package-lock.json @@ -83,6 +83,7 @@ "remark-parse": "^11.0.0", "remark-rehype": "^11.1.0", "remark-remove-comments": "^1.0.1", + "remark-stringify": "^11.0.0", "rss-parser": "^3.13.0", "scroll-anchoring": "^0.1.0", "semver": "^7.6.2", @@ -13485,6 +13486,7 @@ "version": "11.0.0", "resolved": "https://registry.npmjs.org/remark-stringify/-/remark-stringify-11.0.0.tgz", "integrity": "sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==", + "license": "MIT", "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-to-markdown": "^2.0.0", diff --git a/package.json b/package.json index c01b658e94d8..91e3f1c81bfe 100644 --- a/package.json +++ b/package.json @@ -313,6 +313,7 @@ "remark-parse": "^11.0.0", "remark-rehype": "^11.1.0", "remark-remove-comments": "^1.0.1", + "remark-stringify": "^11.0.0", "rss-parser": "^3.13.0", "scroll-anchoring": "^0.1.0", "semver": "^7.6.2", diff --git a/src/content-render/index.js b/src/content-render/index.js index 2127bdc1fecb..3f3d1eb85ed5 100644 --- a/src/content-render/index.js +++ b/src/content-render/index.js @@ -1,5 +1,5 @@ import { renderLiquid } from './liquid/index.js' -import { renderUnified } from './unified/index.js' +import { renderMarkdown, renderUnified } from './unified/index.js' import { engine } from './liquid/engine.js' const globalCache = new Map() @@ -26,6 +26,12 @@ export async function renderContent(template = '', context = {}, options = {}) { } try { template = await renderLiquid(template, context) + if (context.markdownRequested) { + const md = await renderMarkdown(template, context, options) + + return md + } + const html = await renderUnified(template, context, options) if (cacheKey) { globalCache.set(cacheKey, html) diff --git a/src/content-render/unified/index.js b/src/content-render/unified/index.js index 71b959af0d8b..0895497c0492 100644 --- a/src/content-render/unified/index.js +++ b/src/content-render/unified/index.js @@ -1,5 +1,5 @@ import { fastTextOnly } from './text-only.js' -import { createProcessor } from './processor.js' +import { createProcessor, createMarkdownOnlyProcessor } from './processor.js' export async function renderUnified(template, context, options) { const processor = createProcessor(context) @@ -12,3 +12,11 @@ export async function renderUnified(template, context, options) { return html.trim() } + +export async function renderMarkdown(template, context, options) { + const processor = createMarkdownOnlyProcessor(context) + const vFile = await processor.process(template) + let markdown = vFile.toString() + + return markdown.trim() +} diff --git a/src/content-render/unified/processor.js b/src/content-render/unified/processor.js index df9a1de77cba..e8fae862cd21 100644 --- a/src/content-render/unified/processor.js +++ b/src/content-render/unified/processor.js @@ -29,6 +29,7 @@ import annotate from './annotate.js' import alerts from './alerts.js' import replaceDomain from './replace-domain.js' import removeHtmlComments from 'remark-remove-comments' +import remarkStringify from 'remark-stringify' export function createProcessor(context) { return ( @@ -79,6 +80,10 @@ export function createProcessor(context) { ) } +export function createMarkdownOnlyProcessor(context) { + return unified().use(remarkParse).use(gfm).use(remarkStringify) +} + export function createMinimalProcessor(context) { return unified() .use(remarkParse) diff --git a/src/fixtures/tests/permissions-callout.js b/src/fixtures/tests/permissions-callout.js index bb12d494c9e7..d9e373908d54 100644 --- a/src/fixtures/tests/permissions-callout.js +++ b/src/fixtures/tests/permissions-callout.js @@ -36,7 +36,7 @@ describe('permission statements', () => { }) test('page with permission frontmatter and product statement', async () => { - const $ = await getDOM('/get-started/foo/page-with-permissions-and-product-callout.md') + const $ = await getDOM('/get-started/foo/page-with-permissions-and-product-callout') const html = $('[data-testid=permissions-callout] div').html() // part of the UI expect(html).toMatch('Who can use this feature') diff --git a/src/frame/middleware/context/context.ts b/src/frame/middleware/context/context.ts index b5b3f05ac784..1c9de3e18139 100644 --- a/src/frame/middleware/context/context.ts +++ b/src/frame/middleware/context/context.ts @@ -38,6 +38,14 @@ export default async function contextualize( req.context.process = { env: {} } + if (req.pagePath && req.pagePath.endsWith('.md')) { + req.context.markdownRequested = true + + // req.pagePath is used later in the rendering pipeline to + // locate the file in the tree so it cannot have .md + req.pagePath = req.pagePath.replace(/\/index\.md$/, '').replace(/\.md$/, '') + } + // define each context property explicitly for code-search friendliness // e.g. searches for "req.context.page" will include results from this file req.context.currentLanguage = req.language diff --git a/src/frame/middleware/render-page.ts b/src/frame/middleware/render-page.ts index 7fe6c62cee86..15df6d0d58bd 100644 --- a/src/frame/middleware/render-page.ts +++ b/src/frame/middleware/render-page.ts @@ -190,6 +190,15 @@ export default async function renderPage(req: ExtendedRequest, res: Response) { } } + if (context.markdownRequested) { + if (!page.autogenerated && page.documentType === 'article') { + return res.type('text/markdown').send(req.context.renderedPage) + } else { + const newUrl = req.originalUrl.replace(req.path, req.path.replace(/\.md$/, '')) + return res.redirect(newUrl) + } + } + defaultCacheControl(res) return nextHandleRequest(req, res) diff --git a/src/shielding/middleware/handle-invalid-paths.ts b/src/shielding/middleware/handle-invalid-paths.ts index 52a64602c2a3..fbc1397bb9f4 100644 --- a/src/shielding/middleware/handle-invalid-paths.ts +++ b/src/shielding/middleware/handle-invalid-paths.ts @@ -83,14 +83,11 @@ export default function handleInvalidPaths( return res.status(404).send('Not found') } - if (req.path.endsWith('/index.md') || req.path.endsWith('.md')) { + if (req.path.endsWith('/index.md')) { defaultCacheControl(res) // The originalUrl is the full URL including query string. // E.g. `/en/foo.md?bar=baz` - const newUrl = req.originalUrl.replace( - req.path, - req.path.replace(/\/index\.md$/, '').replace(/\.md$/, ''), - ) + const newUrl = req.originalUrl.replace(req.path, req.path.replace(/\/index\.md$/, '')) return res.redirect(newUrl) } diff --git a/src/shielding/tests/shielding.ts b/src/shielding/tests/shielding.ts index e0e17c27be13..93d0bb882618 100644 --- a/src/shielding/tests/shielding.ts +++ b/src/shielding/tests/shielding.ts @@ -71,7 +71,8 @@ describe('index.md and .md suffixes', () => { } }) - test('any URL that ends with /.md redirects', async () => { + // TODO-ARTICLEAPI: unskip tests or replace when ready to ship article API + test.skip('any URL that ends with /.md redirects', async () => { // With language prefix { const res = await get('/en/get-started/hello.md') diff --git a/src/types.ts b/src/types.ts index 47d00dbd766f..059f9894a579 100644 --- a/src/types.ts +++ b/src/types.ts @@ -165,6 +165,7 @@ export type Context = { currentLearningTrack?: LearningTrack | null renderedPage?: string miniTocItems?: string | undefined + markdownRequested?: boolean } export type LearningTracks = { [group: string]: {