diff --git a/.github/workflows/gh-pages.yml b/.github/workflows/gh-pages.yml index 4d0e2e67..caa91fb8 100644 --- a/.github/workflows/gh-pages.yml +++ b/.github/workflows/gh-pages.yml @@ -2,7 +2,9 @@ name: Deploy static content to Pages on: - # Allows you to run this workflow manually from the Actions tab + push: + branches: + - main workflow_dispatch: # Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages @@ -14,7 +16,7 @@ permissions: # Allow one concurrent deployment concurrency: group: "pages" - cancel-in-progress: true + cancel-in-progress: false jobs: # Single deploy job since we're just deploying @@ -25,13 +27,26 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v5 + with: + fetch-depth: 0 + - name: Setup Node + uses: actions/setup-node@v6 + with: + node-version: 24 + package-manager-cache: false + - name: Install dependencies + run: npm install --no-package-lock + - name: Build docs + env: + BASE_URL: /${{ github.event.repository.name }}/ + run: npm run docs:build - name: Setup Pages - uses: actions/configure-pages@v4 + uses: actions/configure-pages@v5 - name: Upload artifact uses: actions/upload-pages-artifact@v3 with: - path: './website' + path: './.vitepress/dist' - name: Deploy to GitHub Pages id: deployment uses: actions/deploy-pages@v4 diff --git a/.gitignore b/.gitignore index 272fb7f0..de784c79 100644 --- a/.gitignore +++ b/.gitignore @@ -15,3 +15,9 @@ go2rtc_win* 0_test.go .DS_Store + +.vitepress/cache +.vitepress/dist + +node_modules +package-lock.json \ No newline at end of file diff --git a/.vitepress/config.mts b/.vitepress/config.mts new file mode 100644 index 00000000..09c46e70 --- /dev/null +++ b/.vitepress/config.mts @@ -0,0 +1,160 @@ +import fs from 'fs'; +import path from 'path'; +import { defineConfig } from 'vitepress'; + +const repoRoot = path.resolve(__dirname, '..'); +const srcDir = repoRoot; +const skipDirs = new Set(['.git', 'node_modules', '.vitepress', 'dist', 'scripts']); + +function walkForReadmes(dir: string, results: string[]) { + for (const entry of fs.readdirSync(dir, { withFileTypes: true })) { + if (entry.isDirectory()) { + if (skipDirs.has(entry.name)) { + continue; + } + walkForReadmes(path.join(dir, entry.name), results); + continue; + } + + if (entry.isFile() && entry.name === 'README.md') { + results.push(path.join(dir, entry.name)); + } + } +} + +function extractTitle(filePath: string) { + const content = fs.readFileSync(filePath, 'utf8'); + const match = content.match(/^#\s+(.+)$/m); + return match ? match[1].trim() : ''; +} + +function toTitleCase(value: string) { + return value + .replace(/[-_]/g, ' ') + .replace(/\b\w/g, (char) => char.toUpperCase()); +} + +function toLink(routePath: string) { + const clean = routePath.replace(/index\.md$/, ''); + return clean ? `/${clean}` : '/'; +} + +const readmeFiles: string[] = []; +walkForReadmes(srcDir, readmeFiles); + +const readmePaths = readmeFiles + .map((filePath) => path.relative(srcDir, filePath).replace(/\\/g, '/')) + .sort(); + +const rewrites = Object.fromEntries( + readmePaths.map((relPath) => [relPath, relPath.replace(/README\.md$/, 'index.md')]) +); + +const groupOrder = ['', 'docker', 'api', 'pkg', 'internal', 'examples', 'www']; +const groupTitles = new Map([ + ['', 'Overview'], + ['api', 'API'], + ['pkg', 'Packages'], + ['internal', 'Internal'], + ['examples', 'Examples'], + ['docker', 'Docker'], + ['www', 'WWW'], +]); + +const groupedItems = new Map>(); + +for (const relPath of readmePaths) { + const filePath = path.join(srcDir, relPath); + const segments = relPath.split('/'); + const groupKey = segments.length > 1 ? segments[0] : ''; + const routePath = rewrites[relPath]; + const link = toLink(routePath); + const title = extractTitle(filePath); + const fallback = segments.length > 1 ? segments[segments.length - 2] : 'Overview'; + const text = title || toTitleCase(fallback); + + if (!groupedItems.has(groupKey)) { + groupedItems.set(groupKey, []); + } + groupedItems.get(groupKey)?.push({ text, link }); +} + +for (const items of groupedItems.values()) { + items.sort((a, b) => a.text.localeCompare(b.text)); +} + +const orderedGroups = [...groupedItems.entries()].sort((a, b) => { + const indexA = groupOrder.indexOf(a[0]); + const indexB = groupOrder.indexOf(b[0]); + if (indexA !== -1 || indexB !== -1) { + return (indexA === -1 ? Number.POSITIVE_INFINITY : indexA) - + (indexB === -1 ? Number.POSITIVE_INFINITY : indexB); + } + return a[0].localeCompare(b[0]); +}); + +const sidebar = orderedGroups.flatMap(([groupKey, items]) => { + const groupTitle = groupTitles.get(groupKey) || toTitleCase(groupKey || 'Overview'); + if (items.length === 1) { + const [item] = items; + return [{ text: groupTitle, link: item.link }]; + } + return [{ + text: groupTitle, + collapsed: groupKey !== '', + items, + }]; +}); + +const nav = orderedGroups + .filter(([, items]) => items.length > 0) + .map(([groupKey, items]) => { + if (groupKey === '') { + return { text: groupTitles.get(groupKey) || 'Overview', link: '/' }; + } + const landing = items.find((item) => item.link === `/${groupKey}/`) ?? items[0]; + return { + text: groupTitles.get(groupKey) || toTitleCase(groupKey), + link: landing.link, + }; + }); + +export default defineConfig({ + lang: 'en-US', + title: 'go2rtc Docs', + description: 'go2rtc documentation', + srcDir, + base: process.env.BASE_URL || '/', + cleanUrls: true, + ignoreDeadLinks: true, + rewrites, + head: [ + ['link', { rel: 'preconnect', href: 'https://fonts.googleapis.com' }], + ['link', { rel: 'preconnect', href: 'https://fonts.gstatic.com', crossorigin: '' }], + [ + 'link', + { + rel: 'stylesheet', + href: + 'https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;600&family=IBM+Plex+Sans:wght@400;500;600;700&display=swap', + }, + ], + ], + markdown: { + theme: { + light: "catppuccin-latte", + dark: "catppuccin-mocha", + }, + }, + themeConfig: { + nav, + sidebar: { + '/': sidebar, + }, + outline: [2, 3], + search: { + provider: 'local', + }, + socialLinks: [{ icon: "github", link: "https://github.com/AlexxIT/go2rtc" }], + }, +}); diff --git a/.vitepress/theme/custom.css b/.vitepress/theme/custom.css new file mode 100644 index 00000000..1a501f75 --- /dev/null +++ b/.vitepress/theme/custom.css @@ -0,0 +1,4 @@ + +.VPSidebarItem.level-1 { + font-weight: 700; +} \ No newline at end of file diff --git a/.vitepress/theme/index.ts b/.vitepress/theme/index.ts new file mode 100644 index 00000000..b4754c50 --- /dev/null +++ b/.vitepress/theme/index.ts @@ -0,0 +1,5 @@ +import DefaultTheme from 'vitepress/theme'; +import "@catppuccin/vitepress/theme/mocha/mauve.css"; +import './custom.css'; + +export default DefaultTheme; diff --git a/examples/onvif_client/README.md b/examples/onvif_client/README.md index b4ae3383..1fda07f1 100644 --- a/examples/onvif_client/README.md +++ b/examples/onvif_client/README.md @@ -1,4 +1,4 @@ -## Example +## ONVIF Client ```shell go run examples/onvif_client/main.go http://admin:password@192.168.10.90 GetAudioEncoderConfigurations diff --git a/internal/rtmp/README.md b/internal/rtmp/README.md index 662efa19..ce340e63 100644 --- a/internal/rtmp/README.md +++ b/internal/rtmp/README.md @@ -37,8 +37,8 @@ Settings > Stream: - Service: Custom - Server: rtmp://192.168.10.101/tmp -- Stream Key: -- Use auth: +- Stream Key: `` +- Use auth: `` **OpenIPC** diff --git a/package.json b/package.json index 649791f6..af5c7a70 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,13 @@ { "devDependencies": { "eslint": "^8.44.0", - "eslint-plugin-html": "^7.1.0" + "eslint-plugin-html": "^7.1.0", + "vitepress": "^2.0.0-alpha.15" + }, + "scripts": { + "docs:dev": "vitepress dev .", + "docs:build": "vitepress build .", + "docs:preview": "vitepress preview ." }, "eslintConfig": { "env": { @@ -36,5 +42,8 @@ } } ] + }, + "dependencies": { + "@catppuccin/vitepress": "^0.1.2" } } \ No newline at end of file