diff --git a/.env.example b/.env.example index b1fce0684..69ab884ab 100644 --- a/.env.example +++ b/.env.example @@ -19,6 +19,8 @@ NUXT_V5_PATH= NUXT_EXAMPLES_PATH= # Path to a local clone of github.com/nuxt/cli (shared by all doc versions' command reference) NUXT_CLI_PATH= +# Path to a local clone of github.com/nuxt/icon (docs hosted under /modules/icon) +NUXT_ICON_PATH= # ---------------------------------------------------------------------------- # Content revalidation webhook (optional — only needed to test the GitHub diff --git a/app/components/module/ModuleDocsAside.vue b/app/components/module/ModuleDocsAside.vue new file mode 100644 index 000000000..7181c5e66 --- /dev/null +++ b/app/components/module/ModuleDocsAside.vue @@ -0,0 +1,18 @@ + + + diff --git a/app/components/module/ModuleDocsMobileBar.vue b/app/components/module/ModuleDocsMobileBar.vue new file mode 100644 index 000000000..54d5bf9de --- /dev/null +++ b/app/components/module/ModuleDocsMobileBar.vue @@ -0,0 +1,81 @@ + + + diff --git a/app/components/module/ModuleDocsPage.vue b/app/components/module/ModuleDocsPage.vue new file mode 100644 index 000000000..70b0ff8d2 --- /dev/null +++ b/app/components/module/ModuleDocsPage.vue @@ -0,0 +1,33 @@ + + + diff --git a/app/pages/modules/[slug].vue b/app/pages/modules/[slug].vue deleted file mode 100644 index e390e20b3..000000000 --- a/app/pages/modules/[slug].vue +++ /dev/null @@ -1,320 +0,0 @@ - - - diff --git a/app/pages/modules/[slug]/[...path].vue b/app/pages/modules/[slug]/[...path].vue new file mode 100644 index 000000000..823a162c0 --- /dev/null +++ b/app/pages/modules/[slug]/[...path].vue @@ -0,0 +1,445 @@ + + + diff --git a/nuxt.config.ts b/nuxt.config.ts index 95708eeb9..9765700d9 100644 --- a/nuxt.config.ts +++ b/nuxt.config.ts @@ -2,6 +2,7 @@ import { createResolver } from 'nuxt/kit' import type { LLMsSection } from 'nuxt-llms' import { agentHowToCall, agentWhenToUse } from './shared/utils/agents' import { ARCHIVED_DOC_VERSIONS, CURRENT_DOCS_VERSION, NEXT_DOC_VERSIONS } from './shared/utils/docs' +import { MODULE_DOCS, MODULE_DOCS_NAMES, moduleDocsPathPrefix } from './shared/utils/module-docs' const { resolve } = createResolver(import.meta.url) @@ -612,6 +613,8 @@ export default defineNuxtConfig({ '/blog/**', '/deploy/**', { path: '/modules', raw: '/raw/modules.md' }, + // Hosted module docs: the module page is the docs `index.md`, so it negotiates too. + ...MODULE_DOCS_NAMES.flatMap(name => [moduleDocsPathPrefix(name), `${moduleDocsPathPrefix(name)}/**`]), { path: '/changelog', raw: '/raw/changelog.md' }, { path: '/updates', raw: '/raw/updates.md' } ], @@ -775,6 +778,11 @@ export default defineNuxtConfig({ description: 'Release announcements and deep dives', navigation: '/blog' }, + ...MODULE_DOCS_NAMES.map(name => ({ + title: `${MODULE_DOCS[name].title} Documentation`, + description: 'Documentation of an official Nuxt module', + navigation: moduleDocsPathPrefix(name) + })), { title: 'Optional', links: [ diff --git a/server/api/__sitemap__/urls.get.ts b/server/api/__sitemap__/urls.get.ts index bb8768539..be946c4cf 100644 --- a/server/api/__sitemap__/urls.get.ts +++ b/server/api/__sitemap__/urls.get.ts @@ -1,13 +1,15 @@ -import { cliInstanceKey, docsInstanceKey } from '#shared/utils/content' +import { cliInstanceKey, docsInstanceKey, moduleInstanceKey } from '#shared/utils/content' import { CURRENT_DOCS_VERSION } from '#shared/utils/docs' +import { MODULE_DOCS_NAMES } from '#shared/utils/module-docs' // Sitemap source for @nuxtjs/sitemap (see `sitemap.sources` in nuxt.config). export default defineEventHandler(async () => { - const [docs, cli, examples, siteContent] = await Promise.all([ + const [docs, cli, examples, siteContent, moduleDocs] = await Promise.all([ listInstancePages(docsInstanceKey(CURRENT_DOCS_VERSION)), listInstancePages(cliInstanceKey(CURRENT_DOCS_VERSION)).catch(() => []), listInstancePages('examples').catch(() => []), - getInstanceAtHead('site') + getInstanceAtHead('site'), + Promise.all(MODULE_DOCS_NAMES.map(name => listInstancePages(moduleInstanceKey(name)).catch(() => []))) ]) // The site instance holds markdown pages and the yml-backed landing pages @@ -28,6 +30,7 @@ export default defineEventHandler(async () => { ...docs.map(page => ({ loc: page.path })), ...cli.map(page => ({ loc: page.path })), ...examples.map(page => ({ loc: page.path })), + ...moduleDocs.flat().map(page => ({ loc: page.path })), ...site ] }) diff --git a/server/api/v1/modules/[name].get.ts b/server/api/v1/modules/[name].get.ts index 2d70500a8..635addd9a 100644 --- a/server/api/v1/modules/[name].get.ts +++ b/server/api/v1/modules/[name].get.ts @@ -1,5 +1,6 @@ import { z } from 'zod' import type { Module } from '#shared/types' +import { hasModuleDocs } from '#shared/utils/module-docs' import { resolveReadmeUrls } from '../../../utils/content/readme' export default defineCachedEventHandler(async (event) => { @@ -19,10 +20,12 @@ export default defineCachedEventHandler(async (event) => { // Health served separately (/api/v1/modules/health), merged client-side — // keeps nuxt.care off this page's SSR path. + // A module with hosted docs renders them instead of its README: skip it, it would ship in every docs page payload. + const withReadme = !hasModuleDocs(module.name) const [stats, contributors, readme] = await Promise.all([ fetchModuleStats(event, module), fetchModuleContributors(event, module), - fetchModuleReadme(event, module) + withReadme ? fetchModuleReadme(event, module) : undefined ]) // Who has a Nuxter profile, to link there instead of GitHub. Not blocking: GitHub links on an error. const known = await nuxters.known({ @@ -39,11 +42,11 @@ export default defineCachedEventHandler(async (event) => { generatedAt: new Date().toISOString(), contributors: contributors.map(contributor => ({ ...contributor, nuxter: known.githubIds.has(contributor.id) })), stats, - readme: resolveReadmeUrls(readme, module.repo, stats.defaultBranch) + readme: readme && resolveReadmeUrls(readme, module.repo, stats.defaultBranch) } satisfies Module }, { - // Bumped when the payload changes: `modules:v4` entries have no `providers`. - name: 'modules:v5', + // Bumped when the payload changes: `modules:v5` entries carry the README of modules with docs. + name: 'modules:v6', getKey: event => event.context.params!.name!, swr: true, maxAge: 60 * 60 // 1 hour diff --git a/server/utils/agent-source.ts b/server/utils/agent-source.ts index 1c6101060..a6d920c8c 100644 --- a/server/utils/agent-source.ts +++ b/server/utils/agent-source.ts @@ -1,12 +1,14 @@ import type { ComarkContent, NavigationItem } from 'comark-content' import { createComarkSource } from '#agent-discovery/comark' import { AGENT_DOC_VERSIONS } from '#shared/utils/docs' +import { moduleInstanceKey } from '#shared/utils/content' +import { MODULE_DOCS_NAMES, moduleDocsPathPrefix } from '#shared/utils/module-docs' /** * Content source for nuxt-agent-discovery, bridging the comark instances. * * The module's comark adapter works on one content surface; nuxt.com serves - * eight instances (see `server/utils/content/instances.ts`). This facade + * several instances (see `server/utils/content/instances.ts`). This facade * routes `get()` to the instance owning the path and merges every instance's * navigation for `list()`/`firstLeaf()` (the cli reference grafted into its * docs version through the same `docTree()` the UI navigation uses), so the @@ -23,11 +25,19 @@ function prune(items: NavigationItem[]): NavigationItem[] { const facade = { async navigation() { - const [site, examplesContent, versions] = await Promise.all([ + const [site, examplesContent, versions, moduleDocs] = await Promise.all([ getInstanceAtHead('site').then(content => content.navigation()), getInstanceAtHead('examples').catch(() => null), // Examples are left out of the version trees - Promise.all(AGENT_DOC_VERSIONS.map(version => docTree(version, { withExamples: false }).catch(() => [] as NavigationItem[]))) + Promise.all(AGENT_DOC_VERSIONS.map(version => docTree(version, { withExamples: false }).catch(() => [] as NavigationItem[]))), + // Each module's docs, rooted at its module page + Promise.all(MODULE_DOCS_NAMES.map(name => getInstanceAtHead(moduleInstanceKey(name)) + .then(async (content) => { + const root = findByPath(await content.navigation(), moduleDocsPathPrefix(name)) + // The root already is the module page (`index.md`): drop its duplicate `Overview` child. + return root && { ...root, children: root.children?.filter(item => item.path !== root.path) } + }) + .catch(() => undefined))) ]) const examples = examplesContent ? findByPath(await examplesContent.navigation(), '/docs/examples') : undefined @@ -35,7 +45,8 @@ const facade = { return [ ...prune(site), ...versions.flat(), - ...(examples ? [examples] : []) + ...(examples ? [examples] : []), + ...moduleDocs.filter(item => !!item) ] }, async get(path: string) { diff --git a/server/utils/content/instances.ts b/server/utils/content/instances.ts index 26822c5d0..be13cf2c2 100644 --- a/server/utils/content/instances.ts +++ b/server/utils/content/instances.ts @@ -5,7 +5,8 @@ import type { GithubSource } from 'comark-content/sources/github' import type { FSSourceOptions } from 'comark-content/sources/fs' import { CLI_DOCS_REFS, CLI_DOCS_REPO, CLI_DOCS_PREFIX, cliDocsPathPrefix, hasCliDocs, type CliDocVersion } from '../../../shared/utils/cli' import { DOCS_REFS, DOCS_REPO, docsPathPrefix, isDocVersion, type DocVersion } from '../../../shared/utils/docs' -import { CONTENT_INSTANCE_KEYS, isContentInstanceKey, cliInstanceKey, docsInstanceKey, type ContentInstanceKey } from '../../../shared/utils/content' +import { MODULE_DOCS, hasModuleDocs, moduleDocsPathPrefix, type ModuleDocsName } from '../../../shared/utils/module-docs' +import { CONTENT_INSTANCE_KEYS, isContentInstanceKey, cliInstanceKey, docsInstanceKey, moduleInstanceKey, type ContentInstanceKey } from '../../../shared/utils/content' /** What comark-content's sources own; a required `prefix` is what the page resolver depends on. */ type ComarkSourceOptions = Pick & { @@ -45,7 +46,7 @@ const DOCS_EXCLUDE = ['README.md', '**/*.json'] /** * Per-source branch overrides (testing the push webhook off a non-production branch). */ -const BRANCH_ENV: Record = { +const BRANCH_ENV: Record, string> = { 'site': 'NUXT_COM_BRANCH', 'examples': 'NUXT_EXAMPLES_BRANCH', 'docs:2.x': 'NUXT_V2_BRANCH', @@ -58,7 +59,7 @@ const BRANCH_ENV: Record = { } /** Resolve a branch, honouring env var overrides; log a warning if one is active in production. */ -function branchFor(key: ContentInstanceKey, fallback: string): string { +function branchFor(key: keyof typeof BRANCH_ENV, fallback: string): string { const envVar = BRANCH_ENV[key] const override = process.env[envVar]?.trim() if (override && process.env.VERCEL_ENV === 'production') { @@ -130,7 +131,7 @@ export function instanceSource(key: ContentInstanceKey): { name: string, source: name: 'cli', source: { repo: CLI_DOCS_REPO, - branch: branchFor(key as ContentInstanceKey, CLI_DOCS_REFS[version].branch), + branch: branchFor(key as `cli:${CliDocVersion}`, CLI_DOCS_REFS[version].branch), contentDir: 'docs', prefix: cliDocsPathPrefix(version), listingFields: DOCS_LISTING_FIELDS, @@ -140,6 +141,26 @@ export function instanceSource(key: ContentInstanceKey): { name: string, source: } } + if (key.startsWith('module:')) { + const name = key.slice('module:'.length) + if (!hasModuleDocs(name)) { + throw createError({ statusCode: 404, statusMessage: `Unknown content instance: ${key}` }) + } + const { repo, branch, contentDir, envOverride } = MODULE_DOCS[name] + return { + name: 'module', + source: { + repo, + branch, + contentDir, + prefix: moduleDocsPathPrefix(name), + listingFields: DOCS_LISTING_FIELDS, + exclude: DOCS_EXCLUDE, + envOverride + } + } + } + const version = key.slice('docs:'.length) if (!isDocVersion(version)) { throw createError({ statusCode: 404, statusMessage: `Unknown content instance: ${key}` }) @@ -148,7 +169,7 @@ export function instanceSource(key: ContentInstanceKey): { name: string, source: name: 'docs', source: { repo: DOCS_REPO, - branch: branchFor(key as ContentInstanceKey, DOCS_REFS[version].branch), + branch: branchFor(key as `docs:${DocVersion}`, DOCS_REFS[version].branch), contentDir: 'docs', prefix: docsPathPrefix(version), listingFields: DOCS_LISTING_FIELDS, @@ -188,6 +209,7 @@ export function instanceFromPagePath(path: string): ContentInstanceKey { * - `examples/…` (the examples instance, code examples) * - `docs//…` (one instance per docs version) * - `cli//…` (one instance per docs version's command reference) + * - `module//…` (one instance per module with hosted docs) * * Shared by the live, `head/` and `blob//` routes, which must agree on where an instance's * prefix begins — they are the same path with different qualifiers in front. Unlike @@ -200,6 +222,7 @@ export function instanceKeyFromSegments(segments: string[]): ContentInstanceKey let key: string if (first === 'docs') key = docsInstanceKey(version) else if (first === 'cli') key = cliInstanceKey(version as CliDocVersion) + else if (first === 'module') key = moduleInstanceKey(second as ModuleDocsName) else key = first ?? '' if (isContentInstanceKey(key)) return key diff --git a/server/utils/content/plugins/index.ts b/server/utils/content/plugins/index.ts index 8fc6f5611..f35669859 100644 --- a/server/utils/content/plugins/index.ts +++ b/server/utils/content/plugins/index.ts @@ -7,10 +7,11 @@ import type { ContentPlugin } from 'comark-content' import { highlightPlugin } from './highlight' import { dottedFrontmatter } from './dotted-frontmatter' import { docsLinks } from './docs-links' -import { cliDocs } from './cli-docs' +import { repoImages } from './repo-images' import { configDocs } from './config-docs' import { sanitizeData } from './sanitize-data' -import type { CliDocVersion } from '../../../../shared/utils/cli' +import { CLI_DOCS_REFS, CLI_DOCS_REPO, type CliDocVersion } from '../../../../shared/utils/cli' +import { MODULE_DOCS, type ModuleDocsName } from '../../../../shared/utils/module-docs' import type { DocVersion } from '../../../../shared/utils/docs' import type { ContentInstanceKey } from '../../../../shared/utils/content' @@ -46,7 +47,11 @@ export function instancePlugins(key: ContentInstanceKey): ContentPlugin[] { if (key.startsWith('cli:')) { const version = key.slice('cli:'.length) as CliDocVersion // Command pages are written with the same unversioned `/docs/*` links as the docs. - return [dottedFrontmatter(), docsLinks(version), cliDocs(version)] + return [dottedFrontmatter(), docsLinks(version), repoImages(CLI_DOCS_REPO, CLI_DOCS_REFS[version].branch)] + } + if (key.startsWith('module:')) { + const { repo, branch } = MODULE_DOCS[key.slice('module:'.length) as ModuleDocsName] + return [dottedFrontmatter(), repoImages(repo, branch)] } const version = key.slice('docs:'.length) as DocVersion diff --git a/server/utils/content/plugins/cli-docs.ts b/server/utils/content/plugins/repo-images.ts similarity index 68% rename from server/utils/content/plugins/cli-docs.ts rename to server/utils/content/plugins/repo-images.ts index 6062db92d..599b53322 100644 --- a/server/utils/content/plugins/cli-docs.ts +++ b/server/utils/content/plugins/repo-images.ts @@ -1,18 +1,18 @@ import type { Node } from 'comark' import { visit } from 'comark/utils' import type { ContentPlugin } from 'comark-content' -import { CLI_DOCS_REFS, CLI_DOCS_REPO, type CliDocVersion } from '../../../../shared/utils/cli' const isImage = (node: Node): boolean => Array.isArray(node) && node[0] === 'img' /** - * Resolve the `nuxt/cli` command reference's image sources. + * Resolve root-relative image sources against the repository a source reads + * (the `nuxt/cli` command reference, a module's docs). */ -export function cliDocs(version: CliDocVersion): ContentPlugin { +export function repoImages(repo: string, branch: string): ContentPlugin { return { - name: 'nuxt-cli-docs', + name: 'repo-images', setup(content) { - const rawBase = `https://raw.githubusercontent.com/${CLI_DOCS_REPO}/${CLI_DOCS_REFS[version].branch}` + const rawBase = `https://raw.githubusercontent.com/${repo}/${branch}` content.hooks.hook('file:parsed', ({ file }) => { if (!file?.nodes?.length) return diff --git a/shared/utils/content.ts b/shared/utils/content.ts index 50acaa08b..378bcad02 100644 --- a/shared/utils/content.ts +++ b/shared/utils/content.ts @@ -1,5 +1,6 @@ import { CLI_DOC_VERSIONS, hasCliDocs, type CliDocVersion } from './cli' import { DOC_VERSIONS, hasExamples, isDocVersion, type DocVersion } from './docs' +import { MODULE_DOCS_NAMES, hasModuleDocs, type ModuleDocsName } from './module-docs' /** * Identifies one of the content instances nuxt.com reads from. @@ -9,8 +10,9 @@ import { DOC_VERSIONS, hasExamples, isDocVersion, type DocVersion } from './docs * - `cli:` the command reference backing that version, from `nuxt/cli` — mounted * under the docs URL tree (`/docs//api/commands`) but its own repo, so its own instance * - `examples` the `nuxt/examples` showcase + * - `module:` a module's docs, from its own repo — mounted under its module page (`/modules/`) */ -export type ContentInstanceKey = 'site' | 'examples' | `docs:${DocVersion}` | `cli:${CliDocVersion}` +export type ContentInstanceKey = 'site' | 'examples' | `docs:${DocVersion}` | `cli:${CliDocVersion}` | `module:${ModuleDocsName}` /** The docs instance serving `version`, e.g. `docsInstanceKey('4.x') === 'docs:4.x'`. */ export function docsInstanceKey(version: DocVersion): ContentInstanceKey { @@ -22,12 +24,18 @@ export function cliInstanceKey(version: CliDocVersion): ContentInstanceKey { return `cli:${version}` } +/** The instance serving a module's docs, e.g. `moduleInstanceKey('icon') === 'module:icon'`. */ +export function moduleInstanceKey(name: ModuleDocsName): ContentInstanceKey { + return `module:${name}` +} + /** Every instance nuxt.com serves, derived so a new docs version cannot be missed. */ export const CONTENT_INSTANCE_KEYS: ContentInstanceKey[] = [ 'site', 'examples', ...DOC_VERSIONS.map(docsInstanceKey), - ...CLI_DOC_VERSIONS.map(cliInstanceKey) + ...CLI_DOC_VERSIONS.map(cliInstanceKey), + ...MODULE_DOCS_NAMES.map(moduleInstanceKey) ] /** @@ -53,6 +61,7 @@ export const SEARCH_INDEXED_KEYS: Set = new Set(DOC_VERSIONS export function isContentInstanceKey(value: string): value is ContentInstanceKey { if (value === 'site' || value === 'examples') return true if (value.startsWith('docs:')) return isDocVersion(value.slice('docs:'.length)) + if (value.startsWith('module:')) return hasModuleDocs(value.slice('module:'.length)) return value.startsWith('cli:') && hasCliDocs(value.slice('cli:'.length)) } @@ -96,6 +105,8 @@ export function navigationPath(version: DocVersion): string { */ export function navPathsForInstance(key: ContentInstanceKey): string[] { if (key === 'site' || key === 'examples') return DOC_VERSIONS.map(navigationPath) + // A module page reads its own instance's navigation, not a docs version's tree. + if (key.startsWith('module:')) return [] return [navigationPath(key.split(':')[1] as DocVersion)] } diff --git a/shared/utils/module-docs.ts b/shared/utils/module-docs.ts new file mode 100644 index 000000000..c846219b2 --- /dev/null +++ b/shared/utils/module-docs.ts @@ -0,0 +1,33 @@ +/** + * Modules whose documentation nuxt.com hosts under their module page, e.g. `/modules/icon/configuration`. + * + * Keyed by the module's `name` in the `nuxt/modules` registry. A module without an entry keeps + * the README-only page: the docs navigation only exists when a source is configured here. + * The source's `index.md` replaces the README on the module page, so it must have one. + */ +export const MODULE_DOCS = { + // TODO: back to `main` once nuxt/icon's `docs` branch is merged. + icon: { title: 'Nuxt Icon', repo: 'nuxt/icon', branch: 'docs', contentDir: 'docs', envOverride: 'NUXT_ICON_PATH' } +} as const satisfies Record + +/** Modules with hosted docs. */ +export type ModuleDocsName = keyof typeof MODULE_DOCS + +export const MODULE_DOCS_NAMES = Object.keys(MODULE_DOCS) as ModuleDocsName[] + +/** Whether a module has hosted docs. */ +export function hasModuleDocs(name: string): name is ModuleDocsName { + return Object.hasOwn(MODULE_DOCS, name) +} + +/** Where a module's docs are mounted — its module page. */ +export function moduleDocsPathPrefix(name: ModuleDocsName): string { + return `/modules/${name}` +} diff --git a/shared/utils/pull.ts b/shared/utils/pull.ts index f757f76e1..024222e4f 100644 --- a/shared/utils/pull.ts +++ b/shared/utils/pull.ts @@ -71,12 +71,14 @@ export function pullNavigationPath(version: DocVersion, pull: PullTarget): strin } /** - * The `nuxt` org repo `key` reads from, as `/pull/:repo` names it. + * The `nuxt` org repo `key` reads from, as `/pull/:repo` names it — `undefined` for module docs, + * which have no previews. * Pinned to `instanceSource(key).source.repo` by `test/unit/pull-paths.spec.ts`. */ -export function instanceRepo(key: ContentInstanceKey): PullRepo { +export function instanceRepo(key: ContentInstanceKey): PullRepo | undefined { if (key === 'site') return 'nuxt.com' if (key === 'examples') return 'examples' + if (key.startsWith('module:')) return undefined return key.startsWith('cli:') ? 'cli' : 'nuxt' } diff --git a/test/unit/pull-paths.spec.ts b/test/unit/pull-paths.spec.ts index e77bf6189..3ad670921 100644 --- a/test/unit/pull-paths.spec.ts +++ b/test/unit/pull-paths.spec.ts @@ -75,7 +75,8 @@ describe('pull paths', () => { describe('instanceRepo', () => { it('agrees with `instanceSource(key).source.repo` for every instance', () => { for (const key of CONTENT_INSTANCE_KEYS) { - expect(pullRepoName(instanceRepo(key)), key).toBe(instanceSource(key).source.repo) + const repo = instanceRepo(key) + if (repo) expect(pullRepoName(repo), key).toBe(instanceSource(key).source.repo) } }) }) @@ -99,6 +100,9 @@ describe('isPullContentPage', () => { /** Docs tree pages rendering docs data without `useContent()`. */ const DOCS_PAGES_WITHOUT_CONTENT = new Set(['app/pages/docs/[version]/errors/index.vue']) +/** Pages calling `useContent()` only for module docs, which have no previews. */ +const PAGES_WITHOUT_PREVIEWABLE_CONTENT = new Set(['app/pages/modules/[slug]/[...path].vue']) + /** A URL a page file serves: `blog/[slug].vue` → `/blog/x`, `docs/[...slug].vue` → `/docs/x/y`. */ function samplePath(file: string): string { return file @@ -117,7 +121,8 @@ describe('PULL_CONTENT_PAGES', () => { .map(file => `${dir}/${file}`)) it.each(pages)('previews %s only if it renders content', (file) => { - const rendersContent = DOCS_PAGES_WITHOUT_CONTENT.has(file) || readFileSync(join(root, file), 'utf8').includes('useContent(') + const rendersContent = DOCS_PAGES_WITHOUT_CONTENT.has(file) + || (!PAGES_WITHOUT_PREVIEWABLE_CONTENT.has(file) && readFileSync(join(root, file), 'utf8').includes('useContent(')) expect(isPullContentPage(samplePath(file))).toBe(rendersContent) })