使用 Docusaurus 整合 OpenAPI/Swagger 文檔

Docusaurus (Document(文件)+ saurus(恐龍)),是由 Facebook 推出的開源靜態網站生成工具,以 React 技術構建,提供快速建置以文檔內容為核心的網站。我在 Survey 有哪些框架可以快速的把一堆 OpenAPI 文檔,轉成靜態頁面並且搜尋頁面內容,Claude 推薦我使用 Docusaurus

Claude  推薦我使用 Docusaurus 的原因如下:

  • 完整支援 Markdown
  • 可以整合 OpenAPI/Swagger 透過插件 docusaurus-plugin-openapi-docs
  • 內建全文搜尋功能,也可以整合 Algolia DocSearch
  • React-based,客製化彈性高
  • Meta (Facebook) 維護,社群活躍
  • 部署簡單,可以直接部署到靜態網站服務

於是,我來了。

開發環境

  • Windows 11 Home
  • node.js
  • docusaurus 3.6.3

確保已經安裝 nodejs

node -v 
npm -v

若沒有安裝的話使用 scoop 安裝

scoop install nodejs

 

確保已經安裝 yarn

若沒有 yarn,可以用 scoop 安裝

scoop install yarn

 

安裝 docusaurus

npx create-docusaurus@3.6.3 my-website-1 classic --typescript --package-manager yarn
  • my-website 是你的專案名稱,可以更改為你想要的名稱。
  • classic 是模板類型,也可以使用 minimal 或其他模板。
  • 支援 typescript、javascript
  • 最新版為 docusaurus@latest,當前為 3.6.3

 

切到 my-website-1 資料夾

yarn start 或是 npm start

 

訪問 http://localhost:3001

 

安裝 Open API 套件

yarn add docusaurus-plugin-openapi-docs

 

yarn add docusaurus-theme-openapi-docs

 

把 openapi.yml 放到 openapi 資料夾,目錄結構如下

my-website-1/
├── openapi/
│   ├── product.yml
│   ├── member.yml
├── docusaurus.config.ts

 

docusaurus.config.ts

複製以下內容,我把關鍵的行號加了 //<--- 加這個

import {themes as prismThemes} from 'prism-react-renderer';
import type {Config} from '@docusaurus/types';
import type * as Preset from '@docusaurus/preset-classic';
import type * as OpenApiPlugin from "docusaurus-plugin-openapi-docs"; //<--- 加這個

// This runs in Node.js - Don't use client-side code here (browser APIs, JSX...)

const config: Config = {
  title: 'My Site',
  tagline: 'Dinosaurs are cool',
  favicon: 'img/favicon.ico',

  // Set the production url of your site here
  url: 'https://your-docusaurus-site.example.com',
  // Set the /<baseUrl>/ pathname under which your site is served
  // For GitHub pages deployment, it is often '/<projectName>/'
  baseUrl: '/',

  // GitHub pages deployment config.
  // If you aren't using GitHub pages, you don't need these.
  organizationName: 'facebook', // Usually your GitHub org/user name.
  projectName: 'docusaurus', // Usually your repo name.

  onBrokenLinks: 'throw',
  onBrokenMarkdownLinks: 'warn',

  // Even if you don't use internationalization, you can use this field to set
  // useful metadata like html lang. For example, if your site is Chinese, you
  // may want to replace "en" with "zh-Hans".
  i18n: {
    defaultLocale: 'en',
    locales: ['en'],
  },

  presets: [
    [
      'classic',
      {
        docs: {
          sidebarPath: './sidebars.ts',
          // Please change this to your repo.
          // Remove this to remove the "edit this page" links.
          editUrl:
            'https://github.com/facebook/docusaurus/tree/main/packages/create-docusaurus/templates/shared/',
          docItemComponent: "@theme/ApiItem", //<--- 加這個
        },
        blog: {
          showReadingTime: true,
          feedOptions: {
            type: ['rss', 'atom'],
            xslt: true,
          },
          // Please change this to your repo.
          // Remove this to remove the "edit this page" links.
          editUrl:
            'https://github.com/facebook/docusaurus/tree/main/packages/create-docusaurus/templates/shared/',
          // Useful options to enforce blogging best practices
          onInlineTags: 'warn',
          onInlineAuthors: 'warn',
          onUntruncatedBlogPosts: 'warn',
        },
        theme: {
          customCss: './src/css/custom.css',
        },
      } satisfies Preset.Options,
    ],
  ],

  themeConfig: {
    // Replace with your project's social card
    image: 'img/docusaurus-social-card.jpg',
    navbar: {
      title: 'My Site',
      logo: {
        alt: 'My Site Logo',
        src: 'img/logo.svg',
      },
      items: [
        {
          type: 'docSidebar',
          sidebarId: 'tutorialSidebar',
          position: 'left',
          label: 'Tutorial',
        },
        {to: '/blog', label: 'Blog', position: 'left'},
        {
          href: 'https://github.com/facebook/docusaurus',
          label: 'GitHub',
          position: 'right',
        },
      ],
    },
    footer: {
      style: 'dark',
      links: [
        {
          title: 'Docs',
          items: [
            {
              label: 'Tutorial',
              to: '/docs/intro',
            },
          ],
        },
        {
          title: 'Community',
          items: [
            {
              label: 'Stack Overflow',
              href: 'https://stackoverflow.com/questions/tagged/docusaurus',
            },
            {
              label: 'Discord',
              href: 'https://discordapp.com/invite/docusaurus',
            },
            {
              label: 'X',
              href: 'https://x.com/docusaurus',
            },
          ],
        },
        {
          title: 'More',
          items: [
            {
              label: 'Blog',
              to: '/blog',
            },
            {
              label: 'GitHub',
              href: 'https://github.com/facebook/docusaurus',
            },
          ],
        },
      ],
      copyright: `Copyright © ${new Date().getFullYear()} My Project, Inc. Built with Docusaurus.`,
    },
    prism: {
      theme: prismThemes.github,
      darkTheme: prismThemes.dracula,
    },
  } satisfies Preset.ThemeConfig,

  plugins: [ // <--- 加這個
    [
      'docusaurus-plugin-openapi-docs',
      {
        id: "member",
        docsPluginId: "classic", // unique docsPluginId
        config: {
          member: {
            specPath: "openapi/member.yml",
            outputDir: "docs/api/member",
            sidebarOptions: {
              groupPathsBy: "tag",
            },
          } satisfies OpenApiPlugin.Options,
          product: {
            specPath: "openapi/product.yml",
            outputDir: "docs/api/product",
            sidebarOptions: {
              groupPathsBy: "tag",
            },
          } satisfies OpenApiPlugin.Options
        },

      },
    ],
  ],
  themes: ["docusaurus-theme-openapi-docs"], // <--- 加這個
};

export default config;

 

把 openapi 轉成 mdx

yarn docusaurus gen-api-docs all

 

重新啟動 npm,就可以看到我放進去的 openapi.yml 了

 

若要更新 openapi 得先清掉,再產生

yarn docusaurus clean-api-docs all

參考:https://github.com/PaloAltoNetworks/docusaurus-openapi-docs

 

假若每一次增加一個 openapi.yml 就要調整一次設定,不是很聰明,把它改成讀取 openapi 的資料夾

import path from "path";
import fs from "fs";

const generateOpenApiPlugins = () => {
  const openapiDir = path.resolve(__dirname, 'openapi');
  const openapiFiles = fs.readdirSync(openapiDir).filter(file => file.endsWith('.yml'));

  const config = openapiFiles.reduce((data, file) => {
    const id = path.basename(file, '.yml');
    console.log(`id: ${id}, file: ${file}`);
    data[id] = {
      specPath: path.join(openapiDir, file),
      outputDir: `docs/api/${id}`,
      sidebarOptions: {
        groupPathsBy: 'tag',
      }
    }  satisfies OpenApiPlugin.Options
    return data;
  }, {});

  const result = [
    'docusaurus-plugin-openapi-docs',
    {
      id: 'openapi',
      docsPluginId: 'classic',
      config,
    },
  ];

  console.log("result: " + result);
  return result;
};

 

plugins 調用 generateOpenApiPlugins

plugins: [
  generateOpenApiPlugins(),
],

 

完整 docusaurus.config.ts 內容如下

import {themes as prismThemes} from 'prism-react-renderer';
import type {Config} from '@docusaurus/types';
import type * as Preset from '@docusaurus/preset-classic';
import type * as OpenApiPlugin from "docusaurus-plugin-openapi-docs"; // <--- 加這個
import path from "path"; // <--- 加這個
import fs from "fs"; // <--- 加這個
const generateOpenApiPlugins = () => { // <--- 加這個
 const openapiDir = path.resolve(__dirname, 'openapi');
 const openapiFiles = fs.readdirSync(openapiDir).filter(file => file.endsWith('.yml'));
 const config = openapiFiles.reduce((data, file) => {
   const id = path.basename(file, '.yml');
   console.log(`id: ${id}, file: ${file}`);
   data[id] = {
     specPath: path.join(openapiDir, file),
     outputDir: `docs/api/${id}`,
     sidebarOptions: {
       groupPathsBy: 'tag',
     }
   }  satisfies OpenApiPlugin.Options
   return data;
 }, {});
 const result = [
   'docusaurus-plugin-openapi-docs',
   {
     id: 'openapi',
     docsPluginId: 'classic',
     config,
   },
 ];
 console.log("result: " + result);
 return result;
};
const config: Config = {
 title: 'My Site',
 tagline: 'Dinosaurs are cool',
 favicon: 'img/favicon.ico',
 // Set the production url of your site here
 url: 'https://your-docusaurus-site.example.com',
 // Set the /<baseUrl>/ pathname under which your site is served
 // For GitHub pages deployment, it is often '/<projectName>/'
 baseUrl: '/',
 // GitHub pages deployment config.
 // If you aren't using GitHub pages, you don't need these.
 organizationName: 'facebook', // Usually your GitHub org/user name.
 projectName: 'docusaurus', // Usually your repo name.
 onBrokenLinks: 'throw',
 onBrokenMarkdownLinks: 'warn',
 // Even if you don't use internationalization, you can use this field to set
 // useful metadata like html lang. For example, if your site is Chinese, you
 // may want to replace "en" with "zh-Hans".
 i18n: {
   defaultLocale: 'en',
   locales: ['en'],
 },
 presets: [
   [
     'classic',
     {
       docs: {
         sidebarPath: './sidebars.ts',
         // Please change this to your repo.
         // Remove this to remove the "edit this page" links.
         editUrl:
           'https://github.com/facebook/docusaurus/tree/main/packages/create-docusaurus/templates/shared/',
         docItemComponent: "@theme/ApiItem", //<--- 加這個
       },
       blog: {
         showReadingTime: true,
         feedOptions: {
           type: ['rss', 'atom'],
           xslt: true,
         },
         // Please change this to your repo.
         // Remove this to remove the "edit this page" links.
         editUrl:
           'https://github.com/facebook/docusaurus/tree/main/packages/create-docusaurus/templates/shared/',
         // Useful options to enforce blogging best practices
         onInlineTags: 'warn',
         onInlineAuthors: 'warn',
         onUntruncatedBlogPosts: 'warn',
       },
       theme: {
         customCss: './src/css/custom.css',
       },
     } satisfies Preset.Options,
   ],
 ],
 themeConfig: {
   // Replace with your project's social card
   image: 'img/docusaurus-social-card.jpg',
   navbar: {
     title: 'My Site',
     logo: {
       alt: 'My Site Logo',
       src: 'img/logo.svg',
     },
     items: [
       {
         type: 'docSidebar',
         sidebarId: 'tutorialSidebar',
         position: 'left',
         label: 'Tutorial',
       },
       {to: '/blog', label: 'Blog', position: 'left'},
       {
         href: 'https://github.com/facebook/docusaurus',
         label: 'GitHub',
         position: 'right',
       },
     ],
   },
   footer: {
     style: 'dark',
     links: [
       {
         title: 'Docs',
         items: [
           {
             label: 'Tutorial',
             to: '/docs/intro',
           },
         ],
       },
       {
         title: 'Community',
         items: [
           {
             label: 'Stack Overflow',
             href: 'https://stackoverflow.com/questions/tagged/docusaurus',
           },
           {
             label: 'Discord',
             href: 'https://discordapp.com/invite/docusaurus',
           },
           {
             label: 'X',
             href: 'https://x.com/docusaurus',
           },
         ],
       },
       {
         title: 'More',
         items: [
           {
             label: 'Blog',
             to: '/blog',
           },
           {
             label: 'GitHub',
             href: 'https://github.com/facebook/docusaurus',
           },
         ],
       },
     ],
     copyright: `Copyright © ${new Date().getFullYear()} My Project, Inc. Built with Docusaurus.`,
   },
   prism: {
     theme: prismThemes.github,
     darkTheme: prismThemes.dracula,
   },
 } satisfies Preset.ThemeConfig,
 plugins: [ // <--- 加這個
   generateOpenApiPlugins(),
 ],
 // plugins: [ // <--- 加這個
 //   [
 //     'docusaurus-plugin-openapi-docs',
 //     {
 //       id: "member",
 //       docsPluginId: "classic", // unique docsPluginId
 //       config: {
 //         member: {
 //           specPath: "openapi/member.yml",
 //           outputDir: "docs/api/member",
 //           sidebarOptions: {
 //             groupPathsBy: "tag",
 //           },
 //         } satisfies OpenApiPlugin.Options,
 //         product: {
 //           specPath: "openapi/product.yml",
 //           outputDir: "docs/api/product",
 //           sidebarOptions: {
 //             groupPathsBy: "tag",
 //           },
 //         } satisfies OpenApiPlugin.Options
 //       },
 //
 //     },
 //   ],
 // ],
 themes: ["docusaurus-theme-openapi-docs"], // <--- 加這個
};
export default config;

 

Search local

當文件越來越多的時候,搜尋功能就很重要了,中國開發者開發的  @easyops-cn/docusaurus-search-local - npm,是 search local 目前最多人在使用的,

安裝

yarn add @easyops-cn/docusaurus-search-local

 

在 docusaurus.config.ts,加上參數

  themes: ["docusaurus-theme-openapi-docs",
    [
      "@easyops-cn/docusaurus-search-local", // <--- 加這個
      {
        hashed: true,
        // language: ["en", "zh"],
        // highlightSearchTermsOnTargetPage: true,
        // explicitSearchResultPath: true,
      },
    ],
  ],

更多的設定請參考:easyops-cn/docusaurus-search-local: Offline/local search for Docusaurus v2/v3

工作原理:多语言全文搜索 | WeAreOutMan

 

建立索引檔

yarn run docusaurus build

 

可以看到多出了 build 資料夾

使用 serve 叫起服務

yarn run docusaurus serve

查詢效果如下

 

範例位置

sample.dotblog/Docusaurus/my-website-1 at master · yaochangyu/sample.dotblog

code 拉下來後,執行 yarn install 或 npm install

若有謬誤,煩請告知,新手發帖請多包涵


Microsoft MVP Award 2010~2017 C# 第四季
Microsoft MVP Award 2018~2022 .NET

Image result for microsoft+mvp+logo