diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 14d539e6..89480cca 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -1,21 +1,60 @@ -name: Build and deploy - -on: [push] - -jobs: - build-and-deploy: - runs-on: ubuntu-latest - steps: - - name: 'Checkout the source code from GitHub' - uses: actions/checkout@master - - name: Install dependencies - run: npm ci - - name: Build - run: npm run-script docs:build - - name: Deploy - if: ${{ github.ref == 'refs/heads/master' }} - uses: peaceiris/actions-gh-pages@v3 - with: - github_token: ${{ secrets.GITHUB_TOKEN }} - cname: wiki.gruppe-adler.de - publish_dir: ./dist +name: Deploy to GitHub Pages + +on: + push: + branches: [master] + workflow_dispatch: + +permissions: + contents: read + pages: write + id-token: write + +# Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued. +# However, do NOT cancel in-progress runs as we want to allow these production deployments to complete. +concurrency: + group: pages + cancel-in-progress: false + +jobs: + # Build job + build: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 # Not needed if lastUpdated is not enabled + + - name: Setup Node + uses: actions/setup-node@v4 + with: + node-version: 20 + cache: npm + + - name: Setup Pages + uses: actions/configure-pages@v4 + + - name: Install dependencies + run: npm ci + + - name: Build + run: npm run docs:build + + - name: Upload artifact + uses: actions/upload-pages-artifact@v3 + with: + path: docs/.vuepress/dist + + # Deployment job + deploy: + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + needs: build + runs-on: ubuntu-latest + name: Deploy + steps: + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v4 \ No newline at end of file diff --git a/.gitignore b/.gitignore index da9a86be..e42069ba 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,5 @@ node_modules/ .vscode -dist/ -.temp -.cache \ No newline at end of file +docs/.vuepress/dist/ +docs/.vuepress/.temp +docs/.vuepress/.cache \ No newline at end of file diff --git a/docs/.vuepress/config.ts b/docs/.vuepress/config.ts index 91f8aeef..1be4402b 100644 --- a/docs/.vuepress/config.ts +++ b/docs/.vuepress/config.ts @@ -1,34 +1,40 @@ -import { path, fs } from '@vuepress/utils'; +import { path, fs, getDirname } from '@vuepress/utils'; +import type { SidebarItemOptions } from '@vuepress/theme-default'; +import { defineUserConfig } from '@vuepress/cli'; +import { viteBundler } from '@vuepress/bundler-vite'; +import { pwaPlugin } from '@vuepress/plugin-pwa'; +import { searchPlugin } from '@vuepress/plugin-search'; +import { googleAnalyticsPlugin } from '@vuepress/plugin-google-analytics'; +import wikiIndexPlugin from './plugins/wiki-index'; +import redirectFromDePlugin from './plugins/redirect-from-de'; +import resolveImageAliasesPlugin from './plugins/resolve-images-aliases'; +import { gruppeAdlerTheme } from './theme'; -import type { SidebarItem } from '@vuepress/theme-default'; -import type { AppOptions, DefaultThemeOptions, ViteBundlerOptions } from 'vuepress'; -import type { PwaPopupPluginOptions } from '@vuepress/plugin-pwa-popup'; -import type { SearchPluginOptions } from '@vuepress/plugin-search'; -import type { GoogleAnalyticsPluginOptions } from '@vuepress/plugin-google-analytics'; +const __dirname = getDirname(import.meta.url); const CATEGORIES: Array<[string, string]> = [ - ['Bastelstube','/bastelstube/'], - ['Infrastruktur','/infrastruktur/'], - ['Organisatorisches','/organisatorisches/'], - ['Taktik','/taktik/'] + ['Bastelstube', '/bastelstube/'], + ['Infrastruktur', '/infrastruktur/'], + ['Organisatorisches', '/organisatorisches/'], + ['Taktik', '/taktik/'] ]; -export default >>{ +export default defineUserConfig({ title: 'Wiki - Gruppe Adler', - description: 'Hier dokumentieren wir alles, was keiner liest, aber nicht verloren gehen soll. Unter anderem findest du Anleitung zu Missionsbau, Taktik und Infrastruktur.', + description: + 'Hier dokumentieren wir alles, was keiner liest, aber nicht verloren gehen soll. Unter anderem findest du Anleitung zu Missionsbau, Taktik und Infrastruktur.', lang: 'de', - dest: './dist', - source: '.', - bundler: '@vuepress/bundler-vite', + bundler: viteBundler(), head: [ ['link', { rel: 'manifest', href: '/manifest.webmanifest' }], - ['meta', { name: 'theme-color', content: '#000000' }], + ['meta', { name: 'theme-color', content: '#000000' }] ], alias: { - '@assets': path.resolve(__dirname, '../assets/') + '@assets': path.resolve(__dirname, '../assets/'), + '~@assets': path.resolve(__dirname, '../assets/') }, - theme: path.resolve(__dirname, './theme'), - themeConfig: { + + theme: gruppeAdlerTheme({ repo: 'gruppe-adler/wiki.gruppe-adler.de', docsBranch: 'master', docsDir: 'docs', @@ -42,37 +48,55 @@ export default >>{ openInNewWindow: '(öffnet in neuem Fenster)', navbar: CATEGORIES.map(([text, link]) => ({ text, link })), - sidebar: CATEGORIES.reduce((acc, [text, link]) => ({ ...acc, [link]: getSidebarItems(text, link) }), {}), + sidebar: CATEGORIES.reduce((acc, [text, link]) => ({ ...acc, [link]: getSidebarItems(text, link) }), { + '/wiki-index': [], + '/404': [], + '/': [] + }), sidebarDepth: 1, tip: 'TIPP', warning: 'AUFGEPASST', - danger: 'ACHTUNG' - }, - markdown: { - code: { - lineNumbers: false + danger: 'ACHTUNG', + themePlugins: { + prismjs: { + lineNumbers: false + } } - }, + }), + shouldPrefetch: false, plugins: [ - ['@vuepress/pwa', {}], - ['@vuepress/plugin-pwa-popup', { + pwaPlugin({ locales: { - '/': { message: 'Neuer Content ist verfügbar.', buttonText: 'Aktualisieren' } + '/': { + install: 'Installieren', + iOSInstall: "Drücke den Share-Button und dann 'zu Homescreen hinzufügen'", + cancel: 'Abbrechen', + close: 'Schließen', + prevImage: 'Vorheriges Bild', + nextImage: 'Nächstes Bild', + desc: 'Beschreibung', + feature: 'Funktionen', + explain: + 'Diese App kann auf Ihrem PC oder Mobilgerät installiert werden. Dadurch sieht diese Web-App aus und verhält sich wie jede andere installierte App. Sie finden sie in Ihren App-Listen und können sie an den Startbildschirm, die Startmenüs oder die Taskleisten anheften. Diese installierte Web-App kann auch sicher mit anderen Apps und Ihrem Betriebssystem interagieren.', + hint: 'Neuer Inhalt gefunden.', + update: 'Neue Inhalte sind verfügbar.' + } } - }], - ['@vuepress/plugin-search', >{ - locales: { '/': { placeholder: '' }} - }], - ['@vuepress/plugin-google-analytics', { - id: 'G-3NYJQ9NKJL', - }], - [path.resolve(__dirname, './plugins/wiki-index'), {}], - [path.resolve(__dirname, './plugins/redirect-from-de'), {}] + }), + searchPlugin({ + locales: { '/': { placeholder: '' } } + }), + googleAnalyticsPlugin({ + id: 'G-3NYJQ9NKJL' + }), + wikiIndexPlugin, + redirectFromDePlugin, + resolveImageAliasesPlugin ] -}; +}); -function getSidebarItems(displayName: string, directory: string): (SidebarItem|string)[] { +function getSidebarItems(displayName: string, directory: string): (SidebarItemOptions | string)[] { // find all md files let files = fs.readdirSync(path.resolve(__dirname, '..', directory.substr(1))).filter(file => /\.md$/i.test(file)); @@ -83,16 +107,16 @@ function getSidebarItems(displayName: string, directory: string): (SidebarItem|s files.unshift('README.md'); } - const children: Array = files.map(file => { + const children: Array = files.map(file => { if (file === 'README.md') return { text: '❔ Einleitung', link: directory }; - return `${directory}${file.slice(0, -3)}`; // remove .md extension + return `${directory}${file.slice(0, -3)}`; // remove .md extension }); return [ { text: displayName, - children, + children } ]; -} \ No newline at end of file +} diff --git a/docs/.vuepress/plugins/redirect-from-de/client.ts b/docs/.vuepress/plugins/redirect-from-de/client.ts new file mode 100644 index 00000000..c78bf485 --- /dev/null +++ b/docs/.vuepress/plugins/redirect-from-de/client.ts @@ -0,0 +1,11 @@ +import { defineClientConfig } from '@vuepress/client'; + +export default defineClientConfig({ + enhance: ({ app, router, siteData }) => { + router.addRoute('/', { + path: '/de/:pathMatch(.*)*', + redirect: to => to.fullPath.substring('/de'.length), + name: 'redirect-from-de' + }); + } +}); diff --git a/docs/.vuepress/plugins/redirect-from-de/clientAppEnhance.ts b/docs/.vuepress/plugins/redirect-from-de/clientAppEnhance.ts deleted file mode 100644 index 4f270964..00000000 --- a/docs/.vuepress/plugins/redirect-from-de/clientAppEnhance.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { defineClientAppEnhance } from '@vuepress/client' - -export default defineClientAppEnhance(({ app, router, siteData }) => { - router.addRoute('/', { - path: '/de/:pathMatch(.*)*', - redirect: to => to.fullPath.substr('/de'.length), - name: 'redirect-from-de' - }); -}) \ No newline at end of file diff --git a/docs/.vuepress/plugins/redirect-from-de/index.ts b/docs/.vuepress/plugins/redirect-from-de/index.ts index b6779024..6df3dfcd 100644 --- a/docs/.vuepress/plugins/redirect-from-de/index.ts +++ b/docs/.vuepress/plugins/redirect-from-de/index.ts @@ -1,7 +1,9 @@ -import { PluginObject } from "vuepress"; -import { path } from '@vuepress/utils'; +import type { PluginObject } from 'vuepress'; +import { path, getDirname } from '@vuepress/utils'; -export default { +const __dirname = getDirname(import.meta.url); + +export default { name: '@gruppe-adler/vuepress-plugin-redirect-from-de', - clientAppEnhanceFiles: path.resolve(__dirname, './clientAppEnhance.ts'), -}; \ No newline at end of file + clientConfigFile: path.resolve(__dirname, './client.ts') +} satisfies PluginObject; diff --git a/docs/.vuepress/plugins/resolve-images-aliases/README.md b/docs/.vuepress/plugins/resolve-images-aliases/README.md new file mode 100644 index 00000000..f28d65f3 --- /dev/null +++ b/docs/.vuepress/plugins/resolve-images-aliases/README.md @@ -0,0 +1,35 @@ +# `@gruppe-adler/vuepress-plugin-resolve-images-aliases` + +This plugin ensures that the `alias` options from the user config is applied to images in markdown. + +From the [Vuepress docs](https://v2.vuepress.vuejs.org/guide/assets.html#packages-and-path-aliases): + +> Although it is not a common usage, you can reference images from dependent packages: +> +> ```bash +> npm install -D package-name +> ``` +> +> Since markdown image syntax regards image links as relative paths by default, you need to use `` tag: +> +> ```md +> Image from dependency +> ``` +> +> The path aliases that set in config file are also supported: +> +> ```ts +> import { getDirname, path } from 'vuepress/utils'; +> +> const __dirname = getDirname(import.meta.url); +> +> export default { +> alias: { +> '@alias': path.resolve(__dirname, './path/to/some/dir') +> } +> }; +> ``` +> +> ```md +> Image from path alias +> ``` diff --git a/docs/.vuepress/plugins/resolve-images-aliases/index.ts b/docs/.vuepress/plugins/resolve-images-aliases/index.ts new file mode 100644 index 00000000..9f87b0b9 --- /dev/null +++ b/docs/.vuepress/plugins/resolve-images-aliases/index.ts @@ -0,0 +1,36 @@ +import type { PluginObject } from 'vuepress'; +import { path, getDirname } from '@vuepress/utils'; +import * as MarkdownIt from 'markdown-it'; + +const __dirname = getDirname(import.meta.url); + +export default { + name: '@gruppe-adler/vuepress-plugin-resolve-images-aliases', + extendsMarkdown: async (md: MarkdownIt, app) => { + const userConfig = + app.pluginApi.plugins.find(plugin => plugin.name === 'user-config') ?? + ({ name: 'user-config', alias: {} } as PluginObject); + + const aliasConfig = Object.entries( + typeof userConfig.alias === 'function' ? await userConfig.alias(app, true) : userConfig.alias + ) as [string, string][]; + + md.use(instance => { + const original = instance.renderer.rules.image; + + instance.renderer.rules.image = (tokens, idx, options, env, self) => { + const token = tokens[idx]; + const src = token.attrGet('src'); + + const aliasEntry = aliasConfig.find(([prefix]) => src.startsWith(prefix)); + + if (aliasEntry) { + const [prefix, absPath] = aliasEntry; + token.attrSet('src', src.replace(prefix, absPath)); + } + + return original(tokens, idx, options, env, self); + }; + }); + } +} satisfies PluginObject; diff --git a/docs/.vuepress/plugins/wiki-index/index.ts b/docs/.vuepress/plugins/wiki-index/index.ts index 5d15b23f..1f59232e 100644 --- a/docs/.vuepress/plugins/wiki-index/index.ts +++ b/docs/.vuepress/plugins/wiki-index/index.ts @@ -1,6 +1,6 @@ -import { PluginObject } from "vuepress"; -import { App, createPage } from '@vuepress/core'; -import { withSpinner } from '@vuepress/utils' +import { PluginObject } from 'vuepress'; +import { createPage } from '@vuepress/core'; +import { withSpinner } from '@vuepress/utils'; import emojiRegexFactory from 'emoji-regex'; const excludedPaths = ['/', '/404.html']; @@ -10,35 +10,34 @@ const emojiRegex = emojiRegexFactory(); const stripEmoji = (str: string): string => { const s = str.replace(emojiRegex, '').trim(); return s; -} +}; -async function renderIndex(app: App) { - await withSpinner('Generating Index')(async () => { - const allPages = app.pages.map(p => ({ path: p.path, title: p.title })); - - const pagesStr = allPages - .filter(p => !excludedPaths.includes(p.path)) - .map(p => ({ ...p, emojiLessTitle: stripEmoji(p.title) })) - .sort((a, b): number => a.emojiLessTitle.localeCompare(b.emojiLessTitle)) - .map(({ path, title }) => `#### [${title || 'Seite ohne Name'}](${path})`) - .join('\n'); - - const indexPage = await createPage(app, { - path: '/wiki-index', - frontmatter: { - sidebar: false, - editLink: false, - contributors: false, - lastUpdated: false - }, - content: `# 📑 Index\n${pagesStr}` - }) +export default { + name: '@gruppe-adler/vuepress-plugin-wiki-index', + onInitialized: async app => { + await withSpinner('Generating Index')(async () => { + const allPages = app.pages.map(p => ({ path: p.path, title: p.title })); - app.pages.push(indexPage); - }) -} + const pagesStr = allPages + .filter(p => !excludedPaths.includes(p.path)) + .map(p => ({ ...p, emojiLessTitle: stripEmoji(p.title) })) + .sort((a, b): number => a.emojiLessTitle.localeCompare(b.emojiLessTitle)) + .map(({ path, title }) => `#### [${title || 'Seite ohne Name'}](${path})`) + .join('\n'); -export default { - name: '@gruppe-adler/vuepress-plugin-wiki-index', - onInitialized: renderIndex -}; \ No newline at end of file + const indexPage = await createPage(app, { + path: '/wiki-index.html', + frontmatter: { + layout: 'Layout', + sidebar: false, + editLink: false, + contributors: false, + lastUpdated: false + }, + content: `# 📑 Index\n${pagesStr}` + }); + + app.pages.push(indexPage); + }); + } +} satisfies PluginObject; diff --git a/docs/.vuepress/theme/client.ts b/docs/.vuepress/theme/client.ts new file mode 100644 index 00000000..f32e5254 --- /dev/null +++ b/docs/.vuepress/theme/client.ts @@ -0,0 +1,20 @@ +import { defineClientConfig } from '@vuepress/client' +import LandingPage from './layouts/LandingPage.vue' +import NotFound from './layouts/404.vue' +import GitHubLinkVue from './components/GitHubLink.vue'; +import './styles/index.scss' + +export default defineClientConfig({ + enhance: ({ app, router, siteData }) => { + app.component('GradGitHubLink', GitHubLinkVue); + + if (!__VUEPRESS_SSR__) { + import('./registerNavbar').then(({ registerNavbar }) => registerNavbar(router)); + }; + }, + layouts: { + LandingPage, + NotFound, + }, +}); + diff --git a/docs/.vuepress/theme/clientAppEnhance.ts b/docs/.vuepress/theme/clientAppEnhance.ts deleted file mode 100644 index 1877d2c3..00000000 --- a/docs/.vuepress/theme/clientAppEnhance.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { defineClientAppEnhance } from '@vuepress/client' -import GitHubLinkVue from './components/GitHubLink.vue'; -import './styles/index.scss' - -export default defineClientAppEnhance(({ app, router, siteData }) => { - app.component('GradGitHubLink', GitHubLinkVue); - - if (!__VUEPRESS_SSR__) { - import('./registerNavbar').then(({ registerNavbar }) => registerNavbar(router)); - }; -}); diff --git a/docs/.vuepress/theme/components/GitHubLink.vue b/docs/.vuepress/theme/components/GitHubLink.vue index d2a38b27..2f70a3ea 100644 --- a/docs/.vuepress/theme/components/GitHubLink.vue +++ b/docs/.vuepress/theme/components/GitHubLink.vue @@ -1,10 +1,11 @@ diff --git a/docs/.vuepress/theme/index.ts b/docs/.vuepress/theme/index.ts index 6c23b99c..186ffca1 100644 --- a/docs/.vuepress/theme/index.ts +++ b/docs/.vuepress/theme/index.ts @@ -1,12 +1,11 @@ -import { path } from '@vuepress/utils'; +import { defaultTheme, DefaultThemeOptions } from '@vuepress/theme-default'; +import { path, getDirname } from '@vuepress/utils'; import type { ThemeObject } from 'vuepress'; -export default { +const __dirname = getDirname(import.meta.url) + +export const gruppeAdlerTheme = (options: DefaultThemeOptions): ThemeObject => ({ name: 'vuepress-theme-gruppe-adler', - extends: '@vuepress/theme-default', - layouts: { - LandingPage: path.resolve(__dirname, 'layouts/LandingPage.vue'), - 404: path.resolve(__dirname, 'layouts/404.vue'), - }, - clientAppEnhanceFiles: path.resolve(__dirname, './clientAppEnhance.ts'), -} \ No newline at end of file + extends: defaultTheme(options), + clientConfigFile: path.resolve(__dirname, 'client.ts'), +}) diff --git a/docs/.vuepress/theme/layouts/404.vue b/docs/.vuepress/theme/layouts/404.vue index 0fa5e87f..ae6a331b 100644 --- a/docs/.vuepress/theme/layouts/404.vue +++ b/docs/.vuepress/theme/layouts/404.vue @@ -12,7 +12,7 @@ diff --git a/docs/.vuepress/theme/layouts/LandingPage.vue b/docs/.vuepress/theme/layouts/LandingPage.vue index 076d6467..0bc32147 100644 --- a/docs/.vuepress/theme/layouts/LandingPage.vue +++ b/docs/.vuepress/theme/layouts/LandingPage.vue @@ -3,10 +3,10 @@