From 13ce631f53e304798589b9674b6a9419f8da93d4 Mon Sep 17 00:00:00 2001 From: Roman Marshevskyi Date: Thu, 27 Aug 2026 12:42:29 +0300 Subject: [PATCH 01/14] 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 02/14] 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 03/14] 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 04/14] 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: From 2444a4d4b7663f123507faea331dcd0b6e9b61cd Mon Sep 17 00:00:00 2001 From: Roman Marshevskyi Date: Mon, 31 Aug 2026 11:55:25 +0300 Subject: [PATCH 05/14] Update blog/agent-friendly-sdks.page.tsx Co-authored-by: Illia Adamchuk --- blog/agent-friendly-sdks.page.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/blog/agent-friendly-sdks.page.tsx b/blog/agent-friendly-sdks.page.tsx index 1615ee80d..fe6031520 100644 --- a/blog/agent-friendly-sdks.page.tsx +++ b/blog/agent-friendly-sdks.page.tsx @@ -447,7 +447,7 @@ export default function AgentFriendlySdksPost() { From 3d80b85464a474c373790d9548add074ce3e6b61 Mon Sep 17 00:00:00 2001 From: Roman Marshevskyi Date: Mon, 31 Aug 2026 12:55:45 +0300 Subject: [PATCH 06/14] docs(blog): select frontmatter loader by route type instead of path regex Realm's markdown plugin stamps its routes with metadata.type === 'markdown', so branch on that rather than matching the .page.tsx filename shape. Also skip any post whose frontmatter cannot be loaded. --- @theme/utils/blog-post.js | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/@theme/utils/blog-post.js b/@theme/utils/blog-post.js index 563b97973..a5a23bac3 100644 --- a/@theme/utils/blog-post.js +++ b/@theme/utils/blog-post.js @@ -10,17 +10,19 @@ export const buildAndSortBlogPosts = async (postRoutes, context, outdir) => { const metadata = await transformMetadata(metadataContentRecord.data, context.fs.cwd, outdir); for (const route of postRoutes) { - // React blog posts export `frontmatter`; markdown posts use YAML frontmatter - const isReactPage = /\.page\.tsx?$/.test(route.fsPath); + // Markdown routes are stamped with metadata.type === 'markdown' by the Realm + // markdown plugin; other blog post routes are React pages, which export + // `frontmatter` instead of using YAML frontmatter. + const isMarkdownPost = route.metadata?.type === 'markdown'; const { data } = await context.cache.load( route.fsPath, - isReactPage ? 'react-frontmatter' : 'markdown-frontmatter', + isMarkdownPost ? 'markdown-frontmatter' : 'react-frontmatter', ); - const frontmatter = isReactPage ? data : data?.frontmatter; + const frontmatter = isMarkdownPost ? data?.frontmatter : data; if ( - (isReactPage && !frontmatter) || - frontmatter?.ignore === true || + !frontmatter || + frontmatter.ignore === true || (await context.isPathIgnored(route.fsPath)) ) { continue; From beb7fc02936fe763bacd9a61a254c00277845437 Mon Sep 17 00:00:00 2001 From: Roman Marshevskyi Date: Mon, 31 Aug 2026 14:40:38 +0300 Subject: [PATCH 07/14] docs(blog): convert client generator post to markdown and slim the PR MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Rewrite the post as a standard markdown blog post; the diagram is an SVG image in the body - Revert all theme changes (BlogPost template, plugin shared data, frontmatter loader) — the recent-posts fix moves to a separate PR - Address review feedback: complete the openapi.yaml snippet so the TypeScript example compiles against it, explain agent skills with the eject file layout, style the tagline as a quote, reword the caveat, and refine the hero diagram (bigger subtitle, no window dots, thinner accent-colored arrows) --- @theme/components/Blog/RecentPosts.tsx | 52 -- @theme/plugin.js | 32 +- @theme/templates/BlogPost.tsx | 64 +- @theme/utils/blog-post.js | 36 +- blog/agent-friendly-sdks.md | 198 ++++++ blog/agent-friendly-sdks.page.tsx | 922 ------------------------- blog/images/agent-friendly-sdks.svg | 62 ++ blog/images/cta-eclipse.svg | 16 - 8 files changed, 284 insertions(+), 1098 deletions(-) delete mode 100644 @theme/components/Blog/RecentPosts.tsx create mode 100644 blog/agent-friendly-sdks.md delete mode 100644 blog/agent-friendly-sdks.page.tsx create mode 100644 blog/images/agent-friendly-sdks.svg delete mode 100644 blog/images/cta-eclipse.svg diff --git a/@theme/components/Blog/RecentPosts.tsx b/@theme/components/Blog/RecentPosts.tsx deleted file mode 100644 index fa1c99e12..000000000 --- a/@theme/components/Blog/RecentPosts.tsx +++ /dev/null @@ -1,52 +0,0 @@ -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 4180912b0..5f3484125 100644 --- a/@theme/plugin.js +++ b/@theme/plugin.js @@ -7,7 +7,6 @@ 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) { @@ -27,11 +26,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, @@ -59,7 +58,7 @@ export default function themePlugin() { templateId: blogTemplateId, hasClientRoutes: true, }); - + const metadataContentRecord = await context.cache.load(BLOG_METADATA_PATH, 'yaml'); const categories = metadataContentRecord.data.categories || []; @@ -85,11 +84,10 @@ 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 @@ -100,14 +98,8 @@ 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 @@ -118,17 +110,11 @@ 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 5e4db51f6..cde0630f7 100644 --- a/@theme/templates/BlogPost.tsx +++ b/@theme/templates/BlogPost.tsx @@ -1,63 +1,3 @@ -import React from 'react'; -import styled from 'styled-components'; +import Page from '@redocly/marketing-pages/templates/BlogPost.js'; -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; -`; +export default Page; \ No newline at end of file diff --git a/@theme/utils/blog-post.js b/@theme/utils/blog-post.js index a5a23bac3..8ed216162 100644 --- a/@theme/utils/blog-post.js +++ b/@theme/utils/blog-post.js @@ -10,21 +10,11 @@ export const buildAndSortBlogPosts = async (postRoutes, context, outdir) => { const metadata = await transformMetadata(metadataContentRecord.data, context.fs.cwd, outdir); for (const route of postRoutes) { - // Markdown routes are stamped with metadata.type === 'markdown' by the Realm - // markdown plugin; other blog post routes are React pages, which export - // `frontmatter` instead of using YAML frontmatter. - const isMarkdownPost = route.metadata?.type === 'markdown'; - const { data } = await context.cache.load( - route.fsPath, - isMarkdownPost ? 'markdown-frontmatter' : 'react-frontmatter', - ); - const frontmatter = isMarkdownPost ? data?.frontmatter : data; + const { + data: { content, frontmatter }, + } = await context.cache.load(route.fsPath, 'markdown-frontmatter'); - if ( - !frontmatter || - frontmatter.ignore === true || - (await context.isPathIgnored(route.fsPath)) - ) { + if (frontmatter?.ignore === true || (await context.isPathIgnored(route.fsPath))) { continue; } @@ -36,15 +26,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 + } }; } }) @@ -82,12 +72,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.md b/blog/agent-friendly-sdks.md new file mode 100644 index 000000000..af9606d61 --- /dev/null +++ b/blog/agent-friendly-sdks.md @@ -0,0 +1,198 @@ +--- +template: ../@theme/templates/BlogPost +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-specifications:openapi + - api-lifecycle:sdks +--- + +# Open-source, agent-friendly SDKs and tooling from OpenAPI description + +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. + +![Diagram: one OpenAPI description flows through redocly generate-client into SDKs, schemas and mocks, a CLI, and docs](./images/agent-friendly-sdks.svg) + +Meet `generate-client`: a new command in the [Redocly CLI](https://github.com/Redocly/redocly-cli), powered by a new package, [`@redocly/client-generator`](https://github.com/Redocly/redocly-cli/tree/main/packages/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. + +```yaml +paths: + /menu-items: + get: + operationId: listMenuItems + parameters: + - name: limit + in: query + schema: + type: integer + /orders/{orderId}: + get: + operationId: getOrderById + security: + - BearerAuth: [] + parameters: + - name: orderId + in: path + required: true + schema: + type: string +components: + securitySchemes: + BearerAuth: + type: http + scheme: bearer +``` + +### 2. Run one command + +No account, no config file — every option has a default. +Customize with flags or a `redocly.yaml` `client` block when you need to. + +```bash +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. + +```typescript +import { configure, listMenuItems, getOrderById } from './client.js'; + +configure({ auth: { bearer: token } }); // sent only where an operation requires it + +const menu = await listMenuItems({ query: { limit: 10 } }); +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.** A skill is a short instruction file that AI agents (such as Claude Code) load before touching related code: it states what a piece of code is for, the rules it must follow, and how to change it safely. Every generator carries its own design document as a skill, and ejecting a generator drops both into your repo beside the generator source: + + ```text + redocly eject-generator zod + + generators/zod/… # the generator source, now yours + generators/AGENTS.md # pointer that leads agents to the skills + .claude/skills/client-generators/SKILL.md # the shared authoring guide + .claude/skills/zod-generator/SKILL.md # why this generator is built the way it is + ``` + + 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 whole instruction your agents need is one sentence: _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: + +| Generator | Artifact | Consumer | +| --- | --- | --- | +| `typescript` (default), `python`, `go`, `php` | the full typed client, in that language | calling your API from any stack | +| `zod` | Zod schemas + validation middleware | runtime contract checks | +| `tanstack-query`, `swr` | query and mutation factories, hooks | React, Vue, Svelte, Solid data fetching | +| `mock` | MSW v2 handlers + typed data factories | tests and demos, offline and deterministic | +| `transformers` | `Date` converters | ISO strings → `Date`, paired with `--date-type Date` | +| `cli` | a bin-ready command-line interface | scripts, CI, agents | +| your own | anything | the 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: + +```bash +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. + +A quick caveat: the command is still experimental, so flags and output may change between releases — pin your CLI version. +The code it generates is strict-TypeScript clean, exhaustively tested, and already carrying Redocly's production traffic. + +## Try it + +One command, no account, runs entirely on your machine: + +```bash +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](/docs/cli/commands/generate-client) +- [Write a custom generator](/docs/cli/guides/customize-client-generation) +- [Runnable examples](https://github.com/Redocly/redocly-cli/tree/main/tests/e2e/generate-client/examples) +- [GitHub](https://github.com/Redocly/redocly-cli) diff --git a/blog/agent-friendly-sdks.page.tsx b/blog/agent-friendly-sdks.page.tsx deleted file mode 100644 index fe6031520..000000000 --- a/blog/agent-friendly-sdks.page.tsx +++ /dev/null @@ -1,922 +0,0 @@ -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 { 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'; - -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-specifications: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. - - -
-
- - - 2 -
- Run one command - - No account, no config required. Flags or a redocly.yaml{' '} - client block, your choice. - - -
-
- - - 3 -
- Call your API - - Every operation is a typed function; every name comes from the description. - - -
-
-
- -

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: -

- - - -

- 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. - - - - - - 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.65; - color: var(--color-text-dimmed); - margin-bottom: 2em; -`; - -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(TextGradient)` - display: block; - font-weight: 700; - font-size: 22px; - line-height: 1.4; - text-align: center; - margin: 48px 0 16px; -`; - -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/agent-friendly-sdks.svg b/blog/images/agent-friendly-sdks.svg new file mode 100644 index 000000000..eff9b66a3 --- /dev/null +++ b/blog/images/agent-friendly-sdks.svg @@ -0,0 +1,62 @@ + + + + + + + + + + + + + + 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 + + diff --git a/blog/images/cta-eclipse.svg b/blog/images/cta-eclipse.svg deleted file mode 100644 index bc7ce0c7c..000000000 --- a/blog/images/cta-eclipse.svg +++ /dev/null @@ -1,16 +0,0 @@ - - - - - - - - - - - - - - - - From 674d74843a49730121f8018651aa76d668a56fd9 Mon Sep 17 00:00:00 2001 From: Roman Marshevskyi Date: Mon, 31 Aug 2026 14:41:43 +0300 Subject: [PATCH 08/14] docs(blog): revert markdownlint workflow change The mcp-server markdownlint exclusion belongs in its own PR. --- .github/workflows/docs-tests.yaml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/docs-tests.yaml b/.github/workflows/docs-tests.yaml index 7397c29cd..2e92dc4b0 100644 --- a/.github/workflows/docs-tests.yaml +++ b/.github/workflows/docs-tests.yaml @@ -27,4 +27,3 @@ 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 From c88718a23f836582f62a51614f410201c1394e3c Mon Sep 17 00:00:00 2001 From: Roman Marshevskyi Date: Mon, 31 Aug 2026 16:11:12 +0300 Subject: [PATCH 09/14] docs(blog): bump publishedDate to 2026-08-31 --- blog/agent-friendly-sdks.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/blog/agent-friendly-sdks.md b/blog/agent-friendly-sdks.md index af9606d61..b83a49866 100644 --- a/blog/agent-friendly-sdks.md +++ b/blog/agent-friendly-sdks.md @@ -6,7 +6,7 @@ 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' +publishedDate: '2026-08-31' categories: - redocly:redocly-cli - api-specifications:openapi From 8ef3d24238e2ef6955676017c4b8332ec6c7c399 Mon Sep 17 00:00:00 2001 From: Roman Marshevskyi Date: Tue, 1 Sep 2026 13:38:04 +0300 Subject: [PATCH 10/14] docs(blog): bump publishedDate to 2026-09-01 --- blog/agent-friendly-sdks.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/blog/agent-friendly-sdks.md b/blog/agent-friendly-sdks.md index b83a49866..a8a69f6b9 100644 --- a/blog/agent-friendly-sdks.md +++ b/blog/agent-friendly-sdks.md @@ -6,7 +6,7 @@ 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-31' +publishedDate: '2026-09-01' categories: - redocly:redocly-cli - api-specifications:openapi From 317969cb0686b29ece5a79453283e5b25fd4ed2c Mon Sep 17 00:00:00 2001 From: Roman Marshevskyi Date: Tue, 1 Sep 2026 16:48:30 +0300 Subject: [PATCH 11/14] docs(blog): use client instance style in the example and scope the zero-deps claim - Show the exported client object (client.auth.bearer, client.listMenuItems) so it is clear the auth is stored on the client the calls go through - Zero runtime dependencies / fetch applies to the TypeScript client; other languages use their own HTTP layer (httpx, stdlib, curl) --- blog/agent-friendly-sdks.md | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/blog/agent-friendly-sdks.md b/blog/agent-friendly-sdks.md index a8a69f6b9..05aafeb7c 100644 --- a/blog/agent-friendly-sdks.md +++ b/blog/agent-friendly-sdks.md @@ -24,7 +24,8 @@ Generated code is the cheapest, safest code an agent can ship, so we built a gen Meet `generate-client`: a new command in the [Redocly CLI](https://github.com/Redocly/redocly-cli), powered by a new package, [`@redocly/client-generator`](https://github.com/Redocly/redocly-cli/tree/main/packages/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 SDKs are fully featured: auth, retries, middleware, pagination iterators, typed Server-Sent Events, query-string serialization, and multipart uploads, with no dependencies beyond each language's own HTTP layer. +The TypeScript client is built on web-standard `fetch`, `AbortController`, and `URLSearchParams` — **zero runtime dependencies**, 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 @@ -72,15 +73,15 @@ 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. +Every operation is a typed method on the generated client; every name comes from the description. ```typescript -import { configure, listMenuItems, getOrderById } from './client.js'; +import { client } from './client.js'; -configure({ auth: { bearer: token } }); // sent only where an operation requires it +client.auth.bearer(token); // stored on this client; sent only with operations whose security requires it -const menu = await listMenuItems({ query: { limit: 10 } }); -const order = await getOrderById({ path: { orderId: 'ord_01khr…' } }); +const menu = await client.listMenuItems({ query: { limit: 10 } }); +const order = await client.getOrderById({ path: { orderId: 'ord_01khr…' } }); ``` That's the whole client. From 3bb5fdaffe1e9d129c3602b1207ed0444ab2f13a Mon Sep 17 00:00:00 2001 From: Roman Marshevskyi Date: Tue, 1 Sep 2026 17:18:55 +0300 Subject: [PATCH 12/14] docs(blog): scope the zero-deps claim in frontmatter descriptions too --- blog/agent-friendly-sdks.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/blog/agent-friendly-sdks.md b/blog/agent-friendly-sdks.md index 05aafeb7c..e6e6521af 100644 --- a/blog/agent-friendly-sdks.md +++ b/blog/agent-friendly-sdks.md @@ -1,10 +1,10 @@ --- template: ../@theme/templates/BlogPost 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." +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, no dependencies beyond each language's HTTP layer, 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." + 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, no dependencies beyond each language's HTTP layer, and built for AI agents." author: roman-marshevskyi publishedDate: '2026-09-01' categories: From 5e6c2cb856a464349d1132932fe1f0c855f06608 Mon Sep 17 00:00:00 2001 From: Roman Marshevskyi Date: Tue, 1 Sep 2026 17:20:41 +0300 Subject: [PATCH 13/14] docs(blog): use npx @redocly/cli in the hero diagram command --- blog/images/agent-friendly-sdks.svg | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/blog/images/agent-friendly-sdks.svg b/blog/images/agent-friendly-sdks.svg index eff9b66a3..c4742df3e 100644 --- a/blog/images/agent-friendly-sdks.svg +++ b/blog/images/agent-friendly-sdks.svg @@ -29,11 +29,11 @@ - + $ - redocly - generate-client - → typed · zero deps · yours + npx @redocly/cli + generate-client + → typed · zero deps · yours From 6bc71abe647502895a874ec3687c68f01d23dcc5 Mon Sep 17 00:00:00 2001 From: Roman Marshevskyi Date: Tue, 1 Sep 2026 18:43:58 +0300 Subject: [PATCH 14/14] docs(blog): remove duplicate H1 (template renders the title) and center diagram command --- blog/agent-friendly-sdks.md | 2 -- blog/images/agent-friendly-sdks.svg | 8 ++++---- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/blog/agent-friendly-sdks.md b/blog/agent-friendly-sdks.md index e6e6521af..5744461d9 100644 --- a/blog/agent-friendly-sdks.md +++ b/blog/agent-friendly-sdks.md @@ -13,8 +13,6 @@ categories: - api-lifecycle:sdks --- -# Open-source, agent-friendly SDKs and tooling from OpenAPI description - 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. diff --git a/blog/images/agent-friendly-sdks.svg b/blog/images/agent-friendly-sdks.svg index c4742df3e..f275ed994 100644 --- a/blog/images/agent-friendly-sdks.svg +++ b/blog/images/agent-friendly-sdks.svg @@ -30,10 +30,10 @@ - $ - npx @redocly/cli - generate-client - → typed · zero deps · yours + $ + npx @redocly/cli + generate-client + → typed · zero deps · yours