diff --git a/services/clojars/clojars-base.js b/services/clojars/clojars-base.js
index 15996eebb37974f38e8e828ebb6f91725efbcbfe..a83cc320552178c64c1223a77d7baa71ba422a43 100644
--- a/services/clojars/clojars-base.js
+++ b/services/clojars/clojars-base.js
@@ -19,4 +19,7 @@ class BaseClojarsService extends BaseJsonService {
   }
 }
 
-export { BaseClojarsService }
+const description =
+  '[Clojars](https://clojars.org/) is a repository for Clojure libraries'
+
+export { BaseClojarsService, description }
diff --git a/services/clojars/clojars-downloads.service.js b/services/clojars/clojars-downloads.service.js
index 2c5c693ccbe37958204f9e07c288d9c638fcad3b..d3672bec9ec86d809fad4c33208840afa9392a4b 100644
--- a/services/clojars/clojars-downloads.service.js
+++ b/services/clojars/clojars-downloads.service.js
@@ -1,6 +1,6 @@
 import { pathParams } from '../index.js'
 import { renderDownloadsBadge } from '../downloads.js'
-import { BaseClojarsService } from './clojars-base.js'
+import { BaseClojarsService, description } from './clojars-base.js'
 
 export default class ClojarsDownloads extends BaseClojarsService {
   static category = 'downloads'
@@ -10,6 +10,7 @@ export default class ClojarsDownloads extends BaseClojarsService {
     '/clojars/dt/{clojar}': {
       get: {
         summary: 'Clojars Downloads',
+        description,
         parameters: pathParams({
           name: 'clojar',
           example: 'prismic',
diff --git a/services/clojars/clojars-version.service.js b/services/clojars/clojars-version.service.js
index 344eddc90110dbb1f15a71e5f27ac4b9525ea770..2b030a09da1ca16fc37152fb57c6a61eaf8055fc 100644
--- a/services/clojars/clojars-version.service.js
+++ b/services/clojars/clojars-version.service.js
@@ -1,7 +1,7 @@
 import Joi from 'joi'
 import { version as versionColor } from '../color-formatters.js'
-import { redirector } from '../index.js'
-import { BaseClojarsService } from './clojars-base.js'
+import { redirector, pathParam, queryParam } from '../index.js'
+import { BaseClojarsService, description } from './clojars-base.js'
 
 const queryParamSchema = Joi.object({
   include_prereleases: Joi.equal(''),
@@ -11,19 +11,25 @@ class ClojarsVersionService extends BaseClojarsService {
   static category = 'version'
   static route = { base: 'clojars/v', pattern: ':clojar+', queryParamSchema }
 
-  static examples = [
-    {
-      title: 'Clojars Version',
-      namedParams: { clojar: 'prismic' },
-      staticPreview: this.render({ clojar: 'clojar', version: '1.2' }),
+  static openApi = {
+    '/clojars/v/{clojar}': {
+      get: {
+        summary: 'Clojars Version',
+        description,
+        parameters: [
+          pathParam({
+            name: 'clojar',
+            example: 'prismic',
+          }),
+          queryParam({
+            name: 'include_prereleases',
+            schema: { type: 'boolean' },
+            example: null,
+          }),
+        ],
+      },
     },
-    {
-      title: 'Clojars Version (including pre-releases)',
-      namedParams: { clojar: 'prismic' },
-      queryParams: { include_prereleases: null },
-      staticPreview: this.render({ clojar: 'clojar', version: '1.2' }),
-    },
-  ]
+  }
 
   static defaultBadgeData = { label: 'clojars' }
 
diff --git a/services/f-droid/f-droid.service.js b/services/f-droid/f-droid.service.js
index 66af156f093a3a1656e406c1ac0cccb2bcef9afe..02f5ab3ee9872ede88906b8107cbebe78fbc157c 100644
--- a/services/f-droid/f-droid.service.js
+++ b/services/f-droid/f-droid.service.js
@@ -5,7 +5,7 @@ import {
 } from '../validators.js'
 import { addv } from '../text-formatters.js'
 import { version as versionColor } from '../color-formatters.js'
-import { BaseJsonService, NotFound } from '../index.js'
+import { BaseJsonService, NotFound, pathParam, queryParam } from '../index.js'
 
 const schema = Joi.object({
   packageName: Joi.string().required(),
@@ -23,21 +23,26 @@ const queryParamSchema = Joi.object({
 export default class FDroid extends BaseJsonService {
   static category = 'version'
   static route = { base: 'f-droid/v', pattern: ':appId', queryParamSchema }
-  static examples = [
-    {
-      title: 'F-Droid',
-      namedParams: { appId: 'org.thosp.yourlocalweather' },
-      staticPreview: this.render({ version: '1.0' }),
-      keywords: ['fdroid', 'android', 'app'],
-    },
-    {
-      title: 'F-Droid (including pre-releases)',
-      namedParams: { appId: 'org.dystopia.email' },
-      queryParams: { include_prereleases: null },
-      staticPreview: this.render({ version: '1.2.1' }),
-      keywords: ['fdroid', 'android', 'app'],
+  static openApi = {
+    '/f-droid/v/{appId}': {
+      get: {
+        summary: 'F-Droid Version',
+        description:
+          '[F-Droid](https://f-droid.org/) is a catalogue of Open Source Android apps',
+        parameters: [
+          pathParam({
+            name: 'appId',
+            example: 'org.dystopia.email',
+          }),
+          queryParam({
+            name: 'include_prereleases',
+            schema: { type: 'boolean' },
+            example: null,
+          }),
+        ],
+      },
     },
-  ]
+  }
 
   static defaultBadgeData = { label: 'f-droid' }
 
diff --git a/services/packagist/packagist-base.js b/services/packagist/packagist-base.js
index 72b18bf391b92242eb42243eb50ab9ce72eb182f..c47ebdca4e6ef317e6ba847e8e4cbff16c87736b 100644
--- a/services/packagist/packagist-base.js
+++ b/services/packagist/packagist-base.js
@@ -15,7 +15,6 @@ const packageSchema = Joi.array().items(
 const allVersionsSchema = Joi.object({
   packages: Joi.object().pattern(/^/, packageSchema).required(),
 }).required()
-const keywords = ['PHP']
 
 class BasePackagistService extends BaseJsonService {
   /**
@@ -164,22 +163,25 @@ class BasePackagistService extends BaseJsonService {
     return versions.filter(version => version.version === release)[0]
   }
 }
+
+const description = `<p>
+  <a href="https://packagist.org/">Packagist</a> is a registry for PHP packages which can be installed with Composer.
+</p>`
+
 const customServerDocumentationFragment = `<p>
-        Note that only network-accessible packagist.org and other self-hosted Packagist instances are supported.
-    </p>
-    `
+    Note that only network-accessible packagist.org and other self-hosted Packagist instances are supported.
+</p>`
 
 const cacheDocumentationFragment = `<p>
-      Displayed data may be slightly outdated.
-      Due to performance reasons, data fetched from packagist JSON API is cached for twelve hours on packagist infrastructure.
-      For more information please refer to <a target="_blank" href="https://packagist.org/apidoc#get-package-data">official packagist documentation</a>.
-  </p>
-  `
+  Displayed data may be slightly outdated.
+  Due to performance reasons, data fetched from packagist JSON API is cached for twelve hours on packagist infrastructure.
+  For more information please refer to <a target="_blank" href="https://packagist.org/apidoc#get-package-data">official packagist documentation</a>.
+</p>`
 
 export {
   allVersionsSchema,
-  keywords,
   BasePackagistService,
+  description,
   customServerDocumentationFragment,
   cacheDocumentationFragment,
 }
diff --git a/services/packagist/packagist-dependency-version.service.js b/services/packagist/packagist-dependency-version.service.js
index e772510876f5783a88ea1ccaab0dc97a0b38224e..7862aad4ee7227595ed96a9f8f2b398373b6dd1d 100644
--- a/services/packagist/packagist-dependency-version.service.js
+++ b/services/packagist/packagist-dependency-version.service.js
@@ -1,10 +1,11 @@
 import Joi from 'joi'
 import { optionalUrl } from '../validators.js'
-import { NotFound } from '../index.js'
+import { NotFound, pathParam, queryParam } from '../index.js'
 import {
   allVersionsSchema,
   BasePackagistService,
   customServerDocumentationFragment,
+  description,
 } from './packagist-base.js'
 
 const queryParamSchema = Joi.object({
@@ -21,63 +22,39 @@ export default class PackagistDependencyVersion extends BasePackagistService {
     queryParamSchema,
   }
 
-  static examples = [
-    {
-      title: 'Packagist Dependency Version',
-      namedParams: {
-        user: 'symfony',
-        repo: 'symfony',
-        dependency: 'twig/twig',
+  static openApi = {
+    '/packagist/dependency-v/{user}/{repo}/{dependency}': {
+      get: {
+        summary: 'Packagist Dependency Version',
+        description,
+        parameters: [
+          pathParam({
+            name: 'user',
+            example: 'guzzlehttp',
+          }),
+          pathParam({
+            name: 'repo',
+            example: 'guzzle',
+          }),
+          pathParam({
+            name: 'dependency',
+            example: 'php',
+            description:
+              '`dependency` can be a PHP package like `twig/twig` or a platform/extension like `php` or `ext-xml`',
+          }),
+          queryParam({
+            name: 'version',
+            example: 'v2.8.0',
+          }),
+          queryParam({
+            name: 'server',
+            description: customServerDocumentationFragment,
+            example: 'https://packagist.org',
+          }),
+        ],
       },
-      staticPreview: this.render({
-        dependency: 'twig/twig',
-        dependencyVersion: '2.13|^3.0.4',
-      }),
     },
-    {
-      title: 'Packagist Dependency Version (specify version)',
-      namedParams: {
-        user: 'symfony',
-        repo: 'symfony',
-        dependency: 'twig/twig',
-      },
-      queryParams: {
-        version: 'v2.8.0',
-      },
-      staticPreview: this.render({
-        dependency: 'twig/twig',
-        dependencyVersion: '1.12',
-      }),
-    },
-    {
-      title: 'Packagist Dependency Version (custom server)',
-      namedParams: {
-        user: 'symfony',
-        repo: 'symfony',
-        dependency: 'twig/twig',
-      },
-      queryParams: {
-        server: 'https://packagist.org',
-      },
-      staticPreview: this.render({
-        dependency: 'twig/twig',
-        dependencyVersion: '2.13|^3.0.4',
-      }),
-      documentation: customServerDocumentationFragment,
-    },
-    {
-      title: 'Packagist PHP Version',
-      namedParams: {
-        user: 'symfony',
-        repo: 'symfony',
-        dependency: 'php',
-      },
-      staticPreview: this.render({
-        dependency: 'php',
-        dependencyVersion: '^7.1.3',
-      }),
-    },
-  ]
+  }
 
   static defaultBadgeData = {
     label: 'dependency version',
diff --git a/services/packagist/packagist-downloads.service.js b/services/packagist/packagist-downloads.service.js
index 0215f40c624bc813796f9a894e0bdae9757b3b86..92e2da657df62b8d3bbe2892cf5e4a8f2fa12ca8 100644
--- a/services/packagist/packagist-downloads.service.js
+++ b/services/packagist/packagist-downloads.service.js
@@ -1,11 +1,12 @@
 import Joi from 'joi'
 import { renderDownloadsBadge } from '../downloads.js'
 import { optionalUrl } from '../validators.js'
+import { pathParam, queryParam } from '../index.js'
 import {
-  keywords,
   BasePackagistService,
   customServerDocumentationFragment,
   cacheDocumentationFragment,
+  description,
 } from './packagist-base.js'
 
 const periodMap = {
@@ -45,38 +46,34 @@ export default class PackagistDownloads extends BasePackagistService {
     queryParamSchema,
   }
 
-  static examples = [
-    {
-      title: 'Packagist Downloads',
-      namedParams: {
-        interval: 'dm',
-        user: 'doctrine',
-        repo: 'orm',
+  static openApi = {
+    '/packagist/{interval}/{user}/{repo}': {
+      get: {
+        summary: 'Packagist Downloads',
+        description: description + cacheDocumentationFragment,
+        parameters: [
+          pathParam({
+            name: 'interval',
+            example: 'dm',
+            schema: { type: 'string', enum: this.getEnum('interval') },
+          }),
+          pathParam({
+            name: 'user',
+            example: 'guzzlehttp',
+          }),
+          pathParam({
+            name: 'repo',
+            example: 'guzzle',
+          }),
+          queryParam({
+            name: 'server',
+            description: customServerDocumentationFragment,
+            example: 'https://packagist.org',
+          }),
+        ],
       },
-      staticPreview: renderDownloadsBadge({
-        downloads: 1000000,
-        interval: 'month',
-      }),
-      keywords,
-      documentation: cacheDocumentationFragment,
     },
-    {
-      title: 'Packagist Downloads (custom server)',
-      namedParams: {
-        interval: 'dm',
-        user: 'doctrine',
-        repo: 'orm',
-      },
-      staticPreview: renderDownloadsBadge({
-        downloads: 1000000,
-        interval: 'month',
-      }),
-      queryParams: { server: 'https://packagist.org' },
-      keywords,
-      documentation:
-        customServerDocumentationFragment + cacheDocumentationFragment,
-    },
-  ]
+  }
 
   static defaultBadgeData = { label: 'downloads' }
 
diff --git a/services/packagist/packagist-license.service.js b/services/packagist/packagist-license.service.js
index 0511d4cbc912ad8963e14ed11613bbc210ec23e5..b3c644d7e55f48fb1ad3ec38bf5028e05b00c2ef 100644
--- a/services/packagist/packagist-license.service.js
+++ b/services/packagist/packagist-license.service.js
@@ -1,11 +1,11 @@
 import Joi from 'joi'
 import { renderLicenseBadge } from '../licenses.js'
 import { optionalUrl } from '../validators.js'
-import { NotFound } from '../index.js'
+import { NotFound, pathParam, queryParam } from '../index.js'
 import {
-  keywords,
   BasePackagistService,
   customServerDocumentationFragment,
+  description,
 } from './packagist-base.js'
 
 const packageSchema = Joi.array()
@@ -34,22 +34,29 @@ export default class PackagistLicense extends BasePackagistService {
     queryParamSchema,
   }
 
-  static examples = [
-    {
-      title: 'Packagist License',
-      namedParams: { user: 'doctrine', repo: 'orm' },
-      staticPreview: renderLicenseBadge({ license: 'MIT' }),
-      keywords,
+  static openApi = {
+    '/packagist/l/{user}/{repo}': {
+      get: {
+        summary: 'Packagist License',
+        description,
+        parameters: [
+          pathParam({
+            name: 'user',
+            example: 'guzzlehttp',
+          }),
+          pathParam({
+            name: 'repo',
+            example: 'guzzle',
+          }),
+          queryParam({
+            name: 'server',
+            description: customServerDocumentationFragment,
+            example: 'https://packagist.org',
+          }),
+        ],
+      },
     },
-    {
-      title: 'Packagist License (custom server)',
-      namedParams: { user: 'doctrine', repo: 'orm' },
-      queryParams: { server: 'https://packagist.org' },
-      staticPreview: renderLicenseBadge({ license: 'MIT' }),
-      keywords,
-      documentation: customServerDocumentationFragment,
-    },
-  ]
+  }
 
   static defaultBadgeData = {
     label: 'license',
diff --git a/services/packagist/packagist-stars.service.js b/services/packagist/packagist-stars.service.js
index 2b3820ffbcddc8a749e91cfafa8d63aa097bb510..5c32849aa07a4f80b78d22e89ee43c7c56d0bee6 100644
--- a/services/packagist/packagist-stars.service.js
+++ b/services/packagist/packagist-stars.service.js
@@ -1,11 +1,12 @@
 import Joi from 'joi'
 import { metric } from '../text-formatters.js'
 import { nonNegativeInteger, optionalUrl } from '../validators.js'
+import { pathParam, queryParam } from '../index.js'
 import {
-  keywords,
   BasePackagistService,
   customServerDocumentationFragment,
   cacheDocumentationFragment,
+  description,
 } from './packagist-base.js'
 
 const schema = Joi.object({
@@ -27,34 +28,29 @@ export default class PackagistStars extends BasePackagistService {
     queryParamSchema,
   }
 
-  static examples = [
-    {
-      title: 'Packagist Stars',
-      namedParams: {
-        user: 'guzzlehttp',
-        repo: 'guzzle',
+  static openApi = {
+    '/packagist/stars/{user}/{repo}': {
+      get: {
+        summary: 'Packagist Stars',
+        description: description + cacheDocumentationFragment,
+        parameters: [
+          pathParam({
+            name: 'user',
+            example: 'guzzlehttp',
+          }),
+          pathParam({
+            name: 'repo',
+            example: 'guzzle',
+          }),
+          queryParam({
+            name: 'server',
+            description: customServerDocumentationFragment,
+            example: 'https://packagist.org',
+          }),
+        ],
       },
-      staticPreview: this.render({
-        stars: 1000,
-      }),
-      keywords,
-      documentation: cacheDocumentationFragment,
     },
-    {
-      title: 'Packagist Stars (custom server)',
-      namedParams: {
-        user: 'guzzlehttp',
-        repo: 'guzzle',
-      },
-      staticPreview: this.render({
-        stars: 1000,
-      }),
-      queryParams: { server: 'https://packagist.org' },
-      keywords,
-      documentation:
-        customServerDocumentationFragment + cacheDocumentationFragment,
-    },
-  ]
+  }
 
   static defaultBadgeData = {
     label: 'stars',
diff --git a/services/packagist/packagist-version.service.js b/services/packagist/packagist-version.service.js
index ae78b0e4049788fbab325a4d55e99e70a8545974..aafd1256c1535aaf20ea92dc72340a98f819f3c0 100644
--- a/services/packagist/packagist-version.service.js
+++ b/services/packagist/packagist-version.service.js
@@ -1,11 +1,11 @@
 import Joi from 'joi'
 import { renderVersionBadge } from '../version.js'
 import { optionalUrl } from '../validators.js'
-import { redirector } from '../index.js'
+import { redirector, pathParam, queryParam } from '../index.js'
 import {
-  keywords,
   BasePackagistService,
   customServerDocumentationFragment,
+  description,
 } from './packagist-base.js'
 
 const packageSchema = Joi.array().items(
@@ -32,40 +32,34 @@ class PackagistVersion extends BasePackagistService {
     queryParamSchema,
   }
 
-  static examples = [
-    {
-      title: 'Packagist Version',
-      namedParams: {
-        user: 'symfony',
-        repo: 'symfony',
+  static openApi = {
+    '/packagist/v/{user}/{repo}': {
+      get: {
+        summary: 'Packagist Version',
+        description,
+        parameters: [
+          pathParam({
+            name: 'user',
+            example: 'symfony',
+          }),
+          pathParam({
+            name: 'repo',
+            example: 'symfony',
+          }),
+          queryParam({
+            name: 'include_prereleases',
+            schema: { type: 'boolean' },
+            example: null,
+          }),
+          queryParam({
+            name: 'server',
+            description: customServerDocumentationFragment,
+            example: 'https://packagist.org',
+          }),
+        ],
       },
-      staticPreview: renderVersionBadge({ version: '4.2.2' }),
-      keywords,
     },
-    {
-      title: 'Packagist Version (including pre-releases)',
-      namedParams: {
-        user: 'symfony',
-        repo: 'symfony',
-      },
-      queryParams: { include_prereleases: null },
-      staticPreview: renderVersionBadge({ version: '4.3-dev' }),
-      keywords,
-    },
-    {
-      title: 'Packagist Version (custom server)',
-      namedParams: {
-        user: 'symfony',
-        repo: 'symfony',
-      },
-      queryParams: {
-        server: 'https://packagist.org',
-      },
-      staticPreview: renderVersionBadge({ version: '4.2.2' }),
-      keywords,
-      documentation: customServerDocumentationFragment,
-    },
-  ]
+  }
 
   static defaultBadgeData = {
     label: 'packagist',
diff --git a/services/piwheels/piwheels-version.service.js b/services/piwheels/piwheels-version.service.js
index 96b815e0c4353d6480eda7c5eafb7cd387a511a3..49f9397fc014fed4621049546f5b1710a9ac1582 100644
--- a/services/piwheels/piwheels-version.service.js
+++ b/services/piwheels/piwheels-version.service.js
@@ -1,5 +1,10 @@
 import Joi from 'joi'
-import { BaseJsonService, InvalidResponse } from '../index.js'
+import {
+  BaseJsonService,
+  InvalidResponse,
+  pathParam,
+  queryParam,
+} from '../index.js'
 import { renderVersionBadge } from '../version.js'
 import { pep440VersionColor } from '../color-formatters.js'
 
@@ -20,30 +25,31 @@ const queryParamSchema = Joi.object({
   include_prereleases: Joi.equal(''),
 }).required()
 
-const keywords = ['python', 'arm', 'raspberry pi']
-
 export default class PiWheelsVersion extends BaseJsonService {
   static category = 'version'
 
   static route = { base: 'piwheels/v', pattern: ':wheel', queryParamSchema }
 
-  static examples = [
-    {
-      title: 'piwheels',
-      namedParams: { wheel: 'numpy' },
-      staticPreview: this.render({ version: '1.22.2' }),
-      keywords,
-    },
-    {
-      title: 'piwheels (including prereleases)',
-      namedParams: { wheel: 'flask' },
-      queryParams: {
-        include_prereleases: null,
+  static openApi = {
+    '/piwheels/v/{wheel}': {
+      get: {
+        summary: 'PiWheels Version',
+        description:
+          '[PiWheels](https://www.piwheels.org/) is a Python package repository providing Arm platform wheels for the Raspberry Pi',
+        parameters: [
+          pathParam({
+            name: 'wheel',
+            example: 'flask',
+          }),
+          queryParam({
+            name: 'include_prereleases',
+            schema: { type: 'boolean' },
+            example: null,
+          }),
+        ],
       },
-      staticPreview: this.render({ version: '2.0.0rc2' }),
-      keywords,
     },
-  ]
+  }
 
   static defaultBadgeData = { label: 'piwheels' }
 
diff --git a/services/vpm/vpm-version.service.js b/services/vpm/vpm-version.service.js
index 6b5145eb1bd85d63cd605551de51c2bb12d263f3..3e486ac4587313276b49d6ef69312e7ec78ff997 100644
--- a/services/vpm/vpm-version.service.js
+++ b/services/vpm/vpm-version.service.js
@@ -1,7 +1,7 @@
 import Joi from 'joi'
 import { optionalUrl } from '../validators.js'
 import { latest, renderVersionBadge } from '../version.js'
-import { BaseJsonService, NotFound } from '../index.js'
+import { BaseJsonService, NotFound, pathParam, queryParam } from '../index.js'
 
 const queryParamSchema = Joi.object({
   repository_url: optionalUrl.required(),
@@ -28,29 +28,30 @@ export default class VpmVersion extends BaseJsonService {
     queryParamSchema,
   }
 
-  static examples = [
-    {
-      title: 'VPM Package Version',
-      namedParams: {
-        packageId: 'com.vrchat.udonsharp',
+  static openApi = {
+    '/vpm/v/{packageId}': {
+      get: {
+        summary: 'VPM Package Version',
+        description: 'VPM is the VRChat Package Manager',
+        parameters: [
+          pathParam({
+            name: 'packageId',
+            example: 'com.vrchat.udonsharp',
+          }),
+          queryParam({
+            name: 'repository_url',
+            example: 'https://packages.vrchat.com/curated?download',
+            required: true,
+          }),
+          queryParam({
+            name: 'include_prereleases',
+            schema: { type: 'boolean' },
+            example: null,
+          }),
+        ],
       },
-      queryParams: {
-        repository_url: 'https://packages.vrchat.com/curated?download',
-      },
-      staticPreview: renderVersionBadge({ version: '1.1.6' }),
     },
-    {
-      title: 'VPM Package Version (including prereleases)',
-      namedParams: {
-        packageId: 'com.vrchat.udonsharp',
-      },
-      queryParams: {
-        repository_url: 'https://packages.vrchat.com/curated?download',
-        include_prereleases: null,
-      },
-      staticPreview: renderVersionBadge({ version: '1.1.6' }),
-    },
-  ]
+  }
 
   static defaultBadgeData = {
     label: 'vpm',