diff --git a/src/routes/_library/$libraryId/$version.docs.$.tsx b/src/routes/_library/$libraryId/$version.docs.$.tsx index 8b75b329b..a8919cbb8 100644 --- a/src/routes/_library/$libraryId/$version.docs.$.tsx +++ b/src/routes/_library/$libraryId/$version.docs.$.tsx @@ -9,6 +9,7 @@ import { import { findLibrary, getBranch, getLibrary } from '~/libraries' import { DocContainer } from '~/components/DocContainer' import { getDocsCacheHeaders } from '~/utils/docs-cache-headers' +import { getDocsEmbedRuntimeHeaders } from '~/utils/docs-embed-headers' import { notFound, redirect, @@ -120,10 +121,16 @@ export const Route = createFileRoute('/_library/$libraryId/$version/docs/$')({ } }, component: Docs, - headers: ({ params }) => { + headers: ({ loaderData, params }) => { const { version, libraryId } = params - return getDocsCacheHeaders({ libraryId, version }) + return { + ...getDocsCacheHeaders({ libraryId, version }), + ...getDocsEmbedRuntimeHeaders({ + content: loaderData?.content ?? '', + version, + }), + } }, }) diff --git a/src/routes/_library/$libraryId/$version.docs.framework.$framework.$.tsx b/src/routes/_library/$libraryId/$version.docs.framework.$framework.$.tsx index 60fde30f1..f028d6fac 100644 --- a/src/routes/_library/$libraryId/$version.docs.framework.$framework.$.tsx +++ b/src/routes/_library/$libraryId/$version.docs.framework.$framework.$.tsx @@ -17,6 +17,7 @@ import { findLibrary, getBranch, getLibrary } from '~/libraries' import { capitalize } from '~/utils/utils' import { DocContainer } from '~/components/DocContainer' import { getDocsCacheHeaders } from '~/utils/docs-cache-headers' +import { getDocsEmbedRuntimeHeaders } from '~/utils/docs-embed-headers' export const Route = createFileRoute( '/_library/$libraryId/$version/docs/framework/$framework/$', @@ -77,10 +78,16 @@ export const Route = createFileRoute( } }, component: Docs, - headers: ({ params }) => { + headers: ({ loaderData, params }) => { const { libraryId, version } = params - return getDocsCacheHeaders({ libraryId, version }) + return { + ...getDocsCacheHeaders({ libraryId, version }), + ...getDocsEmbedRuntimeHeaders({ + content: loaderData?.content ?? '', + version, + }), + } }, head: (ctx) => { const { libraryId, version, framework, _splat: docsPath } = ctx.params diff --git a/src/utils/docs-embed-headers.ts b/src/utils/docs-embed-headers.ts new file mode 100644 index 000000000..a65afdebe --- /dev/null +++ b/src/utils/docs-embed-headers.ts @@ -0,0 +1,46 @@ +import { getClientExampleConfig } from './client-example-config' +import { getExampleRuntimeHeaders } from './stackblitz-embed' + +const clientExampleCommentPattern = //gi +const clientExampleAttributePattern = /([a-z]+)=([a-z0-9-]+)/gi + +/** + * Return WebContainer COOP/COEP headers when the markdown embeds a + * `::client-example` that boots a WebContainer. Docs pages do not set these + * by default. The Examples tab does. + */ +export function getDocsEmbedRuntimeHeaders({ + content, + version, +}: { + content: string + version: string +}) { + for (const match of content.matchAll(clientExampleCommentPattern)) { + const attributes: Record = {} + for (const attribute of (match[1] ?? '').matchAll( + clientExampleAttributePattern, + )) { + const key = attribute[1] + const value = attribute[2] + if (key && value) attributes[key] = value + } + + const library = attributes.library + const framework = attributes.framework + const slug = attributes.slug + if (!library || !framework || !slug) continue + + const config = getClientExampleConfig({ + framework, + libraryId: library, + slug, + version, + }) + if (config?.runtime?.type === 'webcontainer') { + return getExampleRuntimeHeaders('webcontainer') + } + } + + return {} +} diff --git a/tests/docs-embed-headers.test.ts b/tests/docs-embed-headers.test.ts new file mode 100644 index 000000000..fd680afd1 --- /dev/null +++ b/tests/docs-embed-headers.test.ts @@ -0,0 +1,36 @@ +import assert from 'node:assert/strict' +import test from 'node:test' +import { getDocsEmbedRuntimeHeaders } from '../src/utils/docs-embed-headers' +import { webContainerHeaders } from '../src/utils/stackblitz-embed' + +test('docs with an AI client-example get WebContainer isolation headers', () => { + const headers = getDocsEmbedRuntimeHeaders({ + content: [ + '# Basic Chat', + '', + '', + ].join('\n'), + version: 'latest', + }) + + assert.deepEqual(headers, webContainerHeaders) +}) + +test('docs without a client-example get no isolation headers', () => { + const headers = getDocsEmbedRuntimeHeaders({ + content: '# Overview\n\nNo sandbox here.', + version: 'latest', + }) + + assert.deepEqual(headers, {}) +}) + +test('docs with a bad client-example comment get no isolation headers', () => { + const headers = getDocsEmbedRuntimeHeaders({ + content: + '', + version: 'latest', + }) + + assert.deepEqual(headers, {}) +})