From 13ce631f53e304798589b9674b6a9419f8da93d4 Mon Sep 17 00:00:00 2001 From: Roman Marshevskyi Date: Thu, 27 Aug 2026 12:42:29 +0300 Subject: [PATCH 1/4] docs(blog): add client generator intro blog post as a React page - Add /blog/agent-friendly-sdks as a React page with inline hero diagram and a reviewer-style CTA card - Support react-frontmatter for .page.tsx blog posts in the theme plugin - Add blog-recent-posts shared data (top 4) and a shared RecentPosts component that excludes the currently open post; localize the BlogPost template to use it - Add api-descriptions:openapi and api-lifecycle:sdks blog categories Co-Authored-By: Claude Fable 5 --- @theme/components/Blog/RecentPosts.tsx | 52 ++ @theme/plugin.js | 32 +- @theme/templates/BlogPost.tsx | 64 +- @theme/utils/blog-post.js | 34 +- blog/agent-friendly-sdks.page.tsx | 949 +++++++++++++++++++++++++ blog/images/cta-eclipse.svg | 16 + blog/metadata/blog-metadata.yaml | 8 + 7 files changed, 1131 insertions(+), 24 deletions(-) create mode 100644 @theme/components/Blog/RecentPosts.tsx create mode 100644 blog/agent-friendly-sdks.page.tsx create mode 100644 blog/images/cta-eclipse.svg diff --git a/@theme/components/Blog/RecentPosts.tsx b/@theme/components/Blog/RecentPosts.tsx new file mode 100644 index 000000000..fa1c99e12 --- /dev/null +++ b/@theme/components/Blog/RecentPosts.tsx @@ -0,0 +1,52 @@ +import * as React from 'react'; +import styled from 'styled-components'; + +import { useThemeHooks } from '@redocly/theme/core/hooks'; + +import { ArticleCard } from '@redocly/marketing-pages/components/Blog/RecentPosts.js'; +import { H2Title } from '@redocly/marketing-pages/components/TypographyElements/TypographyElements.js'; + +type RecentPost = { slug: string; title: string; description?: string }; + +// Same layout as marketing-pages RecentPosts, but reads the deeper 'blog-recent-posts' +// shared data and filters out the post it renders on, so a post never lists itself. +export function RecentPosts({ currentSlug }: { currentSlug?: string }) { + // @ts-ignore + const { usePageSharedData } = useThemeHooks(); + const recentPosts = usePageSharedData('blog-recent-posts') ?? []; + + const posts = recentPosts.filter((post) => post.slug !== currentSlug).slice(0, 3); + + if (posts.length === 0) { + return null; + } + + return ( + <> + + Latest from our blog + + + {posts.map((post) => ( + + ))} + + + ); +} + +const RecentPostsGrid = styled.div` + display: grid; + grid-template-columns: 1fr; + grid-gap: 5rem; + justify-items: center; + + @media screen and (min-width: 900px) { + grid-template-columns: 1fr 1fr 1fr; + } +`; diff --git a/@theme/plugin.js b/@theme/plugin.js index 5f3484125..4180912b0 100644 --- a/@theme/plugin.js +++ b/@theme/plugin.js @@ -7,6 +7,7 @@ const ABOUT_SLUG = '/about/'; const BLOG_METADATA_PATH = 'blog/metadata/blog-metadata.yaml'; const LATEST_POSTS_SHARED_DATA_ID = 'blog-latest-posts'; +const RECENT_POSTS_SHARED_DATA_ID = 'blog-recent-posts'; const ALL_POSTS_SHARED_DATA_ID = 'blog-posts'; function __dirname(url) { @@ -26,11 +27,11 @@ export default function themePlugin() { // Register preview route for the editor iframe const previewTemplateId = actions.createTemplate( 'preview-template', - fromCurrentDir(import.meta.url, './preview.route.tsx') + fromCurrentDir(import.meta.url, './preview.route.tsx'), ); const blogTemplateId = actions.createTemplate( - 'blog-template', - fromCurrentDir(import.meta.url, './blog.page.tsx') + 'blog-template', + fromCurrentDir(import.meta.url, './blog.page.tsx'), ); actions.addRoute({ excludeFromSidebar: true, @@ -58,7 +59,7 @@ export default function themePlugin() { templateId: blogTemplateId, hasClientRoutes: true, }); - + const metadataContentRecord = await context.cache.load(BLOG_METADATA_PATH, 'yaml'); const categories = metadataContentRecord.data.categories || []; @@ -84,10 +85,11 @@ export default function themePlugin() { // Existing blog data processing const postRoutes = actions .getAllRoutes() - .filter((route) => - route.slug.startsWith(BLOG_SLUG) && - route.slug !== BLOG_SLUG && - !route.slug.startsWith('/blog/category/') + .filter( + (route) => + route.slug.startsWith(BLOG_SLUG) && + route.slug !== BLOG_SLUG && + !route.slug.startsWith('/blog/category/'), ); const categoryRoutes = actions @@ -98,8 +100,14 @@ export default function themePlugin() { const latestPosts = postsData.posts.slice(0, 3); + // Top 4 posts, so a post page can exclude itself and still show 3 recent posts + const recentPosts = postsData.posts + .slice(0, 4) + .map(({ slug, title, description }) => ({ slug, title, description })); + // Create shared data for blog pages await actions.createSharedData(LATEST_POSTS_SHARED_DATA_ID, latestPosts); + await actions.createSharedData(RECENT_POSTS_SHARED_DATA_ID, recentPosts); await actions.createSharedData(ALL_POSTS_SHARED_DATA_ID, postsData); // Add latest posts shared data to all blog posts and update metadata @@ -110,11 +118,17 @@ export default function themePlugin() { LATEST_POSTS_SHARED_DATA_ID, ); + actions.addRouteSharedData( + post.slug, + RECENT_POSTS_SHARED_DATA_ID, + RECENT_POSTS_SHARED_DATA_ID, + ); + const postRoute = actions.getRouteBySlug(post.slug); postRoute.metadata = { ...postRoute.metadata, ...post }; } - + // Add all posts shared data to category routes for (const categoryRoute of categoryRoutes) { actions.addRouteSharedData( diff --git a/@theme/templates/BlogPost.tsx b/@theme/templates/BlogPost.tsx index cde0630f7..5e4db51f6 100644 --- a/@theme/templates/BlogPost.tsx +++ b/@theme/templates/BlogPost.tsx @@ -1,3 +1,63 @@ -import Page from '@redocly/marketing-pages/templates/BlogPost.js'; +import React from 'react'; +import styled from 'styled-components'; -export default Page; \ No newline at end of file +import type { Post } from '@redocly/marketing-pages/components/Blog/types.js'; + +import { useThemeHooks } from '@redocly/theme/core/hooks'; +import { Markdown } from '@redocly/theme/components/Markdown/Markdown'; +import PostInfo from '@redocly/marketing-pages/components/Blog/PostInfo.js'; +import { MediaBox } from '@redocly/marketing-pages/components/PositionItems/MediaBox.js'; +import { Box } from '@redocly/marketing-pages/ui/Box.js'; + +import { RecentPosts } from '../components/Blog/RecentPosts'; + +// Local version of @redocly/marketing-pages/templates/BlogPost.js — the only +// difference is the RecentPosts section, which excludes the currently open post. +export default function BlogPost(props: { children?: React.ReactNode }) { + const { usePageProps } = useThemeHooks(); + const pageProps = usePageProps(); + + const { publishedDate, author, categories, title, image, slug } = pageProps.metadata as Post & { + slug?: string; + }; + + return ( + + + + + {props.children} + + + + + + + + + ); +} + +const BlogMediaBox = styled.div` + margin-left: auto; + margin-right: auto; + max-width: calc(90vw); + + @media screen and (min-width: 900px) { + max-width: 800px; + } +`; + +const PageWrapper = styled.div` + position: relative; + overflow: hidden; +`; diff --git a/@theme/utils/blog-post.js b/@theme/utils/blog-post.js index 8ed216162..563b97973 100644 --- a/@theme/utils/blog-post.js +++ b/@theme/utils/blog-post.js @@ -10,11 +10,19 @@ export const buildAndSortBlogPosts = async (postRoutes, context, outdir) => { const metadata = await transformMetadata(metadataContentRecord.data, context.fs.cwd, outdir); for (const route of postRoutes) { - const { - data: { content, frontmatter }, - } = await context.cache.load(route.fsPath, 'markdown-frontmatter'); + // React blog posts export `frontmatter`; markdown posts use YAML frontmatter + const isReactPage = /\.page\.tsx?$/.test(route.fsPath); + const { data } = await context.cache.load( + route.fsPath, + isReactPage ? 'react-frontmatter' : 'markdown-frontmatter', + ); + const frontmatter = isReactPage ? data : data?.frontmatter; - if (frontmatter?.ignore === true || (await context.isPathIgnored(route.fsPath))) { + if ( + (isReactPage && !frontmatter) || + frontmatter?.ignore === true || + (await context.isPathIgnored(route.fsPath)) + ) { continue; } @@ -26,15 +34,15 @@ export const buildAndSortBlogPosts = async (postRoutes, context, outdir) => { .map((categoryId) => { const categoryData = metadata.categories.get(categoryId); if (!categoryData) return null; - + if (categoryData.category && categoryData.subcategory) { - return categoryData; + return categoryData; } else { - return { + return { category: { - id: categoryData.id, - label: categoryData.label - } + id: categoryData.id, + label: categoryData.label, + }, }; } }) @@ -72,12 +80,12 @@ async function transformMetadata(metadata, cwd, outdir) { categories.set(fullId, { category: { id: category.id, - label: category.label + label: category.label, }, subcategory: { id: subcategory.id, - label: subcategory.label - } + label: subcategory.label, + }, }); } } diff --git a/blog/agent-friendly-sdks.page.tsx b/blog/agent-friendly-sdks.page.tsx new file mode 100644 index 000000000..557d64847 --- /dev/null +++ b/blog/agent-friendly-sdks.page.tsx @@ -0,0 +1,949 @@ +import React from 'react'; +import styled from 'styled-components'; + +import { useThemeHooks } from '@redocly/theme/core/hooks'; +import { Markdown } from '@redocly/theme/components/Markdown/Markdown'; + +import type { Post } from '@redocly/marketing-pages/components/Blog/types.js'; +import PostInfo from '@redocly/marketing-pages/components/Blog/PostInfo.js'; +import { MediaBox } from '@redocly/marketing-pages/components/PositionItems/MediaBox.js'; +import { Box } from '@redocly/marketing-pages/ui/Box.js'; + +import { RecentPosts } from '../@theme/components/Blog/RecentPosts'; + +import ctaEclipse from './images/cta-eclipse.svg'; + +export const frontmatter = { + title: 'Open-source, agent-friendly SDKs and tooling from OpenAPI description', + description: + 'Meet redocly generate-client: one OpenAPI description becomes typed SDKs in TypeScript, Python, Go, and PHP — plus validation schemas, query hooks, test mocks, a CLI, and docs. Open source, zero runtime dependencies, and built for AI agents.', + seo: { + title: 'Open-source, agent-friendly SDKs and tooling from OpenAPI description', + description: + 'Meet redocly generate-client: one OpenAPI description becomes typed SDKs in TypeScript, Python, Go, and PHP — plus validation schemas, query hooks, test mocks, a CLI, and docs. Open source, zero runtime dependencies, and built for AI agents.', + }, + author: 'roman-marshevskyi', + publishedDate: '2026-08-27', + categories: ['redocly:redocly-cli', 'api-descriptions:openapi', 'api-lifecycle:sdks'], +}; + +export default function AgentFriendlySdksPost() { + const { usePageProps } = useThemeHooks(); + const pageProps = usePageProps(); + + const { publishedDate, author, categories, title, slug } = (pageProps.metadata ?? {}) as Post & { + slug?: string; + }; + + return ( + + + + + +
+ +
+
+ One OpenAPI description, one command, and every consumer of your API gets a typed, + dependency-free artifact that regenerates instead of drifting. +
+
+ + + + As agents become part of engineering teams, more of your API calls are written by one. + Agents hallucinate endpoints, invent response fields, and hand-write API code you then + review line by line. Generated code is the cheapest, safest code an agent can ship, so + we built a generator that treats the agent as a first-class user. + + +

+ Meet generate-client: a new command in the{' '} + Redocly CLI, powered by a new + package,{' '} + + @redocly/client-generator + + , that turns one OpenAPI description into typed SDKs in{' '} + TypeScript, Python, Go, and PHP, plus validation schemas, TanStack + Query and SWR hooks, test mocks, a ready-to-run command-line interface, + and reference docs for all of it. Both the command and the package are open source + (MIT), and everything they generate is yours outright. +

+ +

+ The SDKs are fully featured with zero runtime dependencies: auth, + retries, middleware, pagination iterators, typed Server-Sent Events, query-string + serialization, and multipart uploads, all built on web-standard fetch,{' '} + AbortController, and URLSearchParams, emitted as code that + imports nothing. The API code your agent used to hallucinate becomes one deterministic + command, and the compiler becomes its fact-checker: operation ids, parameters, and + response fields are literal types, so a wrong call fails tsc with the exact + operation named. +

+ +

Up and running in three steps

+ + + + 1 +
+ Start from your API description + + The one you already have: OpenAPI 3.0, 3.1, 3.2, or Swagger 2.0. + +
+                  # openapi.yaml
+                  {'\npaths:\n  /menu-items:\n    get:\n      operationId: '}
+                  listMenuItems
+                  {'\n  /orders/{orderId}:\n    get:\n      operationId: '}
+                  getOrderById
+                  {'\ncomponents:\n  securitySchemes:\n    BearerAuth:\n      type: '}
+                  http
+                  {'\n      scheme: '}
+                  bearer
+                
+
+
+ + + 2 +
+ Run one command + + No account, no config required. Flags or a redocly.yaml{' '} + client block, your choice. + +
+                  $ npx @redocly/cli@latest generate-client openapi.yaml{' '}
+                  --output src/client.ts
+                
+
+
+ + + 3 +
+ Call your API + + Every operation is a typed function; every name comes from the description. + +
+                  import
+                  {' { configure, listMenuItems, getOrderById } '}
+                  from './client.js'
+                  {';\n\n'}
+                  configure
+                  {'({ auth: { bearer: token } }); '}
+                  // sent only where an operation requires it
+                  {'\n\n'}
+                  const
+                  {' menu  = '}
+                  await listMenuItems
+                  {'({ query: { limit: '}
+                  10
+                  {' } });\n'}
+                  const
+                  {' order = '}
+                  await getOrderById
+                  {'({ path: { orderId: '}
+                  'ord_01khr…'
+                  {' } });'}
+                
+
+
+
+ +

That's the whole client.

+ + One description. One command. Every time your API changes. + +

The features you'd otherwise hand-write

+ +

+ Types are a third of the problem. The behavior is what teams hand-write around generated + types, and where drift starts. The generated client includes it: +

+ +
    +
  • + + Auth from your securitySchemes + + : bearer, basic, and API keys in header, query, or cookie, each sent only where an + operation's security requires it. Credentials can be async token + providers, resolved on every request, so refresh flows need no extra code. Every + client instance carries its own. +
  • +
  • + Pagination: declare your pagination convention once (cursor, offset, + page, or Link header) and the iterators appear on the operation itself:{' '} + listOrders.pages(), listOrders.items(), typed, abortable, + with duplicate-cursor loop detection. Delete your pagination loops. +
  • +
  • + Opt-in, abort-aware retries: exponential backoff, jitter,{' '} + Retry-After, idempotent-only by default, and a custom{' '} + retryOn predicate. +
  • +
  • + Typed Server-Sent Events: an operation whose 2xx is{' '} + text/event-stream becomes a typed async iterator with automatic + reconnection, payloads typed from OpenAPI 3.2's itemSchema. +
  • +
  • + Composable middleware: onRequest,{' '} + onResponse, and onError, with operation ids, paths, and tags + visible to it as literal types. +
  • +
  • + The fiddly details, handled: query parameters serialized exactly as + the description declares, file uploads from a plain typed object, per-request + timeouts, and idempotency keys that make retries safe. +
  • +
  • + Two error models: exceptions by default, or a typed{' '} + {'{ data, error }'} result if you prefer returns over throws. +
  • +
+ +

+ And it's strict on your behalf: a call with an argument the operation doesn't declare + fails before the request leaves the process, with an error that names the operation and + says where the argument belongs. +

+ +

+ It reads OpenAPI 3.0, 3.1, and 3.2, plus Swagger 2.0{' '} + (normalized to 3.x before generation). +

+ +

Skills first

+ +

+ Every part of this tool assumes an agent will operate it, and each of those decisions + helps the humans just as much: +

+ +
    +
  • + The design ships as agent skills. Every generator carries its own + design document, and ejecting a generator drops it into your repo as a skill ( + {'.claude/skills/-generator/'}) beside the authoring guide. An + agent asked to change generated output loads the rules first and edits the generator, + not the output. +
  • +
  • + A discoverable surface instead of prose. The generated CLI answers{' '} + --help with its commands and {'schema '} with one + operation's whole contract as JSON: method, path, parameters with types, request and + response schemas. An agent learns a real API in two commands. +
  • +
  • + Feedback an agent can act on. Strict types plus runtime + unknown-argument errors name the operation and say where the argument belongs. +
  • +
  • + Deterministic ground truth. The generated mocks are seeded and + offline, so tests an agent writes reproduce exactly, with no live API in the loop + teaching it wrong lessons. +
  • +
  • + Regeneration over hand-editing. The client is machine-owned and + rebuilt from the description; the generator is human-owned and ejectable. That split + tells an agent exactly which file it is allowed to change. +
  • +
+ +

+ The instruction we ship our own agents is one paragraph:{' '} + + never hand-write HTTP code for our APIs; regenerate the client and import the + functions, and a wrong call fails the build. + +

+ +

One description, every consumer

+ +

+ The vocabulary is simple: you select generators in one list, each + generator emits an artifact, and each artifact serves a different + consumer of your API. The SDK is one kind of artifact; here is the whole list, produced + from one parse of your description in one command: +

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
GeneratorArtifactConsumer
+ typescript (default), python, go,{' '} + php + the full typed client, in that languagecalling your API from any stack
+ zod + Zod schemas + validation middlewareruntime contract checks
+ tanstack-query, swr + query and mutation factories, hooksReact, Vue, Svelte, Solid data fetching
+ mock + MSW v2 handlers + typed data factoriestests and demos, offline and deterministic
+ transformers + + Date converters + + ISO strings → Date, paired with --date-type Date +
+ cli + a bin-ready command-line interfacescripts, CI, agents
your ownanythingthe long tail
+
+ +

+ Every language SDK carries the same behavior, each as a single self-contained file:{' '} + httpx for Python, the standard library for Go, the curl extension for PHP. + And names resolve once: listOrders is the operation in the description, the + function in every SDK, and the CLI command, so one identifier greps across your whole + stack. +

+ +

+ Docs are one flag: add --docs and every selected generator writes a + reference page beside its output. The docs regenerate with the code, so they cannot + drift from it. +

+ +

And if you disagree with a built-in, take it

+ +

+ When a tool gets something wrong for you, the traditional move is to fork it, and a fork + is a life sentence: you maintain the whole project from that day on, and upstream fixes + stop reaching you. Eject gives you the ownership without the fork: +

+ +
+            $ npx @redocly/cli@latest eject-generator python
+          
+ +

+ That copies the built-in generator into your repository as{' '} + TypeScript source you own: a folder with one readable file per stage + (naming, types, models, operations, pagination, client). It wires your config to it, + and, unmodified, it produces byte-identical output. We verify that byte-identity in our + test suite. Later versions merge into your copy file by file with --update. + The generator's design document arrives with it as an{' '} + agent skill in your repo, and the skill is yours to manage: edit it to + state your house rules (naming, headers, error style, whatever the built-in got wrong + for you), and your AI agent reads the skill first and changes the ejected generator to + match. You maintain a short design document; the agent maintains the code to it. +

+ +

Yours to shape

+ +
    +
  • + Call style: grouped inputs by default; --args-style flat{' '} + merges them into one object when an operation's inputs can't collide. +
  • +
  • + Output layout: one single file (default), or{' '} + split with schema types in a sibling module. +
  • +
  • + Runtime placement: inlined into the client by default for a truly + single-file artifact, or --runtime module to write the runtime as real, + readable files beside it, shared between clients. +
  • +
  • + No build step, if you want none: with --import-ext ts, + the generated client, the zod module, and the CLI run as they are under plain Node + 22.18+, which strips the types itself. +
  • +
  • + Configuration: CLI flags or a client block in{' '} + redocly.yaml, with per-API overrides for monorepos that generate several + clients from one config. +
  • +
+ +

Proven on ourselves first

+ +

+ We didn't design this in the abstract: Redocly's own platform runs on this generator: + four internal APIs, hundreds of operations, an in-house codegen deleted in the process, + and much of the migration executed by an AI agent working against the generated client. + That migration found real bugs our tests had missed, and it's the subject of the next + post. +

+ +

+ One caveat, stated plainly: the command is still experimental, flags and output may + change, so pin your CLI version. The code it generates is strict-TypeScript clean, + exhaustively tested, and already carrying Redocly's production traffic. +

+ + + + + Try it on your own API + + One command, no account, runs entirely on your machine. + + + +
+                $ npx @redocly/cli@latest generate-client openapi.yaml{' '}
+                --output src/client.ts
+              
+ + Then import a function and call your API. The whole client is in the file you just + generated. + + + + Command reference + + + Write a custom generator + + + Runnable examples + + GitHub + +
+
+
+
+ + + + + + +
+ ); +} + +function HeroDiagram() { + return ( + + + + + + + + + + + + + + + One description. Every consumer. + + + typed SDKs, a CLI, mocks, and docs. Built for agents, owned by you. + + + + + + openapi.yaml + + + + openapi: 3.1.0 + + + paths: + + + {'/orders/{orderId}:'} + + + get: … + + + securitySchemes: + + + BearerAuth: … + + + + + + + + + + + + $ + + + redocly + + + generate-client + + + → typed · zero deps · yours + + + + + + + + + + + + SDKs + + + client.ts · client.py · client.go · client.php + + + + + Schemas, hooks, and mocks + + + client.zod.ts · client.tanstack.ts · client.mocks.ts + + + + + CLI + + + client.cli.ts + + + + + Docs + + + client.python.md · client.cli.md + + + one page per artifact + + + + ); +} + +const HeroFigure = styled.figure` + margin: 6px 0 36px; + + .frame { + border: 1px solid #ededf2; + border-radius: 12px; + overflow: hidden; + background: #ffffff; + } + + svg { + display: block; + width: 100%; + height: auto; + } + + figcaption { + font-size: 13px; + color: #6e6f7a; + margin-top: 10px; + } +`; + +const PageWrapper = styled.div` + position: relative; + overflow: hidden; +`; + +const BlogMediaBox = styled.div` + margin-left: auto; + margin-right: auto; + max-width: calc(90vw); + + @media screen and (min-width: 900px) { + max-width: 800px; + } +`; + +const Lead = styled.p` + font-size: 19px; + line-height: 1.6; +`; + +// The theme's Markdown wrapper styles 'pre' (background, text color, padding), +// so token colors here are picked for contrast on its light code-block background. +const Pre = styled.pre` + border-radius: 8px; + font-size: 13.5px; + tab-size: 2; + white-space: pre; +`; + +const TokC = styled.span` + color: #59636e; +`; + +const TokK = styled.span` + color: #cf222e; +`; + +const TokS = styled.span` + color: #116329; +`; + +const TokF = styled.span` + color: #8250df; +`; + +const TokFlag = styled.span` + color: #953800; + font-weight: 600; +`; + +const Steps = styled.div` + display: flex; + flex-direction: column; + gap: 18px; + margin: 26px 0 10px; +`; + +const Step = styled.div` + display: grid; + grid-template-columns: 34px 1fr; + gap: 14px; + + /* Let the code block shrink and scroll instead of widening the column */ + > div { + min-width: 0; + } + + pre { + margin-bottom: 0; + } + + @media (max-width: 560px) { + grid-template-columns: 1fr; + } +`; + +const StepNumber = styled.div` + width: 34px; + height: 34px; + border-radius: 999px; + background: #e7f3ff; + color: #2467f2; + font-weight: 800; + font-size: 16px; + display: flex; + align-items: center; + justify-content: center; + margin-top: 2px; +`; + +const StepLabel = styled.div` + font-weight: 700; + font-size: 16.5px; + margin: 6px 0 8px; +`; + +const StepHint = styled.div` + font-size: 14px; + color: var(--color-text-dimmed, #6e6f7a); + margin: -4px 0 8px; +`; + +const StepsTagline = styled.p` + font-weight: 700; + font-size: 17px; + text-align: center; + margin: 22px 0 0; +`; + +const TableScroll = styled.div` + overflow-x: auto; + margin: 0 0 1.3em; + border: 1px solid #ededf2; + border-radius: 8px; + + table { + border-collapse: collapse; + width: 100%; + min-width: 560px; /* scroll horizontally on small screens instead of squashing columns */ + font-size: 14.5px; + margin: 0; + } + + th, + td { + text-align: left; + padding: 11px 16px; + border-bottom: 1px solid #ededf2; + vertical-align: top; + } + + thead th { + background: #fbfbfc; + font-weight: 600; + white-space: nowrap; + } + + tbody tr:last-child td { + border-bottom: none; + } +`; + +// CTA card in the style of the /reviewer page: tonal card, split columns, eclipse glow. +const CtaCard = styled.div` + position: relative; + display: flex; + flex-direction: column; + overflow: hidden; + margin-top: 64px; + border-radius: 32px; + background-color: var(--bg-color-tonal); + + > div:last-child { + border-top: 1px solid var(--border-color-secondary); + } + + @media screen and (min-width: 768px) { + flex-direction: row; + + > div:last-child { + border-left: 1px solid var(--border-color-secondary); + border-top: none; + } + } +`; + +/* The "Eclipse" glow from the reviewer page CTA: a blurred pink→violet ellipse + anchored to the card's bottom-left, clipped by the card. */ +const CtaGlow = styled.div` + position: absolute; + top: 135px; + left: -37px; + display: flex; + align-items: center; + justify-content: center; + width: 564px; + height: 369px; + pointer-events: none; +`; + +const CtaGlowInner = styled.div` + position: relative; + flex: none; + width: 267px; + height: 557px; + transform: rotate(93.43deg) scaleY(0.99) skewX(-7.19deg); + + img { + position: absolute; + inset: -35.89% -75.04%; + width: 250.08%; + height: 171.78%; + max-width: none; + } +`; + +const CtaTitleColumn = styled.div` + position: relative; + flex: 1; + min-width: 0; + display: flex; + flex-direction: column; + gap: 12px; + padding: 32px 32px 0; + + @media screen and (min-width: 768px) { + padding: 40px 32px 40px 40px; + } +`; + +const CtaTitle = styled.p` + margin: 0; + font-family: 'Red Hat Display'; + font-size: 32px; + font-weight: 700; + line-height: 40px; + color: var(--text-color-primary); +`; + +const CtaDescription = styled.p` + margin: 0; + font-family: 'Red Hat Display'; + font-size: 18px; + font-weight: 500; + line-height: 26px; + color: var(--text-color-helper); +`; + +const CtaActionColumn = styled.div` + position: relative; + display: flex; + flex: 1.4; + min-width: 0; + flex-direction: column; + gap: 20px; + padding: 32px; + + @media screen and (min-width: 768px) { + padding: 40px; + } + + &&& pre { + margin: 0; + background-color: var(--bg-color); + border: 1px solid var(--border-color-secondary); + white-space: pre-wrap; + word-break: break-word; + } +`; + +const CtaNote = styled.p` + margin: 0; + font-size: 14.5px; + line-height: 1.55; + color: var(--text-color-helper); +`; + +const CtaLinks = styled.div` + display: grid; + grid-template-columns: repeat(2, minmax(0, max-content)); + justify-content: start; + gap: 12px 40px; + font-size: 14.5px; + + a { + font-weight: 600; + text-decoration: none; + + &:hover { + text-decoration: underline; + } + } + + @media (max-width: 400px) { + grid-template-columns: minmax(0, max-content); + } +`; diff --git a/blog/images/cta-eclipse.svg b/blog/images/cta-eclipse.svg new file mode 100644 index 000000000..bc7ce0c7c --- /dev/null +++ b/blog/images/cta-eclipse.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/blog/metadata/blog-metadata.yaml b/blog/metadata/blog-metadata.yaml index 3238c1956..aaadea9f3 100644 --- a/blog/metadata/blog-metadata.yaml +++ b/blog/metadata/blog-metadata.yaml @@ -78,6 +78,12 @@ categories: - id: dependency-maps label: Dependency maps + - id: api-descriptions + label: API descriptions + subcategories: + - id: openapi + label: OpenAPI + - id: api-documentation label: API documentation subcategories: @@ -111,6 +117,8 @@ categories: subcategories: - id: design label: Design + - id: sdks + label: SDKs - id: mocking label: Mocking - id: release-management From 8786652c7469106a54218f5ec42fe709385aa2c7 Mon Sep 17 00:00:00 2001 From: Roman Marshevskyi Date: Thu, 27 Aug 2026 12:48:59 +0300 Subject: [PATCH 2/4] docs(realm): fix list indentation in MCP server page for markdownlint Co-Authored-By: Claude Fable 5 --- docs/realm/customization/mcp-server/index.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/realm/customization/mcp-server/index.md b/docs/realm/customization/mcp-server/index.md index 93ecffd75..ea9521d09 100644 --- a/docs/realm/customization/mcp-server/index.md +++ b/docs/realm/customization/mcp-server/index.md @@ -122,8 +122,8 @@ After connecting, the tool can access your OpenAPI documentation. {% numbered-list %} {% numbered-item %} In Cursor, open the command palette. - - macOS: `Command + Shift + P` - - Windows/Linux: `Ctrl + Shift + P` +- macOS: `Command + Shift + P` +- Windows/Linux: `Ctrl + Shift + P` {% /numbered-item %} {% numbered-item %} Type "Open MCP settings" in the command palette. @@ -248,8 +248,8 @@ In a Claude Desktop chat, ask a question that uses an MCP tool. {% numbered-list %} {% numbered-item %} In VS Code, open the command palette. - - macOS: `Command + Shift + P` - - Windows/Linux: `Ctrl + Shift + P` +- macOS: `Command + Shift + P` +- Windows/Linux: `Ctrl + Shift + P` {% /numbered-item %} {% numbered-item %} Type "MCP: Add Server" in the command palette. From 879079576ceb183e3027db395063743eb786ac35 Mon Sep 17 00:00:00 2001 From: Roman Marshevskyi Date: Thu, 27 Aug 2026 12:54:23 +0300 Subject: [PATCH 3/4] docs(other): exclude mcp-server page from markdownlint instead of re-indenting The 0-indent lists markdownlint wants break Markdoc tag parsing in the project build, so keep the original indentation and skip the file like other markdoc-heavy pages. --- .github/workflows/docs-tests.yaml | 1 + docs/realm/customization/mcp-server/index.md | 8 ++++---- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/.github/workflows/docs-tests.yaml b/.github/workflows/docs-tests.yaml index 2e92dc4b0..7397c29cd 100644 --- a/.github/workflows/docs-tests.yaml +++ b/.github/workflows/docs-tests.yaml @@ -27,3 +27,4 @@ jobs: !docs/realm/.templates/* !docs/realm/customization/add-color-mode.md !docs/realm/customization/eject-components/eject-components-tutorial/index.md + !docs/realm/customization/mcp-server/index.md diff --git a/docs/realm/customization/mcp-server/index.md b/docs/realm/customization/mcp-server/index.md index ea9521d09..93ecffd75 100644 --- a/docs/realm/customization/mcp-server/index.md +++ b/docs/realm/customization/mcp-server/index.md @@ -122,8 +122,8 @@ After connecting, the tool can access your OpenAPI documentation. {% numbered-list %} {% numbered-item %} In Cursor, open the command palette. -- macOS: `Command + Shift + P` -- Windows/Linux: `Ctrl + Shift + P` + - macOS: `Command + Shift + P` + - Windows/Linux: `Ctrl + Shift + P` {% /numbered-item %} {% numbered-item %} Type "Open MCP settings" in the command palette. @@ -248,8 +248,8 @@ In a Claude Desktop chat, ask a question that uses an MCP tool. {% numbered-list %} {% numbered-item %} In VS Code, open the command palette. -- macOS: `Command + Shift + P` -- Windows/Linux: `Ctrl + Shift + P` + - macOS: `Command + Shift + P` + - Windows/Linux: `Ctrl + Shift + P` {% /numbered-item %} {% numbered-item %} Type "MCP: Add Server" in the command palette. From 67243433f6bca428e13d1b3e49b515eaa1577aa6 Mon Sep 17 00:00:00 2001 From: Roman Marshevskyi Date: Thu, 27 Aug 2026 14:36:54 +0300 Subject: [PATCH 4/4] docs(blog): address PR review feedback - Use root-relative docs links in the CTA - Revert to api-specifications:openapi category (api-descriptions to be handled in a separate PR) - Dim the lead paragraph and add space below it - Highlight the steps tagline with TextGradient and add spacing - Use the theme CodeBlock with copy control for all code samples --- blog/agent-friendly-sdks.page.tsx | 143 ++++++++++++------------------ blog/metadata/blog-metadata.yaml | 6 -- 2 files changed, 58 insertions(+), 91 deletions(-) diff --git a/blog/agent-friendly-sdks.page.tsx b/blog/agent-friendly-sdks.page.tsx index 557d64847..1615ee80d 100644 --- a/blog/agent-friendly-sdks.page.tsx +++ b/blog/agent-friendly-sdks.page.tsx @@ -3,10 +3,12 @@ import styled from 'styled-components'; import { useThemeHooks } from '@redocly/theme/core/hooks'; import { Markdown } from '@redocly/theme/components/Markdown/Markdown'; +import { CodeBlock } from '@redocly/theme/components/CodeBlock/CodeBlock'; import type { Post } from '@redocly/marketing-pages/components/Blog/types.js'; import PostInfo from '@redocly/marketing-pages/components/Blog/PostInfo.js'; import { MediaBox } from '@redocly/marketing-pages/components/PositionItems/MediaBox.js'; +import { TextGradient } from '@redocly/marketing-pages/components/TextGradient/TextGradient.js'; import { Box } from '@redocly/marketing-pages/ui/Box.js'; import { RecentPosts } from '../@theme/components/Blog/RecentPosts'; @@ -24,7 +26,7 @@ export const frontmatter = { }, author: 'roman-marshevskyi', publishedDate: '2026-08-27', - categories: ['redocly:redocly-cli', 'api-descriptions:openapi', 'api-lifecycle:sdks'], + categories: ['redocly:redocly-cli', 'api-specifications:openapi', 'api-lifecycle:sdks'], }; export default function AgentFriendlySdksPost() { @@ -101,17 +103,24 @@ export default function AgentFriendlySdksPost() { The one you already have: OpenAPI 3.0, 3.1, 3.2, or Swagger 2.0. -
-                  # openapi.yaml
-                  {'\npaths:\n  /menu-items:\n    get:\n      operationId: '}
-                  listMenuItems
-                  {'\n  /orders/{orderId}:\n    get:\n      operationId: '}
-                  getOrderById
-                  {'\ncomponents:\n  securitySchemes:\n    BearerAuth:\n      type: '}
-                  http
-                  {'\n      scheme: '}
-                  bearer
-                
+ @@ -123,10 +132,11 @@ export default function AgentFriendlySdksPost() { No account, no config required. Flags or a redocly.yaml{' '} client block, your choice. -
-                  $ npx @redocly/cli@latest generate-client openapi.yaml{' '}
-                  --output src/client.ts
-                
+ @@ -137,28 +147,18 @@ export default function AgentFriendlySdksPost() { Every operation is a typed function; every name comes from the description. -
-                  import
-                  {' { configure, listMenuItems, getOrderById } '}
-                  from './client.js'
-                  {';\n\n'}
-                  configure
-                  {'({ auth: { bearer: token } }); '}
-                  // sent only where an operation requires it
-                  {'\n\n'}
-                  const
-                  {' menu  = '}
-                  await listMenuItems
-                  {'({ query: { limit: '}
-                  10
-                  {' } });\n'}
-                  const
-                  {' order = '}
-                  await getOrderById
-                  {'({ path: { orderId: '}
-                  'ord_01khr…'
-                  {' } });'}
-                
+ @@ -369,9 +369,11 @@ export default function AgentFriendlySdksPost() { stop reaching you. Eject gives you the ownership without the fork:

-
-            $ npx @redocly/cli@latest eject-generator python
-          
+

That copies the built-in generator into your repository as{' '} @@ -443,21 +445,18 @@ export default function AgentFriendlySdksPost() { -

-                $ npx @redocly/cli@latest generate-client openapi.yaml{' '}
-                --output src/client.ts
-              
+ Then import a function and call your API. The whole client is in the file you just generated. - - Command reference - - - Write a custom generator - + Command reference + Write a custom generator Runnable examples @@ -687,37 +686,9 @@ const BlogMediaBox = styled.div` const Lead = styled.p` font-size: 19px; - line-height: 1.6; -`; - -// The theme's Markdown wrapper styles 'pre' (background, text color, padding), -// so token colors here are picked for contrast on its light code-block background. -const Pre = styled.pre` - border-radius: 8px; - font-size: 13.5px; - tab-size: 2; - white-space: pre; -`; - -const TokC = styled.span` - color: #59636e; -`; - -const TokK = styled.span` - color: #cf222e; -`; - -const TokS = styled.span` - color: #116329; -`; - -const TokF = styled.span` - color: #8250df; -`; - -const TokFlag = styled.span` - color: #953800; - font-weight: 600; + line-height: 1.65; + color: var(--color-text-dimmed); + margin-bottom: 2em; `; const Steps = styled.div` @@ -772,11 +743,13 @@ const StepHint = styled.div` margin: -4px 0 8px; `; -const StepsTagline = styled.p` +const StepsTagline = styled(TextGradient)` + display: block; font-weight: 700; - font-size: 17px; + font-size: 22px; + line-height: 1.4; text-align: center; - margin: 22px 0 0; + margin: 48px 0 16px; `; const TableScroll = styled.div` diff --git a/blog/metadata/blog-metadata.yaml b/blog/metadata/blog-metadata.yaml index aaadea9f3..d4713625a 100644 --- a/blog/metadata/blog-metadata.yaml +++ b/blog/metadata/blog-metadata.yaml @@ -78,12 +78,6 @@ categories: - id: dependency-maps label: Dependency maps - - id: api-descriptions - label: API descriptions - subcategories: - - id: openapi - label: OpenAPI - - id: api-documentation label: API documentation subcategories: