>>()
+
+ const getCached = async (url: string) => {
+ if (!responseCache.has(url)) {
+ responseCache.set(url, await get(makeURL(url)))
+ }
+ return responseCache.get(url)!
+ }
+
+ beforeAll(() => {
+ if (!process.env.ROOT) {
+ console.warn(
+ 'WARNING: The GraphQL transformer tests require the ROOT environment variable to be set to the fixture root',
+ )
+ }
+ })
+
+ describe('Reference pages', () => {
+ test('queries page renders with markdown structure', async () => {
+ const res = await getCached('/en/graphql/reference/queries')
+ expect(res.statusCode).toBe(200)
+ expect(res.headers['content-type']).toContain('text/markdown')
+
+ // Check for the main heading
+ expect(res.body).toContain('# Queries')
+
+ // Check for intro
+ expect(res.body).toContain(
+ 'The query type defines GraphQL operations that retrieve data from the server.',
+ )
+
+ // Check for manual content section
+ expect(res.body).toContain('## About queries')
+ expect(res.body).toContain('Every GraphQL schema has a root type')
+ })
+
+ test('queries are formatted correctly', async () => {
+ const res = await getCached('/en/graphql/reference/queries')
+ expect(res.statusCode).toBe(200)
+
+ // Check for query heading
+ expect(res.body).toContain('## repository')
+
+ // Check for query description
+ expect(res.body).toContain('Lookup a given repository by the owner and repository name.')
+
+ // Check for type link
+ expect(res.body).toContain('**Type:** [Repository](/en/graphql/reference/objects#repository)')
+ })
+
+ test('query arguments are listed in table format', async () => {
+ const res = await getCached('/en/graphql/reference/queries')
+ expect(res.statusCode).toBe(200)
+
+ // Check for arguments table for codeOfConduct query
+ expect(res.body).toContain('### Arguments for `codeOfConduct`')
+ expect(res.body).toMatch(/\|\s*Name\s*\|\s*Type\s*\|\s*Description\s*\|/)
+ expect(res.body).toMatch(/\|\s*-+\s*\|\s*-+\s*\|\s*-+\s*\|/)
+
+ // Check for specific arguments
+ expect(res.body).toMatch(/\|\s*`key`\s*\|/)
+ expect(res.body).toContain('[`String!`](/en/graphql/reference/scalars#string)')
+ expect(res.body).toContain("The code of conduct's key.")
+ })
+
+ test('mutations page renders correctly', async () => {
+ const res = await getCached('/en/graphql/reference/mutations')
+ expect(res.statusCode).toBe(200)
+
+ // Check for mutation heading
+ expect(res.body).toContain('## createRepository')
+
+ // Check for mutation description
+ expect(res.body).toContain('Create a new repository.')
+
+ // Check for input fields table
+ expect(res.body).toContain('### Input fields for `createRepository`')
+ expect(res.body).toContain('| `input` |')
+
+ // Check for return fields table
+ expect(res.body).toContain('### Return fields for `createRepository`')
+ expect(res.body).toMatch(/\|\s*`repository`\s*\|/)
+ expect(res.body).toContain('The new repository.')
+ })
+
+ test('objects page renders with implements and fields', async () => {
+ const res = await getCached('/en/graphql/reference/objects')
+ expect(res.statusCode).toBe(200)
+
+ // Check for object heading - AddedToMergeQueueEvent has implements
+ expect(res.body).toContain('## AddedToMergeQueueEvent')
+
+ // Check for implements section
+ expect(res.body).toContain('### Implements')
+ expect(res.body).toMatch(/[*-]\s*\[`Node`\]\(\/.*graphql\/reference\/interfaces#node\)/)
+
+ // Check for fields table
+ expect(res.body).toContain('### Fields for `AddedToMergeQueueEvent`')
+ expect(res.body).toMatch(/\|\s*`id`\s*\|/)
+ expect(res.body).toMatch(/\|\s*`actor`\s*\|/)
+ expect(res.body).toMatch(/\|\s*`createdAt`\s*\|/)
+ })
+
+ test('objects page shows field arguments inline', async () => {
+ const res = await getCached('/en/graphql/reference/objects')
+ expect(res.statusCode).toBe(200)
+
+ // Check for User object with repositories field that has arguments
+ expect(res.body).toContain('## User')
+ expect(res.body).toContain('| `repositories` |')
+
+ // Check for inline arguments formatting
+ expect(res.body).toContain('**Arguments:**')
+ expect(res.body).toContain('- `first`')
+ expect(res.body).toContain('Returns the first n elements from the list.')
+ expect(res.body).toContain('- `orderBy`')
+ })
+
+ test('interfaces page renders correctly', async () => {
+ const res = await getCached('/en/graphql/reference/interfaces')
+ expect(res.statusCode).toBe(200)
+
+ // Check for interface heading
+ expect(res.body).toContain('## Node')
+
+ // Check for interface description
+ expect(res.body).toContain('An object with an ID.')
+
+ // Check for fields table
+ expect(res.body).toContain('### Fields for `Node`')
+ expect(res.body).toContain('| `id` |')
+ expect(res.body).toContain('ID of the object.')
+ })
+
+ test('enums page renders with values', async () => {
+ const res = await getCached('/en/graphql/reference/enums')
+ expect(res.statusCode).toBe(200)
+
+ // Check for enum heading
+ expect(res.body).toContain('## RepositoryVisibility')
+
+ // Check for enum description
+ expect(res.body).toContain("The repository's visibility level.")
+
+ // Check for values section
+ expect(res.body).toContain('### Values for `RepositoryVisibility`')
+ expect(res.body).toContain('**`PUBLIC`**')
+ expect(res.body).toContain('The repository is visible to everyone.')
+ expect(res.body).toContain('**`PRIVATE`**')
+ expect(res.body).toContain('The repository is visible only to those with explicit access.')
+ expect(res.body).toContain('**`INTERNAL`**')
+ })
+
+ test('unions page renders with possible types', async () => {
+ const res = await getCached('/en/graphql/reference/unions')
+ expect(res.statusCode).toBe(200)
+
+ // Check for union heading
+ expect(res.body).toContain('## SearchResultItem')
+
+ // Check for union description
+ expect(res.body).toContain('The results of a search.')
+
+ // Check for possible types
+ expect(res.body).toContain('### Possible types for `SearchResultItem`')
+ expect(res.body).toMatch(/[*-]\s*\[`Bot`\]\(\/.*graphql\/reference\/objects#bot\)/)
+ expect(res.body).toMatch(
+ /[*-]\s*\[`PullRequest`\]\(\/.*graphql\/reference\/objects#pullrequest\)/,
+ )
+ expect(res.body).toMatch(/[*-]\s*\[`User`\]\(\/.*graphql\/reference\/objects#user\)/)
+ })
+
+ test('input-objects page renders correctly', async () => {
+ const res = await getCached('/en/graphql/reference/input-objects')
+ expect(res.statusCode).toBe(200)
+
+ // Check for input object heading
+ expect(res.body).toContain('## AbortQueuedMigrationsInput')
+
+ // Check for input object description
+ expect(res.body).toContain('Autogenerated input type of CreateRepository.')
+
+ // Check for input fields table
+ expect(res.body).toContain('### Input fields for `AbortQueuedMigrationsInput`')
+ expect(res.body).toMatch(/\|\s*`ownerId`\s*\|/)
+ expect(res.body).toContain('The ID of the organization that is running the migrations.')
+ })
+
+ test('scalars page renders correctly', async () => {
+ const res = await getCached('/en/graphql/reference/scalars')
+ expect(res.statusCode).toBe(200)
+
+ // Check for scalar heading
+ expect(res.body).toContain('## Boolean')
+
+ // Check for scalar description
+ expect(res.body).toContain('Represents true or false values.')
+
+ // Check for other scalars
+ expect(res.body).toContain('## String')
+ expect(res.body).toContain('## ID')
+ expect(res.body).toContain('## Int')
+ })
+
+ test('reference index page renders', async () => {
+ const res = await getCached('/en/graphql/reference')
+ expect(res.statusCode).toBe(200)
+
+ // Check for main heading
+ expect(res.body).toContain('# Reference')
+
+ // Check for intro with liquid variable rendered
+ expect(res.body).toMatch(/(GitHub|HubGit) GraphQL API schema/)
+ })
+ })
+
+ describe('Overview pages', () => {
+ test('changelog page renders with changes', async () => {
+ const res = await getCached('/en/graphql/overview/changelog')
+ expect(res.statusCode).toBe(200)
+
+ // Check for main heading
+ expect(res.body).toContain('# Changelog')
+
+ // Check for intro
+ expect(res.body).toContain(
+ 'The GraphQL schema changelog is a list of recent and upcoming changes',
+ )
+
+ // Check for manual content
+ expect(res.body).toContain(
+ 'Breaking changes include changes that will break existing queries',
+ )
+
+ // Check for date-based changelog sections
+ expect(res.body).toContain('## Schema changes for 2025-11-30')
+
+ // Check for change items
+ expect(res.body).toContain('### The GraphQL schema includes these changes:')
+ expect(res.body).toContain('Type SuggestedReviewerActor was added')
+ })
+
+ test('changelog removes HTML tags from changes', async () => {
+ const res = await getCached('/en/graphql/overview/changelog')
+ expect(res.statusCode).toBe(200)
+
+ // Check that HTML tags are removed
+ expect(res.body).toContain('Field suggestedReviewerActors was added')
+ expect(res.body).not.toContain('')
+ expect(res.body).not.toContain('')
+ expect(res.body).not.toContain('')
+ expect(res.body).not.toContain('
')
+ })
+
+ test('breaking changes page renders with scheduled changes', async () => {
+ const res = await getCached('/en/graphql/overview/breaking-changes')
+ expect(res.statusCode).toBe(200)
+
+ // Check for main heading
+ expect(res.body).toContain('# Breaking changes')
+
+ // Check for intro
+ expect(res.body).toContain('Learn about recent and upcoming breaking changes')
+
+ // Check for manual content
+ expect(res.body).toContain('## About breaking changes')
+ expect(res.body).toContain('Breaking:** Changes that will break existing queries')
+
+ // Check for date-based sections
+ expect(res.body).toContain('## Changes scheduled for 2025-04-01')
+ expect(res.body).toContain('## Changes scheduled for 2026-04-01')
+ })
+
+ test('breaking changes shows criticality levels', async () => {
+ const res = await getCached('/en/graphql/overview/breaking-changes')
+ expect(res.statusCode).toBe(200)
+
+ // Check for breaking criticality
+ expect(res.body).toMatch(/\*\*Breaking\*\*\s+A change will be made to `\w+\.\w+`\./)
+ expect(res.body).toMatch(/\*\*Description:\*\*.*will be removed/)
+ expect(res.body).toMatch(/\*\*Reason:\*\*/)
+ })
+
+ test('breaking changes removes HTML tags', async () => {
+ const res = await getCached('/en/graphql/overview/breaking-changes')
+ expect(res.statusCode).toBe(200)
+ expect(res.body).toContain('scheduled for')
+
+ // Check that HTML tags are removed from descriptions
+ expect(res.body).not.toContain('')
+ expect(res.body).not.toContain('
')
+ expect(res.body).not.toContain('')
+ expect(res.body).not.toContain('')
+ expect(res.body).not.toContain('')
+ expect(res.body).not.toContain('
')
+ })
+ })
+
+ describe('Liquid tags', () => {
+ test('AUTOTITLE links are resolved in manual content', async () => {
+ const res = await getCached('/en/graphql/reference/queries')
+ expect(res.statusCode).toBe(200)
+
+ // Check that AUTOTITLE has been resolved
+ expect(res.body).toMatch(/(Forming calls with GraphQL|Hello World)/)
+ expect(res.body).toContain('(/en/get-started/start-your-journey/hello-world)')
+
+ // Make sure the raw AUTOTITLE tag is not present
+ expect(res.body).not.toContain('[AUTOTITLE]')
+ })
+
+ test('Liquid variables are rendered in intro', async () => {
+ const res = await getCached('/en/graphql/reference')
+ expect(res.statusCode).toBe(200)
+
+ // Liquid variables should be rendered
+ expect(res.body).toMatch(/(GitHub|HubGit) GraphQL API schema/)
+ expect(res.body).not.toContain('{% data variables.product.prodname_dotcom %}')
+ })
+
+ test('Liquid variables are rendered in breaking changes', async () => {
+ const res = await getCached('/en/graphql/overview/breaking-changes')
+ expect(res.statusCode).toBe(200)
+
+ // Check that liquid variables in intro are rendered
+ expect(res.body).toMatch(/(GitHub|HubGit) GraphQL API/)
+ expect(res.body).not.toContain('{% data variables.product.prodname_dotcom %}')
+ })
+ })
+
+ describe('Multiple items', () => {
+ test('multiple queries are all rendered', async () => {
+ const res = await getCached('/en/graphql/reference/queries')
+ expect(res.statusCode).toBe(200)
+
+ // Check for multiple query headings
+ expect(res.body).toContain('## repository')
+ expect(res.body).toContain('## viewer')
+ })
+
+ test('multiple objects are all rendered', async () => {
+ const res = await getCached('/en/graphql/reference/objects')
+ expect(res.statusCode).toBe(200)
+
+ // Check for multiple object headings
+ expect(res.body).toContain('## Repository')
+ expect(res.body).toContain('## User')
+ })
+
+ test('multiple enums are all rendered', async () => {
+ const res = await getCached('/en/graphql/reference/enums')
+ expect(res.statusCode).toBe(200)
+
+ // Check for multiple enum headings
+ expect(res.body).toContain('## RepositoryVisibility')
+ expect(res.body).toContain('## OrderDirection')
+ })
+ })
+})
diff --git a/src/article-api/transformers/graphql-transformer.ts b/src/article-api/transformers/graphql-transformer.ts
new file mode 100644
index 0000000000..12a2c43a04
--- /dev/null
+++ b/src/article-api/transformers/graphql-transformer.ts
@@ -0,0 +1,478 @@
+import type { Context, Page } from '@/types'
+import type { PageTransformer } from './types'
+import type {
+ QueryT,
+ MutationT,
+ ObjectT,
+ InterfaceT,
+ EnumT,
+ UnionT,
+ InputObjectT,
+ ScalarT,
+ ChangelogItemT,
+ BreakingChangesT,
+ FieldT,
+} from '@/graphql/components/types'
+import { renderContent } from '@/content-render/index'
+import matter from '@gr2m/gray-matter'
+import { readFileSync } from 'fs'
+import { join, dirname } from 'path'
+import { fileURLToPath } from 'url'
+import { fastTextOnly } from '@/content-render/unified/text-only'
+import GithubSlugger from 'github-slugger'
+
+const __filename = fileURLToPath(import.meta.url)
+const __dirname = dirname(__filename)
+
+/**
+ * Transformer for GraphQL pages
+ * Converts GraphQL schema data into markdown format using Liquid templates
+ */
+export class GraphQLTransformer implements PageTransformer {
+ canTransform(page: Page): boolean {
+ return page.autogenerated === 'graphql'
+ }
+
+ async transform(page: Page, pathname: string, context: Context): Promise {
+ const currentVersion = context.currentVersion!
+
+ // Determine the page type from the pathname
+ const pathParts = pathname.split('/').filter(Boolean)
+ const graphqlIndex = pathParts.indexOf('graphql')
+
+ if (graphqlIndex === -1) {
+ throw new Error(`Invalid GraphQL path: ${pathname}`)
+ }
+
+ const section = pathParts[graphqlIndex + 1] // 'reference' or 'overview'
+ const pageType = pathParts[graphqlIndex + 2] // specific page like 'queries', 'changelog', etc.
+
+ // Handle different GraphQL page types
+ if (section === 'overview' && pageType === 'changelog') {
+ return await this.transformChangelog(page, currentVersion, context)
+ } else if (section === 'overview' && pageType === 'breaking-changes') {
+ return await this.transformBreakingChanges(page, currentVersion, context)
+ } else if (section === 'reference' && pageType) {
+ return await this.transformReference(page, currentVersion, context, pageType)
+ } else if (section === 'reference' && !pageType) {
+ // Index page - just render the intro and manual content
+ return await this.transformIndexPage(page, context)
+ }
+
+ throw new Error(`Unsupported GraphQL page type: ${pathname}`)
+ }
+
+ /**
+ * Transform the GraphQL reference index page
+ */
+ private async transformIndexPage(page: Page, context: Context): Promise {
+ const intro = page.intro ? await page.renderProp('intro', context, { textOnly: true }) : ''
+
+ const manualContent = await this.extractManualContent(page, context)
+
+ // Get children links from page metadata
+ const children = page.children || []
+ const childrenLinks = children
+ .map((child) => {
+ const childPath = child.startsWith('/') ? child : `/${child}`
+ const childName = childPath.split('/').pop() || ''
+ const displayName = childName
+ .split('-')
+ .map((word) => word.charAt(0).toUpperCase() + word.slice(1))
+ .join(' ')
+ return `- [${displayName}](${childPath})`
+ })
+ .join('\n')
+
+ const templateData = {
+ pageTitle: page.title,
+ pageIntro: intro,
+ manualContent,
+ childrenLinks,
+ }
+
+ const templatePath = join(__dirname, '../templates/graphql-index.template.md')
+ const templateContent = readFileSync(templatePath, 'utf8')
+
+ return await renderContent(templateContent, {
+ ...context,
+ ...templateData,
+ markdownRequested: true,
+ })
+ }
+
+ /**
+ * Transform GraphQL reference pages (queries, mutations, objects, etc.)
+ */
+ private async transformReference(
+ page: Page,
+ currentVersion: string,
+ context: Context,
+ pageType: string,
+ ): Promise {
+ // Import GraphQL data functions dynamically
+ const { getGraphqlSchema } = await import('@/graphql/lib/index')
+
+ // Map URL-friendly page type to internal schema key
+ const schemaKey = pageType === 'input-objects' ? 'inputObjects' : pageType
+
+ const schema = getGraphqlSchema(currentVersion, schemaKey)
+
+ // Prepare intro and manual content
+ const intro = page.intro ? await page.renderProp('intro', context, { textOnly: true }) : ''
+ const manualContent = await this.extractManualContent(page, context)
+
+ // Prepare the schema items based on page type
+ let preparedItems: Array> = []
+
+ switch (schemaKey) {
+ case 'queries':
+ preparedItems = await Promise.all(
+ (schema as QueryT[]).map((item) => this.prepareQuery(item)),
+ )
+ break
+ case 'mutations':
+ preparedItems = await Promise.all(
+ (schema as MutationT[]).map((item) => this.prepareMutation(item)),
+ )
+ break
+ case 'objects':
+ preparedItems = await Promise.all(
+ (schema as ObjectT[]).map((item) => this.prepareObject(item)),
+ )
+ break
+ case 'interfaces':
+ preparedItems = await Promise.all(
+ (schema as InterfaceT[]).map((item) => this.prepareInterface(item)),
+ )
+ break
+ case 'enums':
+ preparedItems = await Promise.all((schema as EnumT[]).map((item) => this.prepareEnum(item)))
+ break
+ case 'unions':
+ preparedItems = await Promise.all(
+ (schema as UnionT[]).map((item) => this.prepareUnion(item)),
+ )
+ break
+ case 'inputObjects':
+ preparedItems = await Promise.all(
+ (schema as InputObjectT[]).map((item) => this.prepareInputObject(item)),
+ )
+ break
+ case 'scalars':
+ preparedItems = await Promise.all(
+ (schema as ScalarT[]).map((item) => this.prepareScalar(item)),
+ )
+ break
+ }
+
+ const templateData = {
+ pageTitle: page.title,
+ pageIntro: intro,
+ manualContent,
+ items: preparedItems,
+ pageType: schemaKey,
+ }
+
+ const templatePath = join(__dirname, '../templates/graphql-reference.template.md')
+ const templateContent = readFileSync(templatePath, 'utf8')
+
+ return await renderContent(templateContent, {
+ ...context,
+ ...templateData,
+ markdownRequested: true,
+ })
+ }
+
+ /**
+ * Transform changelog page
+ */
+ private async transformChangelog(
+ page: Page,
+ currentVersion: string,
+ context: Context,
+ ): Promise {
+ const { getGraphqlChangelog } = await import('@/graphql/lib/index')
+
+ const schema = getGraphqlChangelog(currentVersion) as ChangelogItemT[]
+
+ const intro = page.intro ? await page.renderProp('intro', context, { textOnly: true }) : ''
+ const manualContent = await this.extractManualContent(page, context)
+
+ // Process changelog items
+ const changelogItems = schema.map((item) => {
+ const processChanges = (changes: Array<{ title: string; changes: string[] }>) =>
+ changes.map((change) => ({
+ title: change.title,
+ changes: change.changes.map((html: string) => {
+ // Remove wrapping tags if present
+ if (html.startsWith('
') && html.endsWith('
')) {
+ return fastTextOnly(html.slice(3, -4))
+ }
+ return fastTextOnly(html)
+ }),
+ }))
+
+ return {
+ date: item.date,
+ schemaChanges: processChanges(item.schemaChanges || []),
+ previewChanges: processChanges(item.previewChanges || []),
+ upcomingChanges: processChanges(item.upcomingChanges || []),
+ }
+ })
+
+ const templateData = {
+ pageTitle: page.title,
+ pageIntro: intro,
+ manualContent,
+ changelogItems,
+ }
+
+ const templatePath = join(__dirname, '../templates/graphql-changelog.template.md')
+ const templateContent = readFileSync(templatePath, 'utf8')
+
+ return await renderContent(templateContent, {
+ ...context,
+ ...templateData,
+ markdownRequested: true,
+ })
+ }
+
+ /**
+ * Transform breaking changes page
+ */
+ private async transformBreakingChanges(
+ page: Page,
+ currentVersion: string,
+ context: Context,
+ ): Promise {
+ const { getGraphqlBreakingChanges } = await import('@/graphql/lib/index')
+
+ const schema = getGraphqlBreakingChanges(currentVersion) as BreakingChangesT
+
+ const intro = page.intro ? await page.renderProp('intro', context, { textOnly: true }) : ''
+ const manualContent = await this.extractManualContent(page, context)
+
+ const slugger = new GithubSlugger()
+
+ // Process breaking changes by date
+ const breakingChangesByDate = Object.keys(schema).map((date) => {
+ const items = schema[date]
+ const heading = `Changes scheduled for ${date}`
+ const slug = slugger.slug(heading)
+
+ return {
+ date,
+ heading,
+ slug,
+ items: items.map((item) => ({
+ location: item.location,
+ description: fastTextOnly(item.description),
+ reason: fastTextOnly(item.reason),
+ criticality: item.criticality,
+ })),
+ }
+ })
+
+ const templateData = {
+ pageTitle: page.title,
+ pageIntro: intro,
+ manualContent,
+ breakingChangesByDate,
+ }
+
+ const templatePath = join(__dirname, '../templates/graphql-breaking-changes.template.md')
+ const templateContent = readFileSync(templatePath, 'utf8')
+
+ return await renderContent(templateContent, {
+ ...context,
+ ...templateData,
+ markdownRequested: true,
+ })
+ }
+
+ /**
+ * Extract manual content from page markdown
+ */
+ private async extractManualContent(page: Page, context: Context): Promise {
+ if (!page.markdown) return ''
+
+ const markerIndex = page.markdown.indexOf(
+ '',
+ )
+
+ if (markerIndex <= 0) return ''
+
+ const { content } = matter(page.markdown)
+ const manualContentMarkerIndex = content.indexOf(
+ '',
+ )
+
+ if (manualContentMarkerIndex <= 0) return ''
+
+ const rawManualContent = content.substring(0, manualContentMarkerIndex).trim()
+ if (!rawManualContent) return ''
+
+ return await renderContent(rawManualContent, {
+ ...context,
+ markdownRequested: true,
+ })
+ }
+
+ /**
+ * Prepare a query item for rendering
+ */
+ private async prepareQuery(query: QueryT): Promise> {
+ return {
+ name: query.name,
+ slug: query.name.toLowerCase(),
+ description: query.description ? fastTextOnly(query.description) : '',
+ type: query.type,
+ href: query.href,
+ isDeprecated: query.isDeprecated || false,
+ deprecationReason: query.deprecationReason
+ ? fastTextOnly(query.deprecationReason)
+ : undefined,
+ args: query.args.map((arg) => ({
+ name: arg.name,
+ type: arg.type,
+ href: arg.href,
+ description: arg.description ? fastTextOnly(arg.description) : '',
+ })),
+ }
+ }
+
+ /**
+ * Prepare a mutation item for rendering
+ */
+ private async prepareMutation(mutation: MutationT): Promise> {
+ return {
+ name: mutation.name,
+ slug: mutation.name.toLowerCase(),
+ description: mutation.description ? fastTextOnly(mutation.description) : '',
+ isDeprecated: mutation.isDeprecated || false,
+ deprecationReason: mutation.deprecationReason
+ ? fastTextOnly(mutation.deprecationReason)
+ : undefined,
+ inputFields: await this.prepareFields(mutation.inputFields),
+ returnFields: await this.prepareFields(mutation.returnFields),
+ }
+ }
+
+ /**
+ * Prepare an object item for rendering
+ */
+ private async prepareObject(object: ObjectT): Promise> {
+ return {
+ name: object.name,
+ slug: object.name.toLowerCase(),
+ description: object.description ? fastTextOnly(object.description) : '',
+ isDeprecated: object.isDeprecated || false,
+ deprecationReason: object.deprecationReason
+ ? fastTextOnly(object.deprecationReason)
+ : undefined,
+ implements: object.implements || [],
+ fields: await this.prepareFields(object.fields),
+ }
+ }
+
+ /**
+ * Prepare an interface item for rendering
+ */
+ private async prepareInterface(item: InterfaceT): Promise> {
+ return {
+ name: item.name,
+ slug: item.name.toLowerCase(),
+ description: item.description ? fastTextOnly(item.description) : '',
+ isDeprecated: item.isDeprecated || false,
+ deprecationReason: item.deprecationReason ? fastTextOnly(item.deprecationReason) : undefined,
+ fields: await this.prepareFields(item.fields),
+ }
+ }
+
+ /**
+ * Prepare an enum item for rendering
+ */
+ private async prepareEnum(item: EnumT): Promise> {
+ return {
+ name: item.name,
+ slug: item.name.toLowerCase(),
+ description: item.description ? fastTextOnly(item.description) : '',
+ isDeprecated: item.isDeprecated || false,
+ deprecationReason: item.deprecationReason ? fastTextOnly(item.deprecationReason) : undefined,
+ values: item.values.map((value) => ({
+ name: value.name,
+ description: value.description ? fastTextOnly(value.description) : '',
+ })),
+ }
+ }
+
+ /**
+ * Prepare a union item for rendering
+ */
+ private async prepareUnion(item: UnionT): Promise> {
+ return {
+ name: item.name,
+ slug: item.name.toLowerCase(),
+ description: item.description ? fastTextOnly(item.description) : '',
+ isDeprecated: item.isDeprecated || false,
+ deprecationReason: item.deprecationReason ? fastTextOnly(item.deprecationReason) : undefined,
+ possibleTypes: item.possibleTypes || [],
+ }
+ }
+
+ /**
+ * Prepare an input object item for rendering
+ */
+ private async prepareInputObject(item: InputObjectT): Promise> {
+ return {
+ name: item.name,
+ slug: item.name.toLowerCase(),
+ description: item.description ? fastTextOnly(item.description) : '',
+ isDeprecated: item.isDeprecated || false,
+ deprecationReason: item.deprecationReason ? fastTextOnly(item.deprecationReason) : undefined,
+ inputFields: await this.prepareFields(item.inputFields),
+ }
+ }
+
+ /**
+ * Prepare a scalar item for rendering
+ */
+ private async prepareScalar(item: ScalarT): Promise> {
+ return {
+ name: item.name,
+ slug: item.name.toLowerCase(),
+ description: item.description ? fastTextOnly(item.description) : '',
+ isDeprecated: item.isDeprecated || false,
+ deprecationReason: item.deprecationReason ? fastTextOnly(item.deprecationReason) : undefined,
+ }
+ }
+
+ /**
+ * Prepare fields for rendering
+ */
+ private async prepareFields(fields: FieldT[]): Promise>> {
+ return fields.map((field) => ({
+ name: field.name,
+ type: field.type,
+ href: field.href,
+ description: field.description ? fastTextOnly(field.description) : '',
+ defaultValue: field.defaultValue,
+ isDeprecated: field.isDeprecated || false,
+ deprecationReason: field.deprecationReason
+ ? fastTextOnly(field.deprecationReason)
+ : undefined,
+ arguments: field.arguments
+ ? field.arguments.map((arg) => ({
+ name: arg.name,
+ description: arg.description ? fastTextOnly(arg.description) : '',
+ defaultValue: arg.defaultValue,
+ type: {
+ name: arg.type.name,
+ href: arg.type.href,
+ },
+ }))
+ : undefined,
+ }))
+ }
+}
diff --git a/src/article-api/transformers/index.ts b/src/article-api/transformers/index.ts
index a759723782..1ee146a920 100644
--- a/src/article-api/transformers/index.ts
+++ b/src/article-api/transformers/index.ts
@@ -1,5 +1,6 @@
import { TransformerRegistry } from './types'
import { RestTransformer } from './rest-transformer'
+import { GraphQLTransformer } from './graphql-transformer'
/**
* Global transformer registry
@@ -10,6 +11,9 @@ export const transformerRegistry = new TransformerRegistry()
// Register REST transformer
transformerRegistry.register(new RestTransformer())
+// Register GraphQL transformer
+transformerRegistry.register(new GraphQLTransformer())
+
// Future transformers can be registered here:
// transformerRegistry.register(new WebhooksTransformer())
// transformerRegistry.register(new GitHubAppsTransformer())
diff --git a/src/fixtures/fixtures/content/graphql/index.md b/src/fixtures/fixtures/content/graphql/index.md
new file mode 100644
index 0000000000..0f156df84b
--- /dev/null
+++ b/src/fixtures/fixtures/content/graphql/index.md
@@ -0,0 +1,12 @@
+---
+title: GitHub GraphQL API documentation
+intro: 'To create integrations, retrieve data, and automate your workflows, use the {% data variables.product.prodname_dotcom %} GraphQL API.'
+shortTitle: GraphQL API
+versions:
+ fpt: '*'
+ ghec: '*'
+ ghes: '*'
+children:
+ - /overview
+ - /reference
+---
\ No newline at end of file
diff --git a/src/fixtures/fixtures/content/graphql/overview/breaking-changes.md b/src/fixtures/fixtures/content/graphql/overview/breaking-changes.md
new file mode 100644
index 0000000000..a00972605c
--- /dev/null
+++ b/src/fixtures/fixtures/content/graphql/overview/breaking-changes.md
@@ -0,0 +1,20 @@
+---
+title: Breaking changes
+intro: Learn about recent and upcoming breaking changes to the {% data variables.product.prodname_dotcom %} GraphQL API.
+versions:
+ fpt: '*'
+ ghec: '*'
+ ghes: '*'
+autogenerated: graphql
+---
+
+## About breaking changes
+
+Breaking changes are any changes that might require action from our integrators. We divide these changes into two categories:
+
+* **Breaking:** Changes that will break existing queries to the GraphQL API. For example, removing a field would be a breaking change.
+* **Dangerous:** Changes that won't break existing queries but could affect the runtime behavior of clients. Adding an enum value is an example of a dangerous change.
+
+We'll announce upcoming breaking changes at least three months before making changes to the GraphQL schema, to give integrators time to make the necessary adjustments.
+
+
\ No newline at end of file
diff --git a/src/fixtures/fixtures/content/graphql/overview/changelog.md b/src/fixtures/fixtures/content/graphql/overview/changelog.md
new file mode 100644
index 0000000000..248d15f70c
--- /dev/null
+++ b/src/fixtures/fixtures/content/graphql/overview/changelog.md
@@ -0,0 +1,13 @@
+---
+title: Changelog
+intro: The GraphQL schema changelog is a list of recent and upcoming changes to our GraphQL API schema.
+versions:
+ fpt: '*'
+ ghec: '*'
+ ghes: '*'
+autogenerated: graphql
+---
+
+Breaking changes include changes that will break existing queries or could affect the runtime behavior of clients. For a list of breaking changes and when they will occur, see our breaking changes log.
+
+
\ No newline at end of file
diff --git a/src/fixtures/fixtures/content/graphql/overview/index.md b/src/fixtures/fixtures/content/graphql/overview/index.md
new file mode 100644
index 0000000000..3bf6a4f197
--- /dev/null
+++ b/src/fixtures/fixtures/content/graphql/overview/index.md
@@ -0,0 +1,15 @@
+---
+title: Overview
+intro: Learn about the GraphQL API overview topics.
+versions:
+ fpt: '*'
+ ghec: '*'
+ ghes: '*'
+children:
+ - /breaking-changes
+ - /changelog
+---
+
+## GraphQL API overview
+
+The GraphQL API provides a powerful way to query GitHub data.
\ No newline at end of file
diff --git a/src/fixtures/fixtures/content/graphql/reference/enums.md b/src/fixtures/fixtures/content/graphql/reference/enums.md
new file mode 100644
index 0000000000..f6a15cadaf
--- /dev/null
+++ b/src/fixtures/fixtures/content/graphql/reference/enums.md
@@ -0,0 +1,17 @@
+---
+title: Enums
+intro: Enums represent possible sets of values for a field.
+versions:
+ fpt: '*'
+ ghec: '*'
+ ghes: '*'
+autogenerated: graphql
+---
+
+## About enums
+
+Enums are special types that define a set of possible values. When a field has an enum type, it can only be set to one of the predefined values.
+
+For more information, see [AUTOTITLE](/get-started/start-your-journey/hello-world).
+
+
\ No newline at end of file
diff --git a/src/fixtures/fixtures/content/graphql/reference/index.md b/src/fixtures/fixtures/content/graphql/reference/index.md
new file mode 100644
index 0000000000..b93b3e6db2
--- /dev/null
+++ b/src/fixtures/fixtures/content/graphql/reference/index.md
@@ -0,0 +1,20 @@
+---
+title: Reference
+intro: View reference documentation to learn about the data types available in the {% data variables.product.prodname_dotcom %} GraphQL API schema.
+versions:
+ fpt: '*'
+ ghec: '*'
+ ghes: '*'
+children:
+ - /queries
+ - /mutations
+ - /objects
+ - /interfaces
+ - /enums
+ - /unions
+ - /input-objects
+ - /scalars
+autogenerated: graphql
+---
+
+
\ No newline at end of file
diff --git a/src/fixtures/fixtures/content/graphql/reference/input-objects.md b/src/fixtures/fixtures/content/graphql/reference/input-objects.md
new file mode 100644
index 0000000000..aa1265a6f1
--- /dev/null
+++ b/src/fixtures/fixtures/content/graphql/reference/input-objects.md
@@ -0,0 +1,17 @@
+---
+title: Input objects
+intro: Input objects are special types that allow you to pass complex objects as arguments to queries and mutations.
+versions:
+ fpt: '*'
+ ghec: '*'
+ ghes: '*'
+autogenerated: graphql
+---
+
+## About input objects
+
+Input objects are used when you need to pass a structured set of values as an argument to a field. They are similar to regular objects, but they are specifically designed to be used as input arguments.
+
+For more information, see [AUTOTITLE](/get-started/start-your-journey/hello-world).
+
+
\ No newline at end of file
diff --git a/src/fixtures/fixtures/content/graphql/reference/interfaces.md b/src/fixtures/fixtures/content/graphql/reference/interfaces.md
new file mode 100644
index 0000000000..f93c982d7c
--- /dev/null
+++ b/src/fixtures/fixtures/content/graphql/reference/interfaces.md
@@ -0,0 +1,17 @@
+---
+title: Interfaces
+intro: Interfaces are abstract types that include a certain set of fields that other types must include if they implement the interface.
+versions:
+ fpt: '*'
+ ghec: '*'
+ ghes: '*'
+autogenerated: graphql
+---
+
+## About interfaces
+
+Interfaces allow you to define a set of fields that multiple object types can implement. They help ensure consistency across your schema.
+
+For more information, see [AUTOTITLE](/get-started/start-your-journey/hello-world).
+
+
\ No newline at end of file
diff --git a/src/fixtures/fixtures/content/graphql/reference/mutations.md b/src/fixtures/fixtures/content/graphql/reference/mutations.md
new file mode 100644
index 0000000000..6d0894956c
--- /dev/null
+++ b/src/fixtures/fixtures/content/graphql/reference/mutations.md
@@ -0,0 +1,17 @@
+---
+title: Mutations
+intro: The mutation type defines GraphQL operations that create, update, or delete data on the server.
+versions:
+ fpt: '*'
+ ghec: '*'
+ ghes: '*'
+autogenerated: graphql
+---
+
+## About mutations
+
+Every GraphQL schema has a root type for both queries and mutations. The mutation type defines GraphQL operations that modify data on the server.
+
+For more information, see [AUTOTITLE](/get-started/start-your-journey/hello-world).
+
+
\ No newline at end of file
diff --git a/src/fixtures/fixtures/content/graphql/reference/objects.md b/src/fixtures/fixtures/content/graphql/reference/objects.md
new file mode 100644
index 0000000000..074cd5b87b
--- /dev/null
+++ b/src/fixtures/fixtures/content/graphql/reference/objects.md
@@ -0,0 +1,17 @@
+---
+title: Objects
+intro: Objects in GraphQL represent the resources you can access. An object can contain a list of fields, which are specifically typed.
+versions:
+ fpt: '*'
+ ghec: '*'
+ ghes: '*'
+autogenerated: graphql
+---
+
+## About objects
+
+Objects are the most common type in a GraphQL schema. They represent the resources you can access and contain fields that define the data you can query.
+
+For more information, see [AUTOTITLE](/get-started/start-your-journey/hello-world).
+
+
\ No newline at end of file
diff --git a/src/fixtures/fixtures/content/graphql/reference/queries.md b/src/fixtures/fixtures/content/graphql/reference/queries.md
new file mode 100644
index 0000000000..10427d7cf7
--- /dev/null
+++ b/src/fixtures/fixtures/content/graphql/reference/queries.md
@@ -0,0 +1,17 @@
+---
+title: Queries
+intro: The query type defines GraphQL operations that retrieve data from the server.
+versions:
+ fpt: '*'
+ ghec: '*'
+ ghes: '*'
+autogenerated: graphql
+---
+
+## About queries
+
+Every GraphQL schema has a root type for both queries and mutations. The query type defines GraphQL operations that retrieve data from the server.
+
+For more information, see [AUTOTITLE](/get-started/start-your-journey/hello-world).
+
+
\ No newline at end of file
diff --git a/src/fixtures/fixtures/content/graphql/reference/scalars.md b/src/fixtures/fixtures/content/graphql/reference/scalars.md
new file mode 100644
index 0000000000..939b3a6ec6
--- /dev/null
+++ b/src/fixtures/fixtures/content/graphql/reference/scalars.md
@@ -0,0 +1,17 @@
+---
+title: Scalars
+intro: Scalars are primitive values in GraphQL. They represent the leaves of a query.
+versions:
+ fpt: '*'
+ ghec: '*'
+ ghes: '*'
+autogenerated: graphql
+---
+
+## About scalars
+
+Scalars are primitive types that represent concrete values. GraphQL comes with a set of default scalar types, including String, Int, Float, Boolean, and ID.
+
+For more information, see [AUTOTITLE](/get-started/start-your-journey/hello-world).
+
+
\ No newline at end of file
diff --git a/src/fixtures/fixtures/content/graphql/reference/unions.md b/src/fixtures/fixtures/content/graphql/reference/unions.md
new file mode 100644
index 0000000000..49c919383c
--- /dev/null
+++ b/src/fixtures/fixtures/content/graphql/reference/unions.md
@@ -0,0 +1,17 @@
+---
+title: Unions
+intro: Unions represent an object that could be one of multiple types.
+versions:
+ fpt: '*'
+ ghec: '*'
+ ghes: '*'
+autogenerated: graphql
+---
+
+## About unions
+
+A union type is a special type that represents an object that could be one of a listed set of types. When you query a field that returns a union type, you need to use conditional fragments to query any fields.
+
+For more information, see [AUTOTITLE](/get-started/start-your-journey/hello-world).
+
+
\ No newline at end of file
diff --git a/src/fixtures/fixtures/content/index.md b/src/fixtures/fixtures/content/index.md
index e3b95a0713..7b817882b2 100644
--- a/src/fixtures/fixtures/content/index.md
+++ b/src/fixtures/fixtures/content/index.md
@@ -32,6 +32,7 @@ children:
- actions
- rest
- webhooks
+ - graphql
- video-transcripts
# - account-and-profile
# - authentication
diff --git a/src/types/types.ts b/src/types/types.ts
index 343ce700ab..74e2819256 100644
--- a/src/types/types.ts
+++ b/src/types/types.ts
@@ -388,6 +388,7 @@ export type Page = {
sidebarLink?: SidebarLink
type?: string
contentType?: string
+ children?: string[]
}
export type SidebarLink = {