├── .editorconfig ├── .envrc ├── .gitattributes ├── .github └── workflows │ ├── ci.yml │ ├── ui-tests.yml │ └── zola.yml ├── .gitignore ├── .pre-commit-config.yaml ├── CLAUDE.md ├── Dockerfile.test ├── Justfile ├── LICENSE ├── README.md ├── bun.lock ├── config.toml ├── content ├── _index.md ├── about.md ├── posts │ ├── _index.md │ ├── character-shortcodes.md │ ├── collab-article.md │ ├── configuration.md │ ├── custom-homepage.md │ ├── markdown.md │ ├── math-symbol.md │ ├── mermaid.md │ └── shortcode.md ├── projects │ ├── _index.md │ ├── bacteria-jax.webm │ ├── bacteria_in_porous_media.md │ ├── project-1.jpg │ ├── project_1.md │ ├── project_2.md │ ├── project_3.md │ ├── project_4.md │ └── project_5.md └── talks │ ├── _index.md │ ├── building-fast-static-sites.md │ ├── debugging-kernel-drivers.md │ ├── intro-to-webassembly.md │ └── rust-security-best-practices.md ├── docker-compose.test.yml ├── flake.lock ├── flake.nix ├── lighthouserc.json ├── package-lock.json ├── package.json ├── playwright.config.ts ├── sass ├── fonts.scss ├── main.scss ├── parts │ ├── _cards.scss │ ├── _character.scss │ ├── _code.scss │ ├── _components.scss │ ├── _header.scss │ ├── _image.scss │ ├── _mermaid.scss │ ├── _misc.scss │ ├── _note.scss │ ├── _search.scss │ ├── _table.scss │ ├── _tags.scss │ ├── _talks.scss │ └── _toc.scss └── theme │ ├── dark.scss │ └── light.scss ├── screenshot-dark.png ├── screenshot.png ├── static ├── fonts │ ├── JetbrainsMono │ │ ├── JetBrainsMono-Bold.ttf │ │ ├── JetBrainsMono-BoldItalic.ttf │ │ ├── JetBrainsMono-ExtraBold.ttf │ │ ├── JetBrainsMono-ExtraBoldItalic.ttf │ │ ├── JetBrainsMono-ExtraLight.ttf │ │ ├── JetBrainsMono-ExtraLightItalic.ttf │ │ ├── JetBrainsMono-Italic.ttf │ │ ├── JetBrainsMono-Light.ttf │ │ ├── JetBrainsMono-LightItalic.ttf │ │ ├── JetBrainsMono-Medium.ttf │ │ ├── JetBrainsMono-MediumItalic.ttf │ │ ├── JetBrainsMono-Regular.ttf │ │ ├── JetBrainsMono-SemiBold.ttf │ │ ├── JetBrainsMono-SemiBoldItalic.ttf │ │ ├── JetBrainsMono-Thin.ttf │ │ └── JetBrainsMono-ThinItalic.ttf │ ├── SpaceGrotesk │ │ ├── SpaceGrotesk-Bold.ttf │ │ ├── SpaceGrotesk-Light.ttf │ │ ├── SpaceGrotesk-Medium.ttf │ │ ├── SpaceGrotesk-Regular.ttf │ │ └── SpaceGrotesk-SemiBold.ttf │ └── zed-fonts │ │ ├── ZedDisplayL-Heavy.woff2 │ │ ├── ZedTextL-Bold.woff2 │ │ └── ZedTextL-Regular.woff2 ├── icons │ ├── auto.svg │ ├── calendar.svg │ ├── code.svg │ ├── map-pin.svg │ ├── moon.svg │ ├── presentation.svg │ ├── search.svg │ ├── social │ │ ├── LICENSE │ │ ├── apple.svg │ │ ├── bitcoin.svg │ │ ├── bluesky.svg │ │ ├── deviantart.svg │ │ ├── diaspora.svg │ │ ├── discord.svg │ │ ├── discourse.svg │ │ ├── email.svg │ │ ├── ethereum.svg │ │ ├── etsy.svg │ │ ├── facebook.svg │ │ ├── fediverse.svg │ │ ├── github.svg │ │ ├── gitlab.svg │ │ ├── globe.svg │ │ ├── google-scholar.svg │ │ ├── google.svg │ │ ├── hacker-news.svg │ │ ├── instagram.svg │ │ ├── linkedin.svg │ │ ├── mastodon.svg │ │ ├── matrix.svg │ │ ├── orcid.svg │ │ ├── paypal.svg │ │ ├── pinterest.svg │ │ ├── quora.svg │ │ ├── reddit.svg │ │ ├── rss.svg │ │ ├── skype.svg │ │ ├── slack.svg │ │ ├── snapchat.svg │ │ ├── soundcloud.svg │ │ ├── spotify.svg │ │ ├── stack-exchange.svg │ │ ├── stack-overflow.svg │ │ ├── steam.svg │ │ ├── telegram.svg │ │ ├── twitter.svg │ │ ├── vimeo.svg │ │ ├── whatsapp.svg │ │ ├── x-twitter.svg │ │ └── youtube.svg │ └── sun.svg ├── images │ ├── characters │ │ └── hooded.png │ └── talks │ │ └── default.webp ├── js │ ├── codeblock.js │ ├── count.js │ ├── imamu.js │ ├── main.js │ ├── mermaid.js │ ├── note.js │ ├── searchElasticlunr.js │ ├── searchElasticlunr.min.js │ ├── themetoggle.js │ └── toc.js ├── processed_images │ ├── default.0b1c95d25c30c7b7.webp │ └── project-1.c835c9e2f29627ee.webp ├── syntax-theme-dark.css └── syntax-theme-light.css ├── templates ├── 404.html ├── _giscus_script.html ├── base.html ├── cards.html ├── homepage.html ├── index.html ├── macros │ ├── components.html │ └── macros.html ├── page.html ├── partials │ ├── header.html │ ├── nav.html │ └── toc.html ├── section.html ├── shortcodes │ ├── character.html │ ├── image.html │ ├── mermaid.html │ └── note.html ├── talks.html ├── taxonomy_list.html └── taxonomy_single.html ├── tests ├── README.md ├── content │ ├── advanced-features.spec.ts │ ├── blog-posts.spec.ts │ ├── configuration.spec.ts │ ├── fonts.spec.ts │ └── pages-and-features.spec.ts ├── helpers.ts ├── navigation │ ├── menu-navigation.spec.ts │ └── table-of-contents.spec.ts ├── theme │ └── theme-switching.spec.ts └── visual │ ├── character-shortcodes-visual.spec.ts │ ├── character-shortcodes-visual.spec.ts-snapshots │ ├── character-shortcodes-dark-Mobile-Chrome-linux.png │ ├── character-shortcodes-dark-chromium-linux.png │ ├── character-shortcodes-light-Mobile-Chrome-linux.png │ └── character-shortcodes-light-chromium-linux.png │ ├── projects-visual.spec.ts │ ├── projects-visual.spec.ts-snapshots │ ├── projects-page-Mobile-Chrome-linux.png │ ├── projects-page-Mobile-Safari-linux.png │ └── projects-page-chromium-linux.png │ ├── responsive-visual.spec.ts │ ├── responsive-visual.spec.ts-snapshots │ ├── homepage-desktop-Mobile-Chrome-linux.png │ ├── homepage-desktop-Mobile-Safari-linux.png │ ├── homepage-desktop-chromium-linux.png │ ├── homepage-mobile-Mobile-Chrome-linux.png │ ├── homepage-mobile-Mobile-Safari-linux.png │ ├── homepage-mobile-chromium-linux.png │ ├── homepage-tablet-Mobile-Chrome-linux.png │ ├── homepage-tablet-Mobile-Safari-linux.png │ ├── homepage-tablet-chromium-linux.png │ ├── navigation-mobile-chromium-linux.png │ └── posts-mobile-chromium-linux.png │ ├── talks-visual.spec.ts │ ├── talks-visual.spec.ts-snapshots │ ├── talks-page-Mobile-Chrome-linux.png │ ├── talks-page-Mobile-Safari-linux.png │ └── talks-page-chromium-linux.png │ ├── theme-visual.spec.ts │ └── theme-visual.spec.ts-snapshots │ ├── homepage-dark-Mobile-Chrome-linux.png │ ├── homepage-dark-Mobile-Safari-linux.png │ ├── homepage-dark-chromium-linux.png │ ├── homepage-light-Mobile-Chrome-linux.png │ ├── homepage-light-Mobile-Safari-linux.png │ ├── homepage-light-chromium-linux.png │ ├── posts-page-dark-chromium-linux.png │ └── posts-page-light-chromium-linux.png ├── theme.toml └── treefmt.toml /.editorconfig: -------------------------------------------------------------------------------- 1 | [*.css] 2 | indent_style = space 3 | indent_size = 2 4 | -------------------------------------------------------------------------------- /.envrc: -------------------------------------------------------------------------------- 1 | use flake 2 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | tests/visual/*.ts-snapshots/*.png filter=lfs diff=lfs merge=lfs -text 2 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | pull_request: 6 | 7 | jobs: 8 | format-check: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v4 12 | - uses: cachix/install-nix-action@v31 13 | - name: Check formatting 14 | run: nix develop --command treefmt --fail-on-change 15 | 16 | lighthouse: 17 | runs-on: ubuntu-latest 18 | permissions: 19 | pull-requests: write 20 | contents: read 21 | steps: 22 | - uses: actions/checkout@v4 23 | - uses: cachix/install-nix-action@v31 24 | - name: Build the site 25 | run: nix develop --command just build 26 | 27 | - name: Run Lighthouse against a static dist dir 28 | uses: treosh/lighthouse-ci-action@v12 29 | with: 30 | # no urls needed, since it uses local folder to scan .html files 31 | configPath: "./lighthouserc.json" 32 | uploadArtifacts: true 33 | temporaryPublicStorage: true 34 | -------------------------------------------------------------------------------- /.github/workflows/ui-tests.yml: -------------------------------------------------------------------------------- 1 | name: UI Tests 2 | 3 | on: 4 | push: 5 | branches: [ main, develop ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | jobs: 10 | test: 11 | timeout-minutes: 60 12 | runs-on: ubuntu-latest 13 | steps: 14 | 15 | # Skip installing package docs to avoid wasting time 16 | # See: https://github.com/actions/runner-images/issues/10977#issuecomment-2810713336 17 | - name: Skip installing package docs 18 | run: | 19 | sudo tee /etc/dpkg/dpkg.cfg.d/01_nodoc > /dev/null << 'EOF' 20 | path-exclude /usr/share/doc/* 21 | path-exclude /usr/share/man/* 22 | path-exclude /usr/share/info/* 23 | EOF 24 | 25 | - uses: actions/checkout@v4 26 | with: 27 | lfs: true 28 | 29 | - name: Setup Node.js 30 | uses: actions/setup-node@v4 31 | with: 32 | node-version: '20' 33 | cache: 'npm' 34 | 35 | - name: Install dependencies 36 | run: npm ci 37 | 38 | - name: Install Playwright Browsers 39 | run: npx playwright install --with-deps chromium webkit 40 | 41 | - name: Setup Zola 42 | uses: taiki-e/install-action@zola 43 | 44 | - name: Run Playwright tests 45 | run: npm run test 46 | 47 | - name: Upload test results 48 | uses: actions/upload-artifact@v4 49 | if: always() 50 | with: 51 | name: playwright-report 52 | path: playwright-report/ 53 | retention-days: 30 54 | 55 | - name: Upload screenshots 56 | uses: actions/upload-artifact@v4 57 | if: failure() 58 | with: 59 | name: test-screenshots 60 | path: test-results/ 61 | retention-days: 30 62 | 63 | - name: Deploy report to GitHub Pages 64 | uses: peaceiris/actions-gh-pages@v3 65 | if: failure() && github.event_name == 'pull_request' 66 | with: 67 | github_token: ${{ secrets.GITHUB_TOKEN }} 68 | publish_dir: ./playwright-report 69 | destination_dir: pr-${{ github.event.pull_request.number }} 70 | keep_files: true 71 | 72 | - name: Find existing PR comment 73 | uses: peter-evans/find-comment@v3 74 | if: always() && github.event_name == 'pull_request' 75 | id: find-comment 76 | with: 77 | issue-number: ${{ github.event.pull_request.number }} 78 | comment-author: 'github-actions[bot]' 79 | body-includes: 'Visual Regression Tests' 80 | 81 | - name: Comment test failure on PR 82 | uses: peter-evans/create-or-update-comment@v5 83 | if: failure() && github.event_name == 'pull_request' 84 | with: 85 | issue-number: ${{ github.event.pull_request.number }} 86 | comment-id: ${{ steps.find-comment.outputs.comment-id }} 87 | edit-mode: replace 88 | body: | 89 | ## Playwright Tests failed! 90 | 91 | [View report](https://${{ github.repository_owner }}.github.io/${{ github.event.repository.name }}/pr-${{ github.event.pull_request.number }}) 92 | 93 | - name: Comment test success on PR 94 | uses: peter-evans/create-or-update-comment@v5 95 | if: success() && github.event_name == 'pull_request' 96 | with: 97 | issue-number: ${{ github.event.pull_request.number }} 98 | comment-id: ${{ steps.find-comment.outputs.comment-id }} 99 | edit-mode: replace 100 | body: | 101 | ## Playwright Tests passed! 102 | 103 | All visual regression tests passed successfully! 104 | -------------------------------------------------------------------------------- /.github/workflows/zola.yml: -------------------------------------------------------------------------------- 1 | name: Zola 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | workflow_dispatch: 7 | 8 | # Allow one concurrent deployment 9 | concurrency: 10 | group: "pages" 11 | cancel-in-progress: true 12 | 13 | 14 | jobs: 15 | build: 16 | runs-on: ubuntu-latest 17 | 18 | steps: 19 | - uses: actions/checkout@v3 20 | 21 | - uses: cachix/install-nix-action@v31 22 | - name: Build the site 23 | run: nix develop --command just build 24 | 25 | - name: Deploy to GitHub Pages 26 | uses: peaceiris/actions-gh-pages@v3 27 | with: 28 | github_token: ${{ secrets.GITHUB_TOKEN }} 29 | publish_dir: ./dist 30 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | public/ 2 | dist/ 3 | .lighthouseci/ 4 | 5 | node_modules/ 6 | npm-debug.log* 7 | 8 | # Playwright 9 | test-results/ 10 | playwright-report/ 11 | playwright/.cache/ 12 | 13 | # IDE 14 | .vscode/ 15 | .idea/ 16 | 17 | # OS 18 | .DS_Store 19 | Thumbs.db 20 | 21 | # Logs 22 | *.log 23 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | exclude: ^(static/|content/) 2 | 3 | repos: 4 | - repo: https://github.com/pre-commit/pre-commit-hooks 5 | rev: v4.6.0 6 | hooks: 7 | - id: check-added-large-files 8 | args: ['--maxkb=100'] 9 | - id: check-case-conflict 10 | - id: check-merge-conflict 11 | - id: check-symlinks 12 | - id: check-toml 13 | - id: check-yaml 14 | - id: end-of-file-fixer 15 | - id: mixed-line-ending 16 | - id: trailing-whitespace 17 | 18 | - repo: local 19 | hooks: 20 | - id: treefmt 21 | name: treefmt 22 | entry: treefmt 23 | language: system 24 | types: 25 | - nix 26 | - toml 27 | - markdown 28 | - scss 29 | - html 30 | -------------------------------------------------------------------------------- /Dockerfile.test: -------------------------------------------------------------------------------- 1 | FROM mcr.microsoft.com/playwright:v1.55.0-noble 2 | 3 | # Install Zola 4 | RUN apt-get update && \ 5 | apt-get install -y wget && \ 6 | wget https://github.com/getzola/zola/releases/download/v0.19.2/zola-v0.19.2-x86_64-unknown-linux-gnu.tar.gz && \ 7 | tar -xzf zola-v0.19.2-x86_64-unknown-linux-gnu.tar.gz && \ 8 | mv zola /usr/local/bin/ && \ 9 | chmod +x /usr/local/bin/zola && \ 10 | rm zola-v0.19.2-x86_64-unknown-linux-gnu.tar.gz && \ 11 | apt-get clean && \ 12 | rm -rf /var/lib/apt/lists/* 13 | 14 | # Set working directory 15 | WORKDIR /app 16 | 17 | # Install dependencies first for better caching 18 | COPY package*.json ./ 19 | RUN npm ci 20 | 21 | # Copy the entire project 22 | COPY . . 23 | 24 | # Install Playwright browsers (they should already be in the base image) 25 | RUN npx playwright install --with-deps chromium webkit 26 | 27 | # Default command 28 | CMD ["npm", "test"] 29 | -------------------------------------------------------------------------------- /Justfile: -------------------------------------------------------------------------------- 1 | build: 2 | zola build --output-dir ./dist --force 3 | minify -r -a -o dist/ dist/ 4 | 5 | lighthouse: build 6 | bunx @lhci/cli@0.15.0 autorun 7 | 8 | lighthouse-open: 9 | bunx @lhci/cli@0.15.0 open 10 | 11 | ci: build lighthouse 12 | 13 | clean: 14 | rm -rf dist/ public/ .lighthouseci/ 15 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 not-matthias 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # apollo 2 | 3 | Modern and minimalistic blog theme powered by [Zola](https://getzola.org). See a live preview [here](https://not-matthias.github.io/apollo). 4 | 5 | Named after the greek god of knowledge, wisdom and intellect 6 | 7 |
8 | Dark theme 9 | 10 | ![blog-dark](./screenshot-dark.png) 11 | 12 |
13 | 14 |
15 | Light theme 16 | 17 | ![blog-light](./screenshot.png) 18 | 19 |
20 | 21 | ## Features 22 | 23 | - [x] Pagination 24 | - [x] Themes (light, dark, auto) 25 | - [x] Projects page 26 | - [x] Analytics using [GoatCounter](https://www.goatcounter.com/) / [Umami](https://umami.is/) / [Google Analytics](https://analytics.google.com/) 27 | - [x] Social Links 28 | - [x] MathJax Rendering 29 | - [x] Taxonomies 30 | - [x] Meta Tags For Individual Pages 31 | - [x] Custom homepage 32 | - [x] Comments 33 | - [x] Search 34 | - [x] RSS feeds 35 | - [x] Mermaid diagram support 36 | - [x] Table of Contents 37 | - [x] Configurable cards layout 38 | 39 | ## Installation 40 | 41 | 1. Download the theme 42 | 43 | ``` 44 | git submodule add https://github.com/not-matthias/apollo themes/apollo 45 | ``` 46 | 47 | 2. Add the following to the top of your `config.toml` 48 | 49 | ```toml 50 | theme = "apollo" 51 | taxonomies = [{ name = "tags" }] 52 | 53 | [extra] 54 | theme = "auto" 55 | socials = [ 56 | # Configure socials here 57 | ] 58 | menu = [ 59 | # Configure menu bar here 60 | ] 61 | 62 | # See this for more options: https://github.com/not-matthias/apollo/blob/main/config.toml#L14 63 | ``` 64 | 65 | 3. Copy the example content 66 | 67 | ``` 68 | cp -r themes/apollo/content/* content/ 69 | ``` 70 | 71 | ## Configuration 72 | 73 | Checkout all the [options you can configure](./content/posts/configuration.md) and the [example pages](./content/posts/). 74 | 75 | ## References 76 | 77 | This theme is based on [archie-zola](https://github.com/XXXMrG/archie-zola/). 78 | -------------------------------------------------------------------------------- /bun.lock: -------------------------------------------------------------------------------- 1 | { 2 | "lockfileVersion": 1, 3 | "workspaces": { 4 | "": { 5 | "name": "apollo-theme", 6 | "devDependencies": { 7 | "@playwright/test": "^1.46.0", 8 | "@types/node": "^20.0.0", 9 | "typescript": "^5.0.0", 10 | }, 11 | }, 12 | }, 13 | "packages": { 14 | "@playwright/test": ["@playwright/test@1.55.0", "", { "dependencies": { "playwright": "1.55.0" }, "bin": { "playwright": "cli.js" } }, "sha512-04IXzPwHrW69XusN/SIdDdKZBzMfOT9UNT/YiJit/xpy2VuAoB8NHc8Aplb96zsWDddLnbkPL3TsmrS04ZU2xQ=="], 15 | 16 | "@types/node": ["@types/node@20.19.13", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-yCAeZl7a0DxgNVteXFHt9+uyFbqXGy/ShC4BlcHkoE0AfGXYv/BUiplV72DjMYXHDBXFjhvr6DD1NiRVfB4j8g=="], 17 | 18 | "fsevents": ["fsevents@2.3.2", "", { "os": "darwin" }, "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA=="], 19 | 20 | "playwright": ["playwright@1.55.0", "", { "dependencies": { "playwright-core": "1.55.0" }, "optionalDependencies": { "fsevents": "2.3.2" }, "bin": "cli.js" }, "sha512-sdCWStblvV1YU909Xqx0DhOjPZE4/5lJsIS84IfN9dAZfcl/CIZ5O8l3o0j7hPMjDvqoTF8ZUcc+i/GL5erstA=="], 21 | 22 | "playwright-core": ["playwright-core@1.55.0", "", { "bin": "cli.js" }, "sha512-GvZs4vU3U5ro2nZpeiwyb0zuFaqb9sUiAJuyrWpcGouD8y9/HLgGbNRjIph7zU9D3hnPaisMl9zG9CgFi/biIg=="], 23 | 24 | "typescript": ["typescript@5.9.2", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A=="], 25 | 26 | "undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /config.toml: -------------------------------------------------------------------------------- 1 | base_url = "https://not-matthias.github.io/apollo/" 2 | title = "not-matthias" 3 | description = "This is an example description" 4 | build_search_index = true 5 | generate_feeds = true 6 | compile_sass = true 7 | minify_html = true 8 | taxonomies = [{ name = "tags" }] 9 | 10 | [search] 11 | include_title = true 12 | include_description = true 13 | include_path = true 14 | include_content = true 15 | index_format = "elasticlunr_json" 16 | 17 | [markdown] 18 | highlight_code = true 19 | highlight_theme = "css" 20 | highlight_themes_css = [ 21 | { theme = "ayu-dark", filename = "syntax-theme-dark.css" }, 22 | { theme = "ayu-light", filename = "syntax-theme-light.css" }, 23 | ] 24 | 25 | [extra] 26 | toc = true 27 | use_cdn = false 28 | favicon = "/icon/favicon.png" 29 | theme = "toggle" # light, dark, auto, toggle 30 | fancy_code = true 31 | dynamic_note = true # a note that can be toggled 32 | mathjax = true 33 | mathjax_dollar_inline_enable = true 34 | repo_url = "https://github.com/not-matthias/apollo/tree/main/content/" 35 | 36 | fediverse = false # author attribution 37 | fediverse_creator = "" # e.g. @user@instance 38 | 39 | menu = [ 40 | { name = "/posts", url = "/posts", weight = 1 }, 41 | { name = "/projects", url = "/projects", weight = 2 }, 42 | { name = "/talks", url = "/talks", weight = 3 }, 43 | { name = "/tags", url = "/tags", weight = 4 }, 44 | ] 45 | 46 | socials = [ 47 | { name = "twitter", url = "https://twitter.com/not_matthias", icon = "twitter" }, 48 | { name = "github", url = "https://github.com/not-matthias/", icon = "github" }, 49 | ] 50 | 51 | stylesheets = [ 52 | # "custom.css" # at /static/custom.css 53 | ] 54 | 55 | [extra.taxonomies] 56 | sort_by = "page_count" # e.g. name, page_count 57 | reverse = true 58 | 59 | [extra.analytics] 60 | enabled = false 61 | 62 | [extra.analytics.goatcounter] 63 | user = "your_user" 64 | host = "example.com" # default= goatcounter.com 65 | 66 | [extra.analytics.umami] 67 | website_id = "43929cd1-1e83...." 68 | host_url = "https://stats.mywebsite.com" # default: https://api-gateway.umami.dev/ 69 | 70 | [extra.analytics.google] 71 | tracking_id = "G-XXXXXXXXXX" # Your Google Analytics tracking ID 72 | -------------------------------------------------------------------------------- /content/_index.md: -------------------------------------------------------------------------------- 1 | +++ 2 | template = "homepage.html" 3 | +++ 4 | 5 | 21 | 22 |
23 |

Apollo

24 |

A modern and minimalistic blog theme powered by Zola.

25 |
26 | 27 | # Features 28 | 29 | - [Light, dark, and auto themes](@/posts/configuration.md#theme-mode-theme) 30 | - [Projects page](@/projects/_index.md) 31 | - [Talks page](https://not-matthias.github.io/talks/) 32 | - [Analytics (GoatCounter, Umami)](@/posts/configuration.md#analytics) 33 | - [Social media links](@/posts/configuration.md#socials) 34 | - [MathJax rendering](@/posts/math-symbol.md) 35 | - [Taxonomies](/apollo/tags) 36 | - [Custom homepage](@/posts/custom-homepage.md) 37 | - [Comments](@/posts/configuration.md#comments-comment) 38 | - [Search functionality](@/posts/configuration.md#search-build-search-index) 39 | - [Characters](@/posts/configuration.md#character-shortcodes) 40 | 41 | Checkout all the [options you can configure](@/posts/configuration.md) and the [example pages](@/posts/_index.md). 42 | 43 | # Quick Start 44 | 45 | 1. **Add the theme as a submodule:** 46 | ```bash 47 | git submodule add https://github.com/not-matthias/apollo themes/apollo 48 | ``` 49 | 2. **Configure your `config.toml`:** 50 | Set `theme = "apollo"` and add your site's configuration. 51 | 3. **Start the Zola server:** 52 | ```bash 53 | zola serve 54 | ``` 55 | -------------------------------------------------------------------------------- /content/about.md: -------------------------------------------------------------------------------- 1 | +++ 2 | title = "About" 3 | path = "about" 4 | +++ 5 | -------------------------------------------------------------------------------- /content/posts/_index.md: -------------------------------------------------------------------------------- 1 | +++ 2 | paginate_by = 7 3 | title = "Posts" 4 | sort_by = "date" 5 | 6 | insert_anchor_links = "heading" 7 | 8 | [extra] 9 | comment = true 10 | +++ 11 | -------------------------------------------------------------------------------- /content/posts/character-shortcodes.md: -------------------------------------------------------------------------------- 1 | +++ 2 | title = "Character Shortcodes Example" 3 | date = "2025-08-11" 4 | +++ 5 | 6 | Did you know that The oldest programming language still in use is FORTRAN which was created in 1957 by John Backus? 7 | 8 | {{ character(name="hooded", body="Whaaaaaaaaaaaaaaaaaaat, that's almost 70 years ago???") }} 9 | 10 | I know, it's crazy. Here's an example program: 11 | 12 | ``` 13 | PROGRAM MAIN 14 | PRINT *, 'HELLO WORLD' 15 | STOP 16 | END 17 | ``` 18 | 19 | {% character(name="hooded") %} 20 | There's also a more modern version which is a bit easier to read: 21 | ``` 22 | program helloWorld 23 | print *, "Hello World!" 24 | end program helloWorld 25 | ``` 26 | {% end %} 27 | 28 | Good to know, thanks buddy! 29 | 30 | {{ character(body=":)", position="left") }} 31 | -------------------------------------------------------------------------------- /content/posts/collab-article.md: -------------------------------------------------------------------------------- 1 | +++ 2 | title = "Collaborative Article" 3 | date = 2025-05-24 4 | authors = ["Alice", "Bob"] 5 | +++ 6 | 7 | Written by Alice and Bob! 8 | 9 | TBA 10 | -------------------------------------------------------------------------------- /content/posts/custom-homepage.md: -------------------------------------------------------------------------------- 1 | +++ 2 | title = "Creating a Custom Homepage" 3 | date = 2025-06-29 4 | +++ 5 | 6 | The Apollo theme provides a default homepage that lists your recent blog posts. However, you might want to create a custom homepage that better reflects your personality and work. This guide will walk you through the process of creating a custom homepage with the Apollo theme. 7 | 8 | ## 1. Create a Custom Homepage Template 9 | 10 | The first step is to create a custom homepage template. In the root of your Zola project, create a new file at `templates/home.html`. This file will contain the HTML for your custom homepage. 11 | 12 | You can use the following code as a starting point: 13 | 14 | ```html 15 | {% extends "base.html" %} 16 | 17 | {% block main_content %} 18 |
19 |
20 |
21 |

Welcome to my custom homepage!

22 |

This is where you can introduce yourself and your work.

23 |
24 |
25 |
26 | {% endblock main_content %} 27 | ``` 28 | 29 | This template extends the theme's `base.html` template and overrides the `main_content` block with your own content. 30 | 31 | ## 2. Set the Homepage Template 32 | 33 | Next, you need to tell Zola to use your custom homepage template. In the `content` directory of your Zola project, you should have a `_index.md` file. If you don't have one, create one. 34 | 35 | In this file, add the following front matter: 36 | 37 | ```toml 38 | +++ 39 | template = "home.html" 40 | +++ 41 | ``` 42 | 43 | This tells Zola to use the `templates/home.html` file to render your homepage. 44 | 45 | ## 3. Add Content to Your Homepage 46 | 47 | Now you can add content to your homepage. The content of the `content/_index.md` file will be available in your `templates/home.html` template as the `section` variable. 48 | 49 | For example, you can add a title and some introductory text to your `content/_index.md` file: 50 | 51 | ```toml 52 | +++ 53 | title = "Hey there! 👋🏼" 54 | template = "home.html" 55 | +++ 56 | 57 | I'm a software engineer who loves to write about technology and programming. 58 | ``` 59 | 60 | You can then display this content in your `templates/home.html` template: 61 | 62 | ```html 63 | {% extends "base.html" %} 64 | 65 | {% macro home_page(section) %} 66 |
67 |
68 |
69 | {{ post_macros::page_header(title=section.title) }} 70 | {{ section.content | safe }} 71 |
72 |
73 |
74 | {% endmacro home_page %} 75 | 76 | {% block main_content %} 77 | {{ self::home_page(section=section) }} 78 | {% endblock main_content %} 79 | ``` 80 | 81 | ## 4. Displaying Posts 82 | 83 | You can also display a list of your recent posts on your homepage. The following code shows how to display the 5 most recent posts: 84 | 85 | ```html 86 | {% extends "base.html" %} 87 | 88 | {% macro home_page(section) %} 89 |
90 |
91 |
92 | {{ post_macros::page_header(title=section.title) }} 93 | {{ section.content | safe }} 94 |
95 |
96 |
97 | {% endmacro home_page %} 98 | 99 | {% block main_content %} 100 | {{ self::home_page(section=section) }} 101 | 102 |

Recent articles

103 |
104 | {% set section = get_section(path="posts/_index.md") %} 105 | {{ post_macros::list_posts(pages=section.pages | slice(end=5)) }} 106 |
107 | {% endblock main_content %} 108 | ``` 109 | 110 | This code gets the `posts` section and then uses the `post_macros::list_posts` macro to display the 5 most recent posts. 111 | 112 | You can also highlight specific posts by getting them by their path: 113 | 114 | ```html 115 | {% set highlights = [ 116 | get_page(path="posts/my-first-post.md"), 117 | get_page(path="posts/my-second-post.md"), 118 | ] %} 119 |
120 | {{ post_macros::list_posts(pages=highlights) }} 121 |
122 | ``` 123 | 124 | This is just a starting point. You can customize your homepage as much as you want. 125 | -------------------------------------------------------------------------------- /content/posts/markdown.md: -------------------------------------------------------------------------------- 1 | +++ 2 | title = "Markdown Test" 3 | date = "2022-01-01" 4 | updated = "2022-05-01" 5 | 6 | [taxonomies] 7 | tags=["example"] 8 | 9 | [extra] 10 | comment = true 11 | +++ 12 | 13 | # H1 14 | 15 | ## H2 16 | 17 | ### H3 18 | 19 | Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Aliquet sagittis id consectetur purus ut. In pellentesque massa placerat duis ultricies. Neque laoreet suspendisse interdum consectetur libero id. Justo nec ultrices dui sapien eget mi proin. Nunc consequat interdum varius sit amet mattis vulputate. Sollicitudin tempor id eu nisl nunc mi ipsum. Non odio euismod lacinia at quis. Sit amet nisl suscipit adipiscing. Amet mattis vulputate enim nulla aliquet porttitor lacus luctus accumsan. Sit amet consectetur adipiscing elit pellentesque habitant. Ac placerat vestibulum lectus mauris. Molestie ac feugiat sed lectus vestibulum mattis ullamcorper velit sed. [Google](https://www.google.com) 20 | 21 | ![Markdown Logo](https://markdown-here.com/img/icon256.png) 22 | 23 | ## Code Block 24 | 25 | ```rust 26 | fn main() { 27 | println!("Hello World"); 28 | } 29 | ``` 30 | 31 | ```rust,hl_lines=2,linenos 32 | fn main() { 33 | println!("Hello World"); 34 | } 35 | ``` 36 | 37 | ## Ordered List 38 | 39 | 1. First item 40 | 2. Second item 41 | 3. Third item 42 | 43 | ## Unordered List 44 | 45 | - List item 46 | - Another item 47 | - And another item 48 | 49 | ## Nested list 50 | 51 | - Fruit 52 | - Apple 53 | - Orange 54 | - Banana 55 | - Dairy 56 | - Milk 57 | - Cheese 58 | 59 | ## Quote 60 | 61 | > Two things are infinite: the universe and human stupidity; and I'm not sure about the 62 | > universe.
63 | > — Albert Einstein 64 | 65 | ## Table Inline Markdown 66 | 67 | | Italics | Bold | Code | StrikeThrough | 68 | | --------- | -------- | ------ | ----------------- | 69 | | _italics_ | **bold** | `code` | ~~strikethrough~~ | 70 | 71 | ## Foldable Text 72 | 73 |
74 | Title 1 75 |

IT'S A SECRET TO EVERYBODY.

76 |
77 | 78 |
79 | Title 2 80 |

Stay awhile, and listen!

81 |
82 | 83 | ## Code tags 84 | 85 | Lorem ipsum `dolor` sit amet, `consectetur adipiscing` elit. 86 | `Lorem ipsum dolor sit amet, consectetur adipiscing elit.` 87 | -------------------------------------------------------------------------------- /content/posts/math-symbol.md: -------------------------------------------------------------------------------- 1 | +++ 2 | title = "Math Symbol Example" 3 | date = "2023-01-06" 4 | 5 | [taxonomies] 6 | tags=["example"] 7 | 8 | [extra] 9 | comment = true 10 | +++ 11 | 12 | Note: This requires the `mathjax` and `mathjax_dollar_inline_enable` option set to `true` in `[extra]` section. 13 | 14 | # Inline Math 15 | 16 | - $(a+b)^2$ = $a^2 + 2ab + b^2$ 17 | - A polynomial P of degree d over $\mathbb{F}_p$ is an expression of the form 18 | $P(s) = a_0 + a_1 . s + a_2 . s^2 + ... + a_d . s^d$ for some 19 | $a_0,..,a_d \in \mathbb{F}_p$ 20 | 21 | # Displayed Math 22 | 23 | $$ 24 | p := (\sum_{k∈I}{c_k.v_k} + \delta_v.t(x))·(\sum_{k∈I}{c_k.w_k} + \delta_w.t(x)) − (\sum_{k∈I}{c_k.y_k} + \delta_y.t(x)) 25 | $$ 26 | -------------------------------------------------------------------------------- /content/posts/mermaid.md: -------------------------------------------------------------------------------- 1 | +++ 2 | title = "Mermaid Example" 3 | date = "2024-12-26" 4 | 5 | [taxonomies] 6 | tags=["example"] 7 | 8 | [extra] 9 | comment = true 10 | +++ 11 | 12 | This Theme supports [mermaid](https://mermaid.js.org/) markdown diagram rendering. 13 | 14 | To use mermaid diagrams in your posts, see the example in the raw markdown code. 15 | https://raw.githubusercontent.com/not-matthias/apollo/refs/heads/main/content/posts/mermaid.md 16 | 17 | ## Rendered Example 18 | 19 | {% mermaid() %} 20 | graph LR 21 | A[Start] --> B[Initialize] 22 | B --> C[Processing] 23 | C --> D[Complete] 24 | D --> E[Success] 25 | 26 | style A fill:#f9f,stroke:#333 27 | style E fill:#9f9,stroke:#333 28 | {% end %} 29 | -------------------------------------------------------------------------------- /content/posts/shortcode.md: -------------------------------------------------------------------------------- 1 | +++ 2 | title = "Shortcode Example" 3 | date = "2024-06-14" 4 | 5 | [taxonomies] 6 | tags=["example"] 7 | 8 | [extra] 9 | comment = true 10 | +++ 11 | 12 | 13 | ## Note 14 | 15 | Here is an example of the `note` shortcode: 16 | 17 | This one is static! 18 | {{ note(header="Note!", body="This blog assumes basic terminal maturity") }} 19 | 20 | This one is clickable! 21 | {{ note(clickable=true, hidden = true, header="Quiz!", body="The answer to the quiz!") }} 22 | 23 | 24 | Syntax: 25 | ``` 26 | {{/* note(header="Note!", body="This blog assumes basic terminal maturity") */}} 27 | {{/* note(clickable=true, hidden = true, header="Quiz!", body="The answer to the quiz!") */}} 28 | ``` 29 | 30 | You can also use some HTML in the text: 31 | {{ note(header="Note!", body="

This blog assumes basic terminal maturity

") }} 32 | 33 | 34 | Literal shortcode: 35 | ``` 36 | {{/* note(header="Note!", body="

This blog assumes basic terminal maturity

") */}} 37 | ``` 38 | 39 | Pretty cool, right? 40 | 41 | Finally, you can do something like this (hopefully): 42 | 43 | {% note(clickable=true, header="Quiz!") %} 44 | 45 | # Hello this is markdown inside a note shortcode 46 | 47 | ```rust 48 | fn main() { 49 | println!("Hello World"); 50 | } 51 | ``` 52 | 53 | We can't call another shortcode inside a shortcode, but this is good enough. 54 | 55 | {% end %} 56 | 57 | Here is the raw markdown: 58 | 59 | ```markdown 60 | {{/* note(clickable=true, header="Quiz!") */}} 61 | 62 | # Hello this is markdown inside a note shortcode 63 | 64 | \`\`\`rust 65 | fn main() { 66 | println!("Hello World"); 67 | } 68 | \`\`\` 69 | 70 | We can't call another shortcode inside a shortcode, but this is good enough. 71 | 72 | {{/* end */}} 73 | ``` 74 | 75 | Finally, we have center 76 | {{ note(center=true, header="Centered Text", body="This is centered text") }} 77 | 78 | ```markdown 79 | {{/* note(center=true, header="Centered Text", body="This is centered text") */}} 80 | ``` 81 | It works good enough for me! 82 | -------------------------------------------------------------------------------- /content/projects/_index.md: -------------------------------------------------------------------------------- 1 | +++ 2 | title = "Projects" 3 | sort_by = "weight" 4 | template = "cards.html" 5 | 6 | [extra] 7 | cards_columns = 2 8 | card_media_height = 250 9 | +++ 10 | -------------------------------------------------------------------------------- /content/projects/bacteria-jax.webm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/not-matthias/apollo/6ae2c5ad18987891460c0b9908797f9a6e0cb8f6/content/projects/bacteria-jax.webm -------------------------------------------------------------------------------- /content/projects/bacteria_in_porous_media.md: -------------------------------------------------------------------------------- 1 | +++ 2 | title = "DevSecOps Pipeline Toolkit" 3 | description = "Ship secure code faster with automated security gates" 4 | weight = 1 5 | date = 2024-06-10 6 | 7 | [extra] 8 | local_video = "bacteria-jax.webm" 9 | github = "https://github.com/example/devsecops-toolkit" 10 | demo = "https://devsecops-demo.example.com" 11 | tags = ["go", "docker", "kubernetes", "terraform"] 12 | +++ 13 | 14 | A comprehensive DevSecOps toolkit that integrates security scanning directly into CI/CD pipelines. Features automated vulnerability detection, container scanning, infrastructure-as-code validation, and compliance checking. Built for cloud-native environments with support for multi-cloud deployments. Includes customizable security policies, detailed reporting, and automated remediation suggestions. 15 | -------------------------------------------------------------------------------- /content/projects/project-1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/not-matthias/apollo/6ae2c5ad18987891460c0b9908797f9a6e0cb8f6/content/projects/project-1.jpg -------------------------------------------------------------------------------- /content/projects/project_1.md: -------------------------------------------------------------------------------- 1 | +++ 2 | title = "Apollo" 3 | description = "A modern and minimalistic blog theme for Zola" 4 | weight = 1 5 | date = 2023-10-26 6 | 7 | [extra] 8 | local_image = "project-1.jpg" 9 | github = "https://github.com/not-matthias/apollo" 10 | demo = "https://not-matthias.github.io/apollo" 11 | tags = ["rust", "scss", "javascript", "tera"] 12 | +++ 13 | 14 | Apollo is a fast, elegant static site theme built for Zola. It features a clean design with light/dark mode support, responsive layouts, and excellent performance. The theme is highly customizable through a simple configuration file and includes built-in support for analytics, social links, and math rendering. 15 | -------------------------------------------------------------------------------- /content/projects/project_2.md: -------------------------------------------------------------------------------- 1 | +++ 2 | title = "Kernel Fuzzing Framework" 3 | description = "Find kernel bugs before attackers do" 4 | weight = 2 5 | date = 2024-03-15 6 | 7 | [extra] 8 | remote_image = "https://images.unsplash.com/photo-1523821741446-edb2b68bb7a0?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1470&q=80" 9 | github = "https://github.com/example/kernel-fuzzer" 10 | tags = ["rust", "c", "llvm", "python"] 11 | +++ 12 | -------------------------------------------------------------------------------- /content/projects/project_3.md: -------------------------------------------------------------------------------- 1 | +++ 2 | title = "Memory Safety Analyzer" 3 | description = "Catch use-after-free and buffer overflows at compile time" 4 | weight = 3 5 | date = 2024-01-20 6 | 7 | [extra] 8 | github = "https://github.com/example/memory-analyzer" 9 | tags = ["rust", "llvm", "c++"] 10 | +++ 11 | -------------------------------------------------------------------------------- /content/projects/project_4.md: -------------------------------------------------------------------------------- 1 | +++ 2 | title = "Binary Analysis Platform" 3 | description = "Dissect binaries with automated decompilation and analysis" 4 | weight = 4 5 | date = 2023-11-08 6 | 7 | [extra] 8 | remote_image = "https://images.unsplash.com/photo-1620121692029-d088224ddc74?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1632&q=80" 9 | github = "https://github.com/example/binary-analyzer" 10 | demo = "https://binary-analysis-demo.example.com" 11 | tags = ["python", "capstone", "ghidra", "angr"] 12 | +++ 13 | -------------------------------------------------------------------------------- /content/projects/project_5.md: -------------------------------------------------------------------------------- 1 | +++ 2 | title = "Exploit Development Framework" 3 | description = "Research-focused exploit development for security professionals" 4 | weight = 5 5 | date = 2023-08-12 6 | 7 | [extra] 8 | github = "https://github.com/example/exploit-framework" 9 | tags = ["python", "assembly", "metasploit"] 10 | +++ 11 | -------------------------------------------------------------------------------- /content/talks/_index.md: -------------------------------------------------------------------------------- 1 | +++ 2 | title = "Talks" 3 | sort_by = "date" 4 | template = "talks.html" 5 | +++ 6 | 7 | A collection of talks and presentations I've given at various conferences and meetups. 8 | -------------------------------------------------------------------------------- /content/talks/building-fast-static-sites.md: -------------------------------------------------------------------------------- 1 | +++ 2 | title = "Building Lightning-Fast Static Sites with Zola" 3 | description = "Learn how to create performant static websites using Zola, a Rust-powered static site generator that compiles your content in milliseconds." 4 | date = 2024-06-22 5 | 6 | [extra] 7 | video = { link = "https://www.youtube.com/watch?v=dQw4w9WgXcQ", thumbnail = "images/talks/default.webp" } 8 | slides = "https://example.com/slides/zola-static-sites" 9 | 10 | [extra.organizer] 11 | name = "Web Performance Meetup" 12 | link = "https://webperf.example.com" 13 | +++ 14 | 15 | An introduction to Zola and static site generation, demonstrating how to achieve sub-second build times and perfect Lighthouse scores. 16 | -------------------------------------------------------------------------------- /content/talks/debugging-kernel-drivers.md: -------------------------------------------------------------------------------- 1 | +++ 2 | title = "Debugging Kernel Drivers: Tools and Techniques" 3 | description = "An exploration of debugging techniques for kernel-level code, including WinDbg, GDB, and custom debugging tools for driver development." 4 | date = 2024-03-10 5 | 6 | [extra] 7 | video = { link = "https://www.youtube.com/watch?v=dQw4w9WgXcQ", thumbnail = "images/talks/default.webp" } 8 | slides = "https://example.com/slides/kernel-debugging" 9 | code = "https://github.com/example/kernel-debug-tools" 10 | 11 | [extra.organizer] 12 | name = "Systems Programming Conference" 13 | link = "https://sysprog.example.com" 14 | +++ 15 | 16 | This talk demonstrates advanced debugging workflows for kernel driver development, including live kernel debugging, crash dump analysis, and performance profiling. 17 | -------------------------------------------------------------------------------- /content/talks/intro-to-webassembly.md: -------------------------------------------------------------------------------- 1 | +++ 2 | title = "Introduction to WebAssembly" 3 | description = "Getting started with WebAssembly: compiling Rust to WASM, integrating with JavaScript, and building high-performance web applications." 4 | date = 2023-11-05 5 | 6 | [extra] 7 | video = { link = "https://www.youtube.com/watch?v=dQw4w9WgXcQ", thumbnail = "images/talks/default.webp" } 8 | slides = "https://example.com/slides/wasm-intro" 9 | 10 | [extra.organizer] 11 | name = "JS Nation Conference" 12 | link = "https://jsnation.com" 13 | +++ 14 | 15 | A beginner-friendly introduction to WebAssembly, showing how to compile Rust code to run in the browser and achieve near-native performance for compute-intensive tasks. 16 | -------------------------------------------------------------------------------- /content/talks/rust-security-best-practices.md: -------------------------------------------------------------------------------- 1 | +++ 2 | title = "Rust Security Best Practices" 3 | description = "A deep dive into security considerations when writing Rust applications, covering memory safety, dependency management, and common pitfalls." 4 | date = 2024-09-15 5 | 6 | [extra] 7 | video = { link = "https://www.youtube.com/watch?v=dQw4w9WgXcQ", thumbnail = "images/talks/default.webp" } 8 | slides = "https://example.com/slides/rust-security" 9 | code = "https://github.com/example/rust-security-demos" 10 | 11 | [extra.organizer] 12 | name = "RustConf 2024" 13 | link = "https://rustconf.com" 14 | +++ 15 | 16 | This talk covers essential security practices for Rust developers, including how to leverage Rust's type system for security, managing unsafe code, and auditing dependencies. 17 | -------------------------------------------------------------------------------- /docker-compose.test.yml: -------------------------------------------------------------------------------- 1 | services: 2 | playwright-tests: 3 | build: 4 | context: . 5 | dockerfile: Dockerfile.test 6 | volumes: 7 | - .:/app 8 | - /app/node_modules 9 | working_dir: /app 10 | environment: 11 | - CI=true 12 | command: npm test 13 | 14 | # Alternative service for interactive testing 15 | playwright-ui: 16 | build: 17 | context: . 18 | dockerfile: Dockerfile.test 19 | volumes: 20 | - .:/app 21 | - /app/node_modules 22 | working_dir: /app 23 | ports: 24 | - "9323:9323" 25 | environment: 26 | - CI=false 27 | command: npx playwright test --ui --ui-host=0.0.0.0 28 | -------------------------------------------------------------------------------- /flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "flake-utils": { 4 | "inputs": { 5 | "systems": "systems" 6 | }, 7 | "locked": { 8 | "lastModified": 1731533236, 9 | "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", 10 | "owner": "numtide", 11 | "repo": "flake-utils", 12 | "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", 13 | "type": "github" 14 | }, 15 | "original": { 16 | "owner": "numtide", 17 | "repo": "flake-utils", 18 | "type": "github" 19 | } 20 | }, 21 | "nixpkgs": { 22 | "locked": { 23 | "lastModified": 1759381078, 24 | "narHash": "sha256-gTrEEp5gEspIcCOx9PD8kMaF1iEmfBcTbO0Jag2QhQs=", 25 | "owner": "NixOS", 26 | "repo": "nixpkgs", 27 | "rev": "7df7ff7d8e00218376575f0acdcc5d66741351ee", 28 | "type": "github" 29 | }, 30 | "original": { 31 | "owner": "NixOS", 32 | "ref": "nixos-unstable", 33 | "repo": "nixpkgs", 34 | "type": "github" 35 | } 36 | }, 37 | "root": { 38 | "inputs": { 39 | "flake-utils": "flake-utils", 40 | "nixpkgs": "nixpkgs" 41 | } 42 | }, 43 | "systems": { 44 | "locked": { 45 | "lastModified": 1681028828, 46 | "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", 47 | "owner": "nix-systems", 48 | "repo": "default", 49 | "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", 50 | "type": "github" 51 | }, 52 | "original": { 53 | "owner": "nix-systems", 54 | "repo": "default", 55 | "type": "github" 56 | } 57 | } 58 | }, 59 | "root": "root", 60 | "version": 7 61 | } 62 | -------------------------------------------------------------------------------- /flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | description = "Apollo - A modern and minimalistic blog theme for Zola"; 3 | 4 | inputs = { 5 | nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; 6 | flake-utils.url = "github:numtide/flake-utils"; 7 | }; 8 | 9 | outputs = { 10 | self, 11 | nixpkgs, 12 | flake-utils, 13 | }: 14 | flake-utils.lib.eachDefaultSystem (system: let 15 | pkgs = nixpkgs.legacyPackages.${system}; 16 | in { 17 | devShells.default = pkgs.mkShell { 18 | nativeBuildInputs = with pkgs; [ 19 | zola 20 | pre-commit 21 | just 22 | bun 23 | 24 | # Formatters 25 | treefmt 26 | nodePackages.prettier 27 | alejandra 28 | djlint 29 | 30 | # For minifying assets 31 | minify 32 | ]; 33 | 34 | shellHook = '' 35 | # Install pre-commit hooks if not already installed 36 | if [ ! -f .git/hooks/pre-commit ]; then 37 | echo "Installing pre-commit hooks..." 38 | pre-commit install 39 | fi 40 | ''; 41 | }; 42 | }); 43 | } 44 | -------------------------------------------------------------------------------- /lighthouserc.json: -------------------------------------------------------------------------------- 1 | { 2 | "ci": { 3 | "collect": { 4 | "staticDistDir": "./dist" 5 | } 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "apollo-theme", 3 | "version": "1.0.0", 4 | "lockfileVersion": 3, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "apollo-theme", 9 | "version": "1.0.0", 10 | "devDependencies": { 11 | "@playwright/test": "^1.46.0", 12 | "@types/node": "^20.0.0", 13 | "typescript": "^5.0.0" 14 | } 15 | }, 16 | "node_modules/@playwright/test": { 17 | "version": "1.55.0", 18 | "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.55.0.tgz", 19 | "integrity": "sha512-04IXzPwHrW69XusN/SIdDdKZBzMfOT9UNT/YiJit/xpy2VuAoB8NHc8Aplb96zsWDddLnbkPL3TsmrS04ZU2xQ==", 20 | "dev": true, 21 | "license": "Apache-2.0", 22 | "dependencies": { 23 | "playwright": "1.55.0" 24 | }, 25 | "bin": { 26 | "playwright": "cli.js" 27 | }, 28 | "engines": { 29 | "node": ">=18" 30 | } 31 | }, 32 | "node_modules/@types/node": { 33 | "version": "20.19.13", 34 | "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.13.tgz", 35 | "integrity": "sha512-yCAeZl7a0DxgNVteXFHt9+uyFbqXGy/ShC4BlcHkoE0AfGXYv/BUiplV72DjMYXHDBXFjhvr6DD1NiRVfB4j8g==", 36 | "dev": true, 37 | "license": "MIT", 38 | "dependencies": { 39 | "undici-types": "~6.21.0" 40 | } 41 | }, 42 | "node_modules/fsevents": { 43 | "version": "2.3.2", 44 | "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", 45 | "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", 46 | "dev": true, 47 | "hasInstallScript": true, 48 | "license": "MIT", 49 | "optional": true, 50 | "os": [ 51 | "darwin" 52 | ], 53 | "engines": { 54 | "node": "^8.16.0 || ^10.6.0 || >=11.0.0" 55 | } 56 | }, 57 | "node_modules/playwright": { 58 | "version": "1.55.0", 59 | "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.55.0.tgz", 60 | "integrity": "sha512-sdCWStblvV1YU909Xqx0DhOjPZE4/5lJsIS84IfN9dAZfcl/CIZ5O8l3o0j7hPMjDvqoTF8ZUcc+i/GL5erstA==", 61 | "dev": true, 62 | "license": "Apache-2.0", 63 | "dependencies": { 64 | "playwright-core": "1.55.0" 65 | }, 66 | "bin": { 67 | "playwright": "cli.js" 68 | }, 69 | "engines": { 70 | "node": ">=18" 71 | }, 72 | "optionalDependencies": { 73 | "fsevents": "2.3.2" 74 | } 75 | }, 76 | "node_modules/playwright-core": { 77 | "version": "1.55.0", 78 | "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.55.0.tgz", 79 | "integrity": "sha512-GvZs4vU3U5ro2nZpeiwyb0zuFaqb9sUiAJuyrWpcGouD8y9/HLgGbNRjIph7zU9D3hnPaisMl9zG9CgFi/biIg==", 80 | "dev": true, 81 | "license": "Apache-2.0", 82 | "bin": { 83 | "playwright-core": "cli.js" 84 | }, 85 | "engines": { 86 | "node": ">=18" 87 | } 88 | }, 89 | "node_modules/typescript": { 90 | "version": "5.9.2", 91 | "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.2.tgz", 92 | "integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==", 93 | "dev": true, 94 | "license": "Apache-2.0", 95 | "bin": { 96 | "tsc": "bin/tsc", 97 | "tsserver": "bin/tsserver" 98 | }, 99 | "engines": { 100 | "node": ">=14.17" 101 | } 102 | }, 103 | "node_modules/undici-types": { 104 | "version": "6.21.0", 105 | "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", 106 | "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", 107 | "dev": true, 108 | "license": "MIT" 109 | } 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "apollo-theme", 3 | "version": "1.0.0", 4 | "description": "UI tests for Apollo Zola theme", 5 | "private": true, 6 | "scripts": { 7 | "test": "playwright test", 8 | "test:docker-compose": "docker-compose -f docker-compose.test.yml run --rm playwright-tests", 9 | "test:ui": "playwright test --ui", 10 | "test:debug": "playwright test --debug", 11 | "test:headed": "playwright test --headed", 12 | "test:update-snapshots": "docker-compose -f docker-compose.test.yml run --rm playwright-tests npm run test:update-snapshots", 13 | "serve": "zola serve", 14 | "build": "zola build" 15 | }, 16 | "devDependencies": { 17 | "@playwright/test": "^1.46.0", 18 | "@types/node": "^20.0.0", 19 | "typescript": "^5.0.0" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /playwright.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig, devices } from '@playwright/test'; 2 | 3 | export default defineConfig({ 4 | testDir: './tests', 5 | fullyParallel: true, 6 | forbidOnly: !!process.env.CI, 7 | retries: process.env.CI ? 2 : 0, 8 | workers: process.env.CI ? 12 : undefined, 9 | reporter: 'html', 10 | timeout: 10000, 11 | use: { 12 | baseURL: 'http://127.0.0.1:1111', 13 | trace: 'on-first-retry', 14 | actionTimeout: 5000, 15 | screenshot: { 16 | mode: 'only-on-failure', 17 | }, 18 | }, 19 | 20 | // Global screenshot comparison settings 21 | expect: { 22 | toHaveScreenshot: { 23 | maxDiffPixelRatio: 0.02, // Allow up to 2% pixel difference 24 | timeout: 10000, 25 | }, 26 | }, 27 | 28 | projects: [ 29 | { 30 | name: 'chromium', 31 | use: { ...devices['Desktop Chrome'] }, 32 | }, 33 | 34 | // { 35 | // name: 'firefox', 36 | // use: { ...devices['Desktop Firefox'] }, 37 | // }, 38 | 39 | // { 40 | // name: 'webkit', 41 | // use: { ...devices['Desktop Safari'] }, 42 | // }, 43 | 44 | // Mobile viewports 45 | { 46 | name: 'Mobile Chrome', 47 | use: { ...devices['Pixel 5'] }, 48 | }, 49 | { 50 | name: 'Mobile Safari', 51 | use: { ...devices['iPhone 12'] }, 52 | }, 53 | ], 54 | 55 | // Run the local dev server before starting tests 56 | webServer: { 57 | command: 'zola serve', 58 | url: 'http://127.0.0.1:1111', 59 | reuseExistingServer: !process.env.CI, 60 | timeout: 120 * 1000, 61 | }, 62 | }); 63 | -------------------------------------------------------------------------------- /sass/fonts.scss: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: "Jetbrains Mono"; 3 | font-style: normal; 4 | font-weight: 400; 5 | src: url("fonts/JetbrainsMono/JetBrainsMono-Regular.ttf"), local("ttf"); 6 | font-display: swap; 7 | } 8 | 9 | @font-face { 10 | font-family: "Jetbrains Mono"; 11 | font-style: normal; 12 | font-weight: 700; 13 | src: url("fonts/JetbrainsMono/JetBrainsMono-Bold.ttf"), local("ttf"); 14 | font-display: swap; 15 | } 16 | 17 | @font-face { 18 | font-family: "Space Grotesk"; 19 | font-style: normal; 20 | font-weight: 400; 21 | src: url("fonts/SpaceGrotesk/SpaceGrotesk-Regular.ttf"), local("ttf"); 22 | font-display: swap; 23 | } 24 | 25 | @font-face { 26 | font-family: "Space Grotesk"; 27 | font-style: normal; 28 | font-weight: 700; 29 | src: url("fonts/SpaceGrotesk/SpaceGrotesk-Bold.ttf"), local("ttf"); 30 | font-display: swap; 31 | } 32 | 33 | // 34 | // 35 | 36 | @font-face { 37 | font-family: "ZedTextFtl"; 38 | src: url(fonts/zed-fonts/ZedTextL-Regular.woff2) format("woff2"); 39 | font-weight: 400; 40 | font-style: normal; 41 | font-display: swap; 42 | } 43 | 44 | @font-face { 45 | font-family: "ZedTextFtl"; 46 | src: url(fonts/zed-fonts/ZedTextL-Bold.woff2) format("woff2"); 47 | font-weight: 700; 48 | font-style: normal; 49 | font-display: swap; 50 | } 51 | 52 | @font-face { 53 | font-family: "ZedDisplayFtl"; 54 | src: url(fonts/zed-fonts/ZedDisplayL-Heavy.woff2) format("woff2"); 55 | font-weight: 400; 56 | font-style: normal; 57 | font-display: swap; 58 | } 59 | -------------------------------------------------------------------------------- /sass/main.scss: -------------------------------------------------------------------------------- 1 | @import "fonts.scss"; 2 | @import "parts/_cards.scss"; 3 | @import "parts/_character.scss"; 4 | @import "parts/_code.scss"; 5 | @import "parts/_header.scss"; 6 | @import "parts/_image.scss"; 7 | @import "parts/_toc.scss"; 8 | @import "parts/_note.scss"; 9 | @import "parts/_misc.scss"; 10 | @import "parts/_table.scss"; 11 | @import "parts/_tags.scss"; 12 | @import "parts/_mermaid.scss"; 13 | @import "parts/_search.scss"; 14 | @import "parts/_talks.scss"; 15 | @import "parts/_components.scss"; 16 | 17 | :root { 18 | /* Used for: block comment, hr, ... */ 19 | --border-color: var(--border-color); 20 | 21 | /* Fonts */ 22 | --font-size-base: 13.5px; 23 | --mono-text-font: "Jetbrains Mono"; 24 | --text-font: "ZedTextFtl"; 25 | --header-font: "ZedDisplayFtl" "Space Grotesk", "Helvetica", sans-serif; 26 | --code-font: "Jetbrains Mono"; 27 | 28 | --line-height: 1.5; 29 | --page-width: 920px; 30 | } 31 | 32 | html { 33 | background-color: var(--bg-0); 34 | color: var(--text-0); 35 | font-family: var(--text-font); 36 | line-height: var(--line-height); 37 | 38 | @media (max-width: 992px) { 39 | font-size: calc(var(--font-size-base) * 0.97); 40 | } 41 | @media (max-width: 768px) { 42 | font-size: calc(var(--font-size-base) * 0.95); 43 | } 44 | @media (max-width: 576px) { 45 | font-size: calc(var(--font-size-base) * 0.92); 46 | } 47 | } 48 | 49 | body { 50 | display: flex; 51 | flex-grow: 1; 52 | padding: 0.9rem; 53 | padding-bottom: 1.5rem; 54 | margin-bottom: 1.5rem; 55 | min-height: calc(100vh - 150px); 56 | 57 | @media (min-width: 992px) { 58 | flex-direction: row; 59 | justify-content: center; 60 | align-items: flex-start; 61 | } 62 | 63 | .content { 64 | width: 100%; 65 | max-width: var(--page-width); 66 | flex-shrink: 0; 67 | padding-bottom: 1.5rem; 68 | margin-bottom: 1.5rem; 69 | word-wrap: break-word; 70 | } 71 | 72 | .left-content { 73 | width: 100%; 74 | 75 | @media (min-width: 992px) { 76 | flex: 1 1 0; 77 | min-width: 0; 78 | } 79 | } 80 | 81 | .right-content { 82 | width: 100%; 83 | 84 | @media (min-width: 992px) { 85 | flex: 1 1 0; 86 | min-width: 0; 87 | 88 | // Don't move this to the TOC, it won't work 89 | // FIXME: Find a workaround 90 | position: sticky; 91 | top: 60px; 92 | padding: 1em; 93 | overflow-y: auto; 94 | max-height: calc(100vh - 100px); 95 | 96 | } 97 | 98 | 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /sass/parts/_cards.scss: -------------------------------------------------------------------------------- 1 | .cards { 2 | column-count: 2; 3 | column-gap: 20px; 4 | padding: 12px 0; 5 | 6 | @media (max-width: 640px) { 7 | column-count: 1; 8 | } 9 | 10 | @media (max-width: 720px) { 11 | gap: 16px; 12 | } 13 | } 14 | 15 | .card { 16 | display: flex; 17 | flex-direction: column; 18 | background: var(--bg-1); 19 | border: 2px solid var(--border-color); 20 | border-radius: 10px; 21 | break-inside: avoid; 22 | margin-bottom: 20px; 23 | 24 | &-media { 25 | width: 100%; 26 | height: 200px; 27 | overflow: hidden; 28 | background: var(--bg-2); 29 | flex-shrink: 0; 30 | 31 | @media (max-width: 720px) { 32 | height: 160px; 33 | } 34 | } 35 | 36 | &-image, 37 | &-video { 38 | width: 100%; 39 | height: 100%; 40 | object-fit: cover; 41 | display: block; 42 | } 43 | 44 | &-video { 45 | border-radius: 8px; 46 | } 47 | 48 | &-content { 49 | flex: 1; 50 | display: flex; 51 | flex-direction: column; 52 | gap: 12px; 53 | padding: 20px; 54 | 55 | @media (max-width: 720px) { 56 | padding: 16px; 57 | } 58 | } 59 | 60 | &-title { 61 | margin: 0; 62 | line-height: 1.3; 63 | 64 | @media (max-width: 720px) { 65 | font-size: 1.1rem; 66 | } 67 | } 68 | 69 | &-tagline { 70 | margin: 0; 71 | font-size: 0.95rem; 72 | color: var(--text-color-secondary); 73 | line-height: 1.5; 74 | } 75 | 76 | &-footer { 77 | display: flex; 78 | justify-content: space-between; 79 | align-items: center; 80 | gap: 12px; 81 | margin-top: auto; 82 | } 83 | 84 | &-links { 85 | display: flex; 86 | gap: 10px; 87 | flex-shrink: 0; 88 | } 89 | 90 | &-tags { 91 | display: flex; 92 | flex-wrap: wrap; 93 | gap: 4px; 94 | justify-content: flex-end; 95 | align-items: center; 96 | } 97 | 98 | &-tag { 99 | font-size: 0.7rem; 100 | color: var(--text-1); 101 | white-space: nowrap; 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /sass/parts/_character.scss: -------------------------------------------------------------------------------- 1 | .character-note { 2 | display: flex; 3 | flex-direction: row; 4 | margin-block: 1.5rem; 5 | margin-inline-start: auto; 6 | margin-inline-end: auto; 7 | 8 | &.character-right { 9 | flex-direction: row-reverse; 10 | 11 | .character-avatar img { 12 | transform: scaleX(-1); 13 | } 14 | } 15 | 16 | &.character-left { 17 | flex-direction: row; 18 | } 19 | 20 | .character-avatar { 21 | font-size: 2rem; 22 | align-self: flex-start; 23 | flex-shrink: 0; 24 | 25 | img { 26 | --head-size: 3.2em; 27 | width: var(--head-size); 28 | height: var(--head-size); 29 | } 30 | } 31 | 32 | .character-content { 33 | font-size: var(--font-size); 34 | align-self: flex-start; 35 | max-width: min(93%, 45em); 36 | overflow: hidden; 37 | } 38 | 39 | .character-bubble { 40 | --character-bubble-bg: var(--bg-1); 41 | --character-bubble-border: var(--border-color); 42 | --character-code-bg: var(--bg-0); 43 | 44 | background: var(--character-bubble-bg); 45 | border: 1px solid var(--character-bubble-border); 46 | border-radius: 0.5rem; 47 | padding-inline: 0.9em; 48 | padding-block: 0.2em; 49 | } 50 | 51 | } -------------------------------------------------------------------------------- /sass/parts/_code.scss: -------------------------------------------------------------------------------- 1 | // Define base colors and fonts for light and dark themes 2 | :root { 3 | --code-font: var(--code-font), monospace; 4 | --bg-primary: var(--bg-1); 5 | --text-color: var(--text-0); // Color of the code text 6 | --label-color: #f0f0f0; // Text color of the label 7 | 8 | --highlight-color: #f0f0f0; 9 | } 10 | 11 | :root.dark { 12 | --highlight-color: #204e8a; 13 | } 14 | 15 | // Define language colors map 16 | $language-colors: ( 17 | "js": ( 18 | #f7df1e, 19 | "JavaScript", 20 | ), 21 | "yaml": ( 22 | #f71e6a, 23 | "YAML", 24 | ), 25 | "shell": ( 26 | #4eaa25, 27 | "Shell", 28 | ), 29 | // Updated to a more specific green shade 30 | "json": ( 31 | dodgerblue, 32 | "JSON", 33 | ), 34 | "python": ( 35 | #3572a5, 36 | "Python", 37 | ), 38 | // Using the specific Python blue 39 | "css": ( 40 | #264de4, 41 | "CSS", 42 | ), 43 | "go": ( 44 | #00add8, 45 | "Go", 46 | ), 47 | // Official Go color 48 | "markdown": ( 49 | #0000ff, 50 | "Markdown", 51 | ), 52 | "rust": ( 53 | #ff4647, 54 | "Rust", 55 | ), 56 | // Adjusted to match Rust's branding 57 | "java": ( 58 | #f89820, 59 | "Java", 60 | ), 61 | // Oracle Java color 62 | "csharp": ( 63 | #178600, 64 | "C#", 65 | ), 66 | "ruby": ( 67 | #701516, 68 | "Ruby", 69 | ), 70 | "swift": ( 71 | #f05138, 72 | "Swift", 73 | ), 74 | "php": ( 75 | #777bb4, 76 | "PHP", 77 | ), 78 | "typescript": ( 79 | #3178c6, 80 | "TypeScript", 81 | ), 82 | "scala": ( 83 | #c22d40, 84 | "Scala", 85 | ), 86 | "kotlin": ( 87 | #f18e33, 88 | "Kotlin", 89 | ), 90 | "lua": ( 91 | #000080, 92 | "Lua", 93 | ), 94 | "perl": ( 95 | #0298c3, 96 | "Perl", 97 | ), 98 | "haskell": ( 99 | #5e5086, 100 | "Haskell", 101 | ), 102 | "r": ( 103 | #198ce7, 104 | "R", 105 | ), 106 | "dart": ( 107 | #00d2b8, 108 | "Dart", 109 | ), 110 | "elixir": ( 111 | #6e4a7e, 112 | "Elixir", 113 | ), 114 | "clojure": ( 115 | #5881d8, 116 | "Clojure", 117 | ), 118 | "bash": ( 119 | #4eaa25, 120 | "Bash", 121 | ), 122 | "default": ( 123 | #333, 124 | "Code", 125 | ), 126 | ); 127 | 128 | @mixin base-label-style($bg-color, $text-color: var(--label-color)) { 129 | background: $bg-color; 130 | color: $text-color; 131 | border-radius: 0 0 0.25rem 0.25rem; 132 | font-size: 12px; 133 | letter-spacing: 0.025rem; 134 | padding: 0.1rem 0.5rem; 135 | text-align: right; 136 | text-transform: uppercase; 137 | position: absolute; 138 | right: 0; 139 | top: 0; 140 | margin-top: 0.1rem; 141 | } 142 | 143 | // Example usage within a specific class for clarity 144 | .code-label { 145 | @include base-label-style(#333); // Default background color 146 | } 147 | 148 | @each $lang, $color-info in $language-colors { 149 | .label-#{$lang} { 150 | @include base-label-style(nth($color-info, 1)); 151 | } 152 | } 153 | 154 | code { 155 | background-color: var(--bg-primary); 156 | padding: 0.1em 0.2em; 157 | border-radius: 5px; 158 | border: 1px solid var(--border-color); 159 | font-family: var(--code-font); 160 | } 161 | 162 | pre { 163 | background-color: var(--bg-primary) !important; 164 | border-radius: 5px; 165 | border: 1px solid var(--border-color); 166 | line-height: 1.4; 167 | overflow-x: auto; 168 | padding: 1em; 169 | position: relative; 170 | 171 | mark { 172 | background-color: var( 173 | --highlight-color 174 | ) !important; // Ensure mark uses the theme background 175 | padding: 0; 176 | border-radius: 0px; 177 | } 178 | 179 | code { 180 | background-color: transparent !important; 181 | color: var(--text-color); 182 | font-size: 100%; 183 | padding: 0; 184 | border: none; 185 | font-family: var(--code-font); 186 | 187 | table { 188 | margin: 0; 189 | border-collapse: collapse; 190 | font-family: var(--code-font); 191 | 192 | mark { 193 | display: block; 194 | color: unset; 195 | padding: 0; 196 | background-color: var(--highlight-color) !important; 197 | filter: brightness(1.2); // Example to slightly increase brightness 198 | } 199 | } 200 | 201 | td, 202 | th, 203 | tr { 204 | padding: 0; 205 | border-bottom: none; 206 | border: none; // Ensure no borders around rows 207 | } 208 | 209 | tbody td:first-child { 210 | text-align: center; 211 | user-select: none; 212 | min-width: 60px; 213 | border-right: none; 214 | } 215 | 216 | tbody tr:nth-child(even), 217 | thead tr { 218 | background-color: unset; 219 | } 220 | } 221 | } 222 | 223 | .clipboard-button, 224 | .clipboard-button svg { 225 | all: unset; 226 | cursor: pointer; 227 | position: absolute; 228 | bottom: 5px; 229 | right: 5px; 230 | z-index: 10; 231 | background-color: transparent; 232 | border: none; 233 | fill: var(--text-color); 234 | } 235 | -------------------------------------------------------------------------------- /sass/parts/_components.scss: -------------------------------------------------------------------------------- 1 | // Icon button component - reusable button with optional icon 2 | .icon-button { 3 | display: inline-flex; 4 | align-items: center; 5 | padding: 4px 8px; 6 | gap: 3px; 7 | background: var(--bg-2); 8 | font-size: 0.75rem; 9 | color: var(--text-color); 10 | 11 | /* Ensure that the a tag has also a bottom border, which is by default hidden */ 12 | border: 1px solid var(--border-color) !important; 13 | border-radius: 6px; 14 | 15 | svg, 16 | img { 17 | flex-shrink: 0; 18 | width: 16px; 19 | height: 16px; 20 | } 21 | 22 | img { 23 | filter: var(--icon-filter); 24 | } 25 | 26 | &:hover { 27 | cursor: pointer; 28 | background: var(--bg-1); 29 | color: var(--text-color); 30 | 31 | // FIXME: Exclude this in the `main a` tag which adds it's own underline 32 | border: 1px solid var(--border-color); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /sass/parts/_header.scss: -------------------------------------------------------------------------------- 1 | .page-header { 2 | font-size: 2.5em; 3 | line-height: 100%; 4 | font-family: var(--header-font); 5 | margin: 4rem 0px 1rem 0px; 6 | } 7 | 8 | /* 404 header */ 9 | .not-found-header { 10 | font-family: var(--header-font); 11 | position: absolute; 12 | top: 40%; 13 | left: 50%; 14 | transform: translate(-50%, -50%); 15 | text-align: center; 16 | font-size: 3em; 17 | } 18 | 19 | nav { 20 | display: flex; 21 | flex-direction: row; 22 | flex-wrap: wrap; 23 | justify-content: space-between; 24 | padding: 0; 25 | 26 | /* Mobile-specific adjustments */ 27 | @media (max-width: 600px) { 28 | flex-direction: column; 29 | } 30 | 31 | /* title and socials */ 32 | .left-nav { 33 | display: flex; 34 | flex-direction: row; 35 | align-items: center; 36 | gap: 12px; 37 | font-size: 1.5rem; 38 | 39 | .socials { 40 | /* flex-container */ 41 | display: flex; 42 | flex-direction: row; 43 | flex-wrap: wrap; 44 | justify-content: flex-start; 45 | align-items: flex-end; 46 | gap: 8px; 47 | 48 | .social img { 49 | width: 16px; 50 | height: 16px; 51 | 52 | } 53 | 54 | /* Don't display background on hover */ 55 | a:hover { 56 | background-color: transparent; 57 | } 58 | } 59 | } 60 | 61 | /* navigation + theme toggle */ 62 | .right-nav { 63 | display: flex; 64 | flex-direction: row; 65 | flex-wrap: wrap; 66 | align-items: center; 67 | 68 | #dark-mode-toggle { 69 | margin-left: 0.5rem; 70 | padding: 0.1rem; 71 | 72 | > img { 73 | width: 16px; 74 | height: 16px; 75 | } 76 | 77 | /* Don't display background on hover */ 78 | &:hover { 79 | background-color: transparent; 80 | } 81 | } 82 | } 83 | } 84 | 85 | /* TODO: Is this still used? */ 86 | .logo { 87 | border-bottom: unset; 88 | background-image: unset; 89 | 90 | > img { 91 | border: unset; 92 | width: auto; 93 | height: 24px; 94 | vertical-align: middle; 95 | } 96 | 97 | &:hover { 98 | background-color: transparent; 99 | } 100 | } 101 | 102 | /* TODO: Is this still used? */ 103 | .meta { 104 | color: #999; 105 | display: flexbox; 106 | /* This changes the meta class to use flexbox, which ensures inline display */ 107 | align-items: center; 108 | /* Aligns items vertically in the middle */ 109 | flex-wrap: wrap; 110 | /* Allows items to wrap as needed */ 111 | } 112 | 113 | h1, 114 | h2, 115 | h3, 116 | h4, 117 | h5, 118 | h6 { 119 | font-family: monospace var(--header-font); 120 | font-size: 1.2rem; 121 | margin-top: 2em; 122 | } 123 | 124 | h1::before { 125 | color: var(--primary-color); 126 | content: "# "; 127 | } 128 | 129 | h2::before { 130 | color: var(--primary-color); 131 | content: "## "; 132 | } 133 | 134 | h3::before { 135 | color: var(--primary-color); 136 | content: "### "; 137 | } 138 | 139 | h4::before { 140 | color: var(--primary-color); 141 | content: "#### "; 142 | } 143 | 144 | h5::before { 145 | color: var(--primary-color); 146 | content: "##### "; 147 | } 148 | 149 | h6::before { 150 | color: var(--primary-color); 151 | content: "###### "; 152 | } 153 | -------------------------------------------------------------------------------- /sass/parts/_image.scss: -------------------------------------------------------------------------------- 1 | img { 2 | max-width: 100%; 3 | border-radius: 0.5rem; 4 | } 5 | 6 | figure { 7 | box-sizing: border-box; 8 | display: inline-block; 9 | margin: 0; 10 | max-width: 100%; 11 | } 12 | 13 | figure img { 14 | max-height: 500px; 15 | } 16 | 17 | @media screen and (min-width: 600px) { 18 | figure { 19 | padding: 0 40px; 20 | } 21 | } 22 | 23 | figure h4 { 24 | font-size: 1rem; 25 | margin: 0; 26 | margin-bottom: 1em; 27 | } 28 | 29 | figure h4::before { 30 | content: "↳ "; 31 | } 32 | -------------------------------------------------------------------------------- /sass/parts/_mermaid.scss: -------------------------------------------------------------------------------- 1 | .mermaid { 2 | text-align: center; 3 | margin-top: 1em; 4 | margin-bottom: 1em; 5 | 6 | strong { 7 | font-weight: bold; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /sass/parts/_misc.scss: -------------------------------------------------------------------------------- 1 | .primary-color { 2 | color: var(--primary-color); 3 | } 4 | 5 | .draft-label { 6 | color: var(--hover-color); 7 | text-decoration: none; 8 | padding: 2px 4px; 9 | border-radius: 4px; 10 | margin-left: 6px; 11 | background-color: var(--primary-color); 12 | } 13 | 14 | ::-moz-selection { 15 | background: var(--primary-color); 16 | color: var(--hover-color); 17 | text-shadow: none; 18 | } 19 | 20 | ::selection { 21 | background: var(--primary-color); 22 | color: var(--hover-color); 23 | } 24 | 25 | hr { 26 | color: var(--border-color); 27 | background: none; 28 | margin: 1.2rem auto; 29 | } 30 | 31 | blockquote { 32 | border-left: 3px solid var(--primary-color); 33 | color: #737373; 34 | margin: 0; 35 | padding-left: 1em; 36 | } 37 | 38 | a { 39 | color: inherit; 40 | text-decoration: none; 41 | 42 | /* use colored hovering */ 43 | &:hover { 44 | background-color: var(--primary-color); 45 | color: var(--hover-color); 46 | } 47 | 48 | /* hover link with child elements (e.g. code) */ 49 | &:hover > code { 50 | background-color: var(--primary-color); 51 | color: var(--hover-color); 52 | 53 | /* disable the border + vertical padding */ 54 | border: none; 55 | padding: 0 .2em; 56 | } 57 | 58 | /* disable colored hovering for video in /talks */ 59 | &.talk-video:hover { 60 | background-color: transparent; 61 | color: inherit; 62 | } 63 | } 64 | 65 | /* Only have colored links inside the text */ 66 | main { 67 | a { 68 | border-bottom: 2px solid var(--primary-color); 69 | 70 | // Make sure the underline is at the top 71 | position: relative; // needed for z-index 72 | z-index: 1; 73 | } 74 | 75 | // Disable colored links in: 76 | // - .meta 77 | // - header (socials, theme toggle) 78 | // - /talks 79 | // - /projects 80 | .meta a, .talks-grid a, .cards a { 81 | border-bottom: none; 82 | } 83 | 84 | // Don't display border on zola internal links on headers 85 | .zola-anchor { 86 | border-bottom: none; 87 | } 88 | } 89 | 90 | time { 91 | color: var(--text-1); 92 | } 93 | 94 | .post-list,.tag-list { 95 | > ul { 96 | margin: 0; 97 | padding: 1rem 0 0 0; 98 | } 99 | 100 | .list-item { 101 | margin-bottom: 0.5rem; 102 | list-style-type: none; 103 | } 104 | 105 | .post-header { 106 | display: grid; 107 | align-items: center; 108 | 109 | @media all and (max-width: 640px) { 110 | grid-template-rows: auto 1fr; 111 | } 112 | @media all and (min-width: 640px) { 113 | grid-template-columns: auto 1fr; 114 | gap: 1rem; 115 | } 116 | 117 | @media only screen and (max-width: 640px) { 118 | margin: 1.6rem 0px; 119 | } 120 | 121 | h1 { 122 | margin: 0; 123 | font-weight: normal; 124 | font-family: var(--header-font); 125 | 126 | a { 127 | border-bottom: none; 128 | } 129 | } 130 | 131 | time { 132 | font-family: var(--mono-text-font); 133 | text-align: left; 134 | 135 | margin: 0; 136 | } 137 | } 138 | 139 | } 140 | 141 | // change the line-through color 142 | del { 143 | text-decoration-color: var(--primary-color); 144 | text-decoration-thickness: 3px; 145 | } 146 | 147 | .MathJax_Display, 148 | .MJXc-display, 149 | .MathJax_SVG_Display { 150 | overflow-x: auto; 151 | overflow-y: hidden; 152 | } 153 | -------------------------------------------------------------------------------- /sass/parts/_note.scss: -------------------------------------------------------------------------------- 1 | :root { 2 | --note-header-bg: var(--bg-2); 3 | --note-header-color: var(--text-0); 4 | --note-content-bg: var(--bg-1); 5 | } 6 | 7 | .note-container { 8 | border-radius: 4px; 9 | overflow: hidden; 10 | margin: 1em 0; 11 | position: relative; 12 | border-left: 3px solid var(--primary-color); 13 | font-family: var(--paragraph-font); 14 | } 15 | 16 | .note-toggle, 17 | .note-header { 18 | color: var(--note-header-color); 19 | background-color: var(--note-header-bg); 20 | padding: 10px 25px; 21 | text-align: left; 22 | border: none; 23 | width: 100%; 24 | position: relative; 25 | outline: none; 26 | font-size: 1.2em; 27 | transition: background-color 0.3s ease; 28 | 29 | p { 30 | margin: 0; 31 | } 32 | 33 | .note-center { 34 | text-align: center; 35 | padding-right: 50px; 36 | } 37 | 38 | .note-icon, 39 | .note-icon { 40 | padding-left: 25px; 41 | } 42 | } 43 | 44 | .note-toggle { 45 | font-family: inherit; 46 | padding: 10px 25px; /* Stop header intersecting with note-icon */ 47 | cursor: pointer; 48 | position: relative; 49 | } 50 | 51 | .note-toggle::before { 52 | content: "▼"; 53 | position: absolute; 54 | right: 20px; 55 | /* Position the arrow to the right */ 56 | top: 50%; 57 | /* Center vertically */ 58 | transform: translateY(-50%); 59 | /* Center vertically */ 60 | } 61 | 62 | .note-toggle:hover, 63 | .note-toggle:focus { 64 | color: var(--note-header-color); 65 | background-color: var(--note-header-bg); 66 | outline: none; 67 | } 68 | 69 | .note-content { 70 | padding: 10px 20px; 71 | background-color: var(--note-content-bg); 72 | } 73 | 74 | .note-icon::before { 75 | content: "✎"; 76 | color: var(--primary-color); 77 | position: absolute; 78 | left: 20px; 79 | top: 50%; 80 | transform: translateY(-50%); 81 | } 82 | 83 | summary { 84 | padding-left: 0.5em; 85 | 86 | &:hover { 87 | background-color: var(--primary-color); 88 | color: var(--hover-color); 89 | cursor: pointer; 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /sass/parts/_search.scss: -------------------------------------------------------------------------------- 1 | $icon-size: 1.3rem; 2 | 3 | // Search button in navbar 4 | .search-button { 5 | background: none; 6 | border: none; 7 | padding: 2px; 8 | cursor: pointer; 9 | display: inline-flex; 10 | align-items: center; 11 | justify-content: center; 12 | margin-left: 0.25em; // Match your other nav items' margin 13 | 14 | img { 15 | border: none; 16 | } 17 | 18 | .search-icon { 19 | width: 16px; // Match your theme button size 20 | height: 16px; 21 | } 22 | 23 | &:hover { 24 | background-color: transparent; 25 | } 26 | } 27 | 28 | 29 | 30 | // Search modal 31 | .search-modal { 32 | display: none; 33 | position: fixed; 34 | top: 0; 35 | left: 0; 36 | width: 100%; 37 | height: 100%; 38 | z-index: 1000; 39 | background: rgba(0, 0, 0, 0.2); 40 | backdrop-filter: blur(8px); 41 | -webkit-backdrop-filter: blur(8px); 42 | 43 | #modal-content { 44 | position: relative; 45 | margin: 8% auto; 46 | width: 80%; 47 | max-width: 28rem; 48 | background-color: var(--bg-0); 49 | border: 1px solid var(--bg-1); 50 | border-radius: 8px; 51 | box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); 52 | } 53 | 54 | #searchBar { 55 | display: flex; 56 | align-items: center; 57 | padding: 1rem; 58 | gap: 0.5rem; 59 | 60 | #searchInput { 61 | flex: 1; 62 | padding: 0.75rem 2.5rem; 63 | font-size: 1rem; 64 | color: var(--text-0); 65 | background: var(--bg-1); 66 | border: 1px solid var(--bg-1); 67 | border-radius: 20px; 68 | width: 100%; 69 | 70 | &:focus { 71 | outline: none; 72 | border-color: var(--primary-color); 73 | } 74 | 75 | &::placeholder { 76 | color: var(--text-1); 77 | } 78 | } 79 | 80 | .close-icon { 81 | position: absolute; 82 | right: 1.5rem; 83 | display: none; 84 | padding: 4px; 85 | cursor: pointer; 86 | 87 | svg { 88 | width: $icon-size; 89 | height: $icon-size; 90 | fill: var(--text-1); 91 | } 92 | } 93 | } 94 | 95 | #results-container { 96 | display: none; 97 | border-top: 1px solid var(--bg-1); 98 | 99 | #results-info { 100 | padding: 0.5rem; 101 | color: var(--text-1); 102 | font-size: 0.8rem; 103 | text-align: center; 104 | } 105 | 106 | #results { 107 | max-height: 50vh; 108 | overflow-y: auto; 109 | 110 | > div { 111 | padding: 0.75rem 1rem; 112 | cursor: pointer; 113 | 114 | &[aria-selected="true"] { 115 | background: var(--primary-color); 116 | 117 | * { 118 | color: var(--hover-color) !important; 119 | } 120 | } 121 | 122 | span:first-child { 123 | display: block; 124 | color: var(--text-0); 125 | font-weight: 500; 126 | margin-bottom: 0.25rem; 127 | } 128 | 129 | span:nth-child(2) { 130 | display: block; 131 | color: var(--text-1); 132 | font-size: 0.9rem; 133 | } 134 | 135 | &:hover:not([aria-selected="true"]) { 136 | background: var(--bg-1); 137 | } 138 | } 139 | } 140 | } 141 | 142 | #modal-content { 143 | position: relative; 144 | margin: 8% auto; 145 | width: 80%; 146 | max-width: 28rem; 147 | background-color: var(--bg-0); 148 | border: 1px solid var(--bg-1); 149 | border-radius: 8px; 150 | box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); 151 | padding: 1rem; 152 | 153 | h1 { 154 | margin-top: 0; // Override the default 2em margin 155 | margin-bottom: 1rem; 156 | font-size: 1.2rem; 157 | 158 | &::before { 159 | color: var(--primary-color); 160 | content: "# "; 161 | } 162 | } 163 | } 164 | } 165 | 166 | #searchBar { 167 | position: relative; 168 | display: flex; 169 | align-items: center; 170 | padding: 1rem; 171 | 172 | // Clear button styling 173 | .clear-button { 174 | position: absolute; 175 | right: 1.5rem; 176 | background: none; 177 | border: none; 178 | padding: 4px; 179 | cursor: pointer; 180 | display: none; // Initially hidden, shown via JS when input has text 181 | width: 24px; 182 | height: 24px; 183 | 184 | svg { 185 | width: 100%; 186 | height: 100%; 187 | fill: var(--text-1); // Use your theme text color 188 | } 189 | 190 | &:hover { 191 | svg { 192 | fill: var(--primary-color); 193 | } 194 | } 195 | } 196 | 197 | // Make sure input accommodates the clear button 198 | #searchInput { 199 | padding-right: 2.5rem; // Give space for the clear button 200 | } 201 | } 202 | 203 | // Mobile adjustments 204 | @media only screen and (max-width: 600px) { 205 | .search-modal { 206 | #modal-content { 207 | margin: 4% auto; 208 | width: 92%; 209 | } 210 | 211 | #results { 212 | max-height: 70vh; 213 | } 214 | } 215 | } 216 | -------------------------------------------------------------------------------- /sass/parts/_table.scss: -------------------------------------------------------------------------------- 1 | table { 2 | border-spacing: 0; 3 | border-collapse: collapse; 4 | } 5 | 6 | table th { 7 | padding: 6px 13px; 8 | border: 1px solid #dfe2e5; 9 | font-size: large; 10 | } 11 | 12 | table td { 13 | padding: 6px 13px; 14 | border: 1px solid #dfe2e5; 15 | } 16 | -------------------------------------------------------------------------------- /sass/parts/_tags.scss: -------------------------------------------------------------------------------- 1 | /* Taxonomies for a post */ 2 | .tags { 3 | a::before { 4 | content: "#"; 5 | display: inline; 6 | white-space: nowrap !important; 7 | } 8 | } 9 | 10 | .authors { 11 | a::before { 12 | content: "@"; 13 | display: inline; 14 | white-space: nowrap !important; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /sass/parts/_talks.scss: -------------------------------------------------------------------------------- 1 | .talks-grid { 2 | display: grid; 3 | gap: 24px; 4 | padding: 12px 0; 5 | grid-template-rows: max-content; 6 | grid-auto-rows: 1fr; 7 | 8 | @media all and (min-width: 640px) and (max-width: 1023.98px) { 9 | grid-template-columns: repeat(2, minmax(0, 1fr)); 10 | } 11 | } 12 | 13 | .talk-card { 14 | background: var(--bg-1); 15 | border: 2px solid var(--border-color); 16 | border-radius: 10px; 17 | overflow: hidden; 18 | 19 | display: flex; 20 | flex-direction: row; 21 | 22 | @media all and (max-width: 1023.98px) { 23 | flex-direction: column; 24 | } 25 | 26 | .talk-video { 27 | position: relative; 28 | flex-shrink: 0; 29 | aspect-ratio: 16 / 9; 30 | 31 | @media all and (min-width: 1024px) { 32 | width: calc(205px * 16 / 9); 33 | min-height: 205px; 34 | height: 100%; 35 | } 36 | 37 | .talk-image { 38 | border: unset; 39 | position: absolute; 40 | width: 100%; 41 | height: 100%; 42 | color: transparent; 43 | top: 0; 44 | left: 0; 45 | bottom: 0; 46 | right: 0; 47 | display: block; 48 | object-fit: cover; 49 | filter: brightness(75%) grayscale(50%); 50 | } 51 | 52 | .video-play-btn { 53 | /* absolute inset-0 flex items-center justify-center */ 54 | position: absolute; 55 | top: 0; 56 | right: 0; 57 | bottom: 0; 58 | left: 0; 59 | display: flex; 60 | align-items: center; 61 | justify-content: center; 62 | 63 | .rounded-btn { 64 | /* bg-black bg-opacity-50 rounded-full p-4 */ 65 | background-color: var(--bg-2); 66 | border-radius: 9999px; 67 | padding: 1rem; 68 | 69 | display: flex; 70 | align-items: center; 71 | justify-content: center; 72 | } 73 | } 74 | } 75 | 76 | .talk-info { 77 | padding: 1.5rem; 78 | padding-top: 1rem; 79 | padding-bottom: 1rem; 80 | display: flex; 81 | flex-direction: column; 82 | flex-grow: 1; 83 | 84 | .talk-title { 85 | margin: 0; 86 | } 87 | 88 | .talk-description { 89 | margin-top: 0.5rem; 90 | margin-bottom: 1rem; 91 | overflow: hidden; 92 | display: -webkit-box; 93 | -webkit-box-orient: vertical; 94 | -webkit-line-clamp: 3; 95 | } 96 | 97 | .meta { 98 | display: flex; 99 | flex-wrap: wrap; 100 | gap: 8px; 101 | } 102 | } 103 | 104 | /* h1::before { 105 | content: ""; 106 | } */ 107 | } 108 | -------------------------------------------------------------------------------- /sass/parts/_toc.scss: -------------------------------------------------------------------------------- 1 | .toc { 2 | @media only screen and (max-width: 1365px) { 3 | display: none; 4 | } 5 | 6 | li, 7 | a { 8 | font-family: sans-serif; 9 | color: var(--text-2); 10 | transition: none; 11 | border-bottom: none; 12 | } 13 | 14 | a:hover { 15 | color: var(--hover-color) !important; 16 | transition: none; 17 | } 18 | 19 | .heading { 20 | font-weight: 700; 21 | } 22 | 23 | ul { 24 | list-style-type: none; 25 | padding-left: 1em; 26 | margin-top: 0; 27 | margin-bottom: 0; 28 | } 29 | & > ul { 30 | padding-left: 0; 31 | } 32 | 33 | li.selected, 34 | li.selected > a { 35 | color: var(--text-0); 36 | } 37 | 38 | .parent > a { 39 | color: var(--text-0); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /sass/theme/dark.scss: -------------------------------------------------------------------------------- 1 | $bg-0: #121212; 2 | 3 | // Dark theme needs a bigger contrast compared to the white theme. 4 | $bg-1: lighten($bg-0, 5%); 5 | $bg-2: lighten($bg-1, 10%); 6 | 7 | $text-0: lighten($bg-0, 87%); 8 | $text-1: lighten($bg-0, 60%); 9 | $text-2: lighten($bg-0, 40%); 10 | 11 | :root.dark { 12 | --text-0: #{$text-0}; 13 | --text-1: #{$text-1}; 14 | --text-2: #{$text-2}; 15 | 16 | --bg-0: #{$bg-0}; 17 | --bg-1: #{$bg-1}; 18 | --bg-2: #{$bg-2}; 19 | 20 | --border-color: var(--bg-2); 21 | 22 | --primary-color: #ef5350; 23 | --hover-color: white; 24 | 25 | --icon-filter: invert(1); 26 | 27 | .social > img, .search-button > img { 28 | filter: invert(1); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /sass/theme/light.scss: -------------------------------------------------------------------------------- 1 | $bg-0: #fff; 2 | $bg-1: darken($bg-0, 2%); 3 | $bg-2: darken($bg-1, 5%); 4 | 5 | $text-0: darken($bg-0, 87%); 6 | $text-1: darken($bg-0, 60%); 7 | $text-2: darken($bg-0, 30%); 8 | 9 | :root.light { 10 | --text-0: #{$text-0}; 11 | --text-1: #{$text-1}; 12 | --text-2: #{$text-2}; 13 | 14 | --bg-0: #{$bg-0}; 15 | --bg-1: #{$bg-1}; 16 | --bg-2: #{$bg-2}; 17 | 18 | --border-color: var(--bg-2); 19 | 20 | --primary-color: #ef5350; 21 | --hover-color: white; 22 | 23 | --icon-filter: none; 24 | } 25 | -------------------------------------------------------------------------------- /screenshot-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/not-matthias/apollo/6ae2c5ad18987891460c0b9908797f9a6e0cb8f6/screenshot-dark.png -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/not-matthias/apollo/6ae2c5ad18987891460c0b9908797f9a6e0cb8f6/screenshot.png -------------------------------------------------------------------------------- /static/fonts/JetbrainsMono/JetBrainsMono-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/not-matthias/apollo/6ae2c5ad18987891460c0b9908797f9a6e0cb8f6/static/fonts/JetbrainsMono/JetBrainsMono-Bold.ttf -------------------------------------------------------------------------------- /static/fonts/JetbrainsMono/JetBrainsMono-BoldItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/not-matthias/apollo/6ae2c5ad18987891460c0b9908797f9a6e0cb8f6/static/fonts/JetbrainsMono/JetBrainsMono-BoldItalic.ttf -------------------------------------------------------------------------------- /static/fonts/JetbrainsMono/JetBrainsMono-ExtraBold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/not-matthias/apollo/6ae2c5ad18987891460c0b9908797f9a6e0cb8f6/static/fonts/JetbrainsMono/JetBrainsMono-ExtraBold.ttf -------------------------------------------------------------------------------- /static/fonts/JetbrainsMono/JetBrainsMono-ExtraBoldItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/not-matthias/apollo/6ae2c5ad18987891460c0b9908797f9a6e0cb8f6/static/fonts/JetbrainsMono/JetBrainsMono-ExtraBoldItalic.ttf -------------------------------------------------------------------------------- /static/fonts/JetbrainsMono/JetBrainsMono-ExtraLight.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/not-matthias/apollo/6ae2c5ad18987891460c0b9908797f9a6e0cb8f6/static/fonts/JetbrainsMono/JetBrainsMono-ExtraLight.ttf -------------------------------------------------------------------------------- /static/fonts/JetbrainsMono/JetBrainsMono-ExtraLightItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/not-matthias/apollo/6ae2c5ad18987891460c0b9908797f9a6e0cb8f6/static/fonts/JetbrainsMono/JetBrainsMono-ExtraLightItalic.ttf -------------------------------------------------------------------------------- /static/fonts/JetbrainsMono/JetBrainsMono-Italic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/not-matthias/apollo/6ae2c5ad18987891460c0b9908797f9a6e0cb8f6/static/fonts/JetbrainsMono/JetBrainsMono-Italic.ttf -------------------------------------------------------------------------------- /static/fonts/JetbrainsMono/JetBrainsMono-Light.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/not-matthias/apollo/6ae2c5ad18987891460c0b9908797f9a6e0cb8f6/static/fonts/JetbrainsMono/JetBrainsMono-Light.ttf -------------------------------------------------------------------------------- /static/fonts/JetbrainsMono/JetBrainsMono-LightItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/not-matthias/apollo/6ae2c5ad18987891460c0b9908797f9a6e0cb8f6/static/fonts/JetbrainsMono/JetBrainsMono-LightItalic.ttf -------------------------------------------------------------------------------- /static/fonts/JetbrainsMono/JetBrainsMono-Medium.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/not-matthias/apollo/6ae2c5ad18987891460c0b9908797f9a6e0cb8f6/static/fonts/JetbrainsMono/JetBrainsMono-Medium.ttf -------------------------------------------------------------------------------- /static/fonts/JetbrainsMono/JetBrainsMono-MediumItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/not-matthias/apollo/6ae2c5ad18987891460c0b9908797f9a6e0cb8f6/static/fonts/JetbrainsMono/JetBrainsMono-MediumItalic.ttf -------------------------------------------------------------------------------- /static/fonts/JetbrainsMono/JetBrainsMono-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/not-matthias/apollo/6ae2c5ad18987891460c0b9908797f9a6e0cb8f6/static/fonts/JetbrainsMono/JetBrainsMono-Regular.ttf -------------------------------------------------------------------------------- /static/fonts/JetbrainsMono/JetBrainsMono-SemiBold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/not-matthias/apollo/6ae2c5ad18987891460c0b9908797f9a6e0cb8f6/static/fonts/JetbrainsMono/JetBrainsMono-SemiBold.ttf -------------------------------------------------------------------------------- /static/fonts/JetbrainsMono/JetBrainsMono-SemiBoldItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/not-matthias/apollo/6ae2c5ad18987891460c0b9908797f9a6e0cb8f6/static/fonts/JetbrainsMono/JetBrainsMono-SemiBoldItalic.ttf -------------------------------------------------------------------------------- /static/fonts/JetbrainsMono/JetBrainsMono-Thin.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/not-matthias/apollo/6ae2c5ad18987891460c0b9908797f9a6e0cb8f6/static/fonts/JetbrainsMono/JetBrainsMono-Thin.ttf -------------------------------------------------------------------------------- /static/fonts/JetbrainsMono/JetBrainsMono-ThinItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/not-matthias/apollo/6ae2c5ad18987891460c0b9908797f9a6e0cb8f6/static/fonts/JetbrainsMono/JetBrainsMono-ThinItalic.ttf -------------------------------------------------------------------------------- /static/fonts/SpaceGrotesk/SpaceGrotesk-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/not-matthias/apollo/6ae2c5ad18987891460c0b9908797f9a6e0cb8f6/static/fonts/SpaceGrotesk/SpaceGrotesk-Bold.ttf -------------------------------------------------------------------------------- /static/fonts/SpaceGrotesk/SpaceGrotesk-Light.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/not-matthias/apollo/6ae2c5ad18987891460c0b9908797f9a6e0cb8f6/static/fonts/SpaceGrotesk/SpaceGrotesk-Light.ttf -------------------------------------------------------------------------------- /static/fonts/SpaceGrotesk/SpaceGrotesk-Medium.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/not-matthias/apollo/6ae2c5ad18987891460c0b9908797f9a6e0cb8f6/static/fonts/SpaceGrotesk/SpaceGrotesk-Medium.ttf -------------------------------------------------------------------------------- /static/fonts/SpaceGrotesk/SpaceGrotesk-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/not-matthias/apollo/6ae2c5ad18987891460c0b9908797f9a6e0cb8f6/static/fonts/SpaceGrotesk/SpaceGrotesk-Regular.ttf -------------------------------------------------------------------------------- /static/fonts/SpaceGrotesk/SpaceGrotesk-SemiBold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/not-matthias/apollo/6ae2c5ad18987891460c0b9908797f9a6e0cb8f6/static/fonts/SpaceGrotesk/SpaceGrotesk-SemiBold.ttf -------------------------------------------------------------------------------- /static/fonts/zed-fonts/ZedDisplayL-Heavy.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/not-matthias/apollo/6ae2c5ad18987891460c0b9908797f9a6e0cb8f6/static/fonts/zed-fonts/ZedDisplayL-Heavy.woff2 -------------------------------------------------------------------------------- /static/fonts/zed-fonts/ZedTextL-Bold.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/not-matthias/apollo/6ae2c5ad18987891460c0b9908797f9a6e0cb8f6/static/fonts/zed-fonts/ZedTextL-Bold.woff2 -------------------------------------------------------------------------------- /static/fonts/zed-fonts/ZedTextL-Regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/not-matthias/apollo/6ae2c5ad18987891460c0b9908797f9a6e0cb8f6/static/fonts/zed-fonts/ZedTextL-Regular.woff2 -------------------------------------------------------------------------------- /static/icons/auto.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /static/icons/calendar.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /static/icons/code.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /static/icons/map-pin.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /static/icons/moon.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /static/icons/presentation.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /static/icons/search.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /static/icons/social/LICENSE: -------------------------------------------------------------------------------- 1 | All icons in this directory are downloaded from [FontAwesome](https://fontawesome.com/). They are part of the [free offer](https://fontawesome.com/license/free) and are licensed under [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/). -------------------------------------------------------------------------------- /static/icons/social/apple.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /static/icons/social/bitcoin.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /static/icons/social/bluesky.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /static/icons/social/deviantart.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /static/icons/social/diaspora.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /static/icons/social/discord.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /static/icons/social/discourse.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /static/icons/social/email.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /static/icons/social/ethereum.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /static/icons/social/etsy.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /static/icons/social/facebook.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /static/icons/social/fediverse.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /static/icons/social/github.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /static/icons/social/gitlab.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /static/icons/social/globe.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /static/icons/social/google-scholar.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /static/icons/social/google.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /static/icons/social/hacker-news.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /static/icons/social/instagram.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /static/icons/social/linkedin.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /static/icons/social/mastodon.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /static/icons/social/matrix.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /static/icons/social/orcid.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 8 | 9 | 10 | 11 | 13 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /static/icons/social/paypal.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /static/icons/social/pinterest.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /static/icons/social/quora.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /static/icons/social/reddit.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /static/icons/social/rss.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /static/icons/social/skype.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /static/icons/social/slack.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /static/icons/social/snapchat.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /static/icons/social/soundcloud.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /static/icons/social/spotify.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /static/icons/social/stack-exchange.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /static/icons/social/stack-overflow.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /static/icons/social/steam.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /static/icons/social/telegram.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /static/icons/social/twitter.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /static/icons/social/vimeo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /static/icons/social/whatsapp.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /static/icons/social/x-twitter.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /static/icons/social/youtube.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /static/icons/sun.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /static/images/characters/hooded.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/not-matthias/apollo/6ae2c5ad18987891460c0b9908797f9a6e0cb8f6/static/images/characters/hooded.png -------------------------------------------------------------------------------- /static/images/talks/default.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/not-matthias/apollo/6ae2c5ad18987891460c0b9908797f9a6e0cb8f6/static/images/talks/default.webp -------------------------------------------------------------------------------- /static/js/codeblock.js: -------------------------------------------------------------------------------- 1 | const successIcon = ` 2 | 3 | `; 4 | const errorIcon = ` 5 | 6 | `; 7 | const copyIcon = ` 8 | 9 | `; 10 | 11 | // Function to change icons after copying 12 | const changeIcon = (button, isSuccess) => { 13 | button.innerHTML = isSuccess ? successIcon : errorIcon; 14 | setTimeout(() => { 15 | button.innerHTML = copyIcon; // Reset to copy icon 16 | }, 2000); 17 | }; 18 | 19 | // Function to get code text from tables, skipping line numbers 20 | const getCodeFromTable = (codeBlock) => { 21 | return [...codeBlock.querySelectorAll('tr')] 22 | .map(row => row.querySelector('td:last-child')?.innerText ?? '') 23 | .join(''); 24 | }; 25 | 26 | // Function to get code text from non-table blocks 27 | const getNonTableCode = (codeBlock) => { 28 | return codeBlock.textContent.trim(); 29 | }; 30 | 31 | document.addEventListener('DOMContentLoaded', function () { 32 | // Select all `pre` elements containing `code` 33 | 34 | const observer = new IntersectionObserver((entries) => { 35 | entries.forEach(entry => { 36 | const pre = entry.target.parentNode; 37 | const clipboardBtn = pre.querySelector('.clipboard-button'); 38 | const label = pre.querySelector('.code-label'); 39 | 40 | if (clipboardBtn) { 41 | // Adjust the position of the clipboard button when the `code` is not fully visible 42 | clipboardBtn.style.right = entry.isIntersecting ? '5px' : `-${entry.boundingClientRect.right - pre.clientWidth + 5}px`; 43 | } 44 | 45 | if (label) { 46 | // Adjust the position of the label similarly 47 | label.style.right = entry.isIntersecting ? '0px' : `-${entry.boundingClientRect.right - pre.clientWidth}px`; 48 | } 49 | }); 50 | }, { 51 | root: null, // observing relative to viewport 52 | rootMargin: '0px', 53 | threshold: 1.0 // Adjust this to control when the callback fires 54 | }); 55 | 56 | document.querySelectorAll('pre code').forEach(codeBlock => { 57 | const pre = codeBlock.parentNode; 58 | pre.style.position = 'relative'; // Ensure parent `pre` can contain absolute elements 59 | 60 | // Create and append the copy button 61 | const copyBtn = document.createElement('button'); 62 | copyBtn.className = 'clipboard-button'; 63 | copyBtn.innerHTML = copyIcon; 64 | copyBtn.setAttribute('aria-label', 'Copy code to clipboard'); 65 | pre.appendChild(copyBtn); 66 | 67 | // Attach event listener to copy button 68 | copyBtn.addEventListener('click', async () => { 69 | // Determine if the code is in a table or not 70 | const isTable = codeBlock.querySelector('table'); 71 | const codeToCopy = isTable ? getCodeFromTable(codeBlock) : getNonTableCode(codeBlock); 72 | try { 73 | await navigator.clipboard.writeText(codeToCopy); 74 | changeIcon(copyBtn, true); // Show success icon 75 | } catch (error) { 76 | console.error('Failed to copy text: ', error); 77 | changeIcon(copyBtn, false); // Show error icon 78 | } 79 | }); 80 | 81 | const langClass = codeBlock.className.match(/language-(\w+)/); 82 | const lang = langClass ? langClass[1] : 'default'; 83 | 84 | // Create and append the label 85 | const label = document.createElement('span'); 86 | label.className = 'code-label label-' + lang; // Use the specific language class 87 | label.textContent = lang.toUpperCase(); // Display the language as label 88 | pre.appendChild(label); 89 | 90 | let ticking = false; 91 | pre.addEventListener('scroll', () => { 92 | if (!ticking) { 93 | window.requestAnimationFrame(() => { 94 | copyBtn.style.right = `-${pre.scrollLeft}px`; 95 | label.style.right = `-${pre.scrollLeft}px`; 96 | ticking = false; 97 | }); 98 | ticking = true; 99 | } 100 | }); 101 | 102 | }); 103 | }); 104 | -------------------------------------------------------------------------------- /static/js/imamu.js: -------------------------------------------------------------------------------- 1 | // https://cloud.umami.is/script.js 2 | !function(){"use strict";(t=>{const{screen:{width:e,height:a},navigator:{language:r},location:n,document:i,history:c}=t,{hostname:s,href:o,origin:u}=n,{currentScript:l,referrer:d}=i,h=o.startsWith("data:")?void 0:t.localStorage;if(!l)return;const m="data-",f=l.getAttribute.bind(l),p=f(m+"website-id"),g=f(m+"host-url"),y=f(m+"tag"),b="false"!==f(m+"auto-track"),v="true"===f(m+"exclude-search"),w=f(m+"domains")||"",S=w.split(",").map((t=>t.trim())),N=`${(g||"https://api-gateway.umami.dev"||l.src.split("/").slice(0,-1).join("/")).replace(/\/$/,"")}/api/send`,T=`${e}x${a}`,A=/data-umami-event-([\w-_]+)/,x=m+"umami-event",O=300,U=t=>{if(t){try{const e=decodeURI(t);if(e!==t)return e}catch(e){return t}return encodeURI(t)}},j=t=>{try{const{pathname:e,search:a,hash:r}=new URL(t,n.href);t=e+a+r}catch(t){}return v?t.split("?")[0]:t},k=()=>({website:p,hostname:s,screen:T,language:r,title:U(q),url:U(W),referrer:U(_),tag:y||void 0}),E=(t,e,a)=>{a&&(_=W,W=j(a.toString()),W!==_&&setTimeout(K,O))},L=()=>!p||h&&h.getItem("umami.disabled")||w&&!S.includes(s),$=async(t,e="event")=>{if(L())return;const a={"Content-Type":"application/json"};void 0!==B&&(a["x-umami-cache"]=B);try{const r=await fetch(N,{method:"POST",body:JSON.stringify({type:e,payload:t}),headers:a}),n=await r.text();return B=n}catch(t){}},I=()=>{D||(K(),(()=>{const t=(t,e,a)=>{const r=t[e];return(...e)=>(a.apply(null,e),r.apply(t,e))};c.pushState=t(c,"pushState",E),c.replaceState=t(c,"replaceState",E)})(),(()=>{const t=new MutationObserver((([t])=>{q=t&&t.target?t.target.text:void 0})),e=i.querySelector("head > title");e&&t.observe(e,{subtree:!0,characterData:!0,childList:!0})})(),i.addEventListener("click",(async t=>{const e=t=>["BUTTON","A"].includes(t),a=async t=>{const e=t.getAttribute.bind(t),a=e(x);if(a){const r={};return t.getAttributeNames().forEach((t=>{const a=t.match(A);a&&(r[a[1]]=e(t))})),K(a,r)}},r=t.target,i=e(r.tagName)?r:((t,a)=>{let r=t;for(let t=0;t{s||(n.href=e)}))}else if("BUTTON"===i.tagName)return a(i)}}),!0),D=!0)},K=(t,e)=>$("string"==typeof t?{...k(),name:t,data:"object"==typeof e?e:void 0}:"object"==typeof t?t:"function"==typeof t?t(k()):k()),R=t=>$({...k(),data:t},"identify");t.umami||(t.umami={track:K,identify:R});let B,D,W=j(o),_=d.startsWith(u)?"":d,q=i.title;b&&!L()&&("complete"===i.readyState?I():i.addEventListener("readystatechange",I,!0))})(window)}(); -------------------------------------------------------------------------------- /static/js/main.js: -------------------------------------------------------------------------------- 1 | mmdElements = document.getElementsByClassName("mermaid"); 2 | const mmdHTML = []; 3 | for (let i = 0; i < mmdElements.length; i++) { 4 | mmdHTML[i] = mmdElements[i].innerHTML; 5 | } 6 | 7 | function mermaidRender(theme) { 8 | if (theme == "dark") { 9 | initOptions = { 10 | startOnLoad: false, 11 | theme: "dark", 12 | }; 13 | } else { 14 | initOptions = { 15 | startOnLoad: false, 16 | theme: "neutral", 17 | }; 18 | } 19 | for (let i = 0; i < mmdElements.length; i++) { 20 | delete mmdElements[i].dataset.processed; 21 | mmdElements[i].innerHTML = mmdHTML[i]; 22 | } 23 | mermaid.initialize(initOptions); 24 | mermaid.run(); 25 | } 26 | -------------------------------------------------------------------------------- /static/js/note.js: -------------------------------------------------------------------------------- 1 | document.addEventListener('DOMContentLoaded', function() { 2 | document.querySelectorAll('.note-toggle').forEach(function(toggleButton) { 3 | var content = toggleButton.nextElementSibling; 4 | var isHidden = content.style.display === 'none'; 5 | toggleButton.setAttribute('aria-expanded', !isHidden); 6 | 7 | toggleButton.addEventListener('click', function() { 8 | var expanded = this.getAttribute('aria-expanded') === 'true'; 9 | this.setAttribute('aria-expanded', !expanded); 10 | content.style.display = expanded ? 'none' : 'block'; 11 | }); 12 | }); 13 | }); 14 | 15 | -------------------------------------------------------------------------------- /static/js/themetoggle.js: -------------------------------------------------------------------------------- 1 | function setTheme(mode) { 2 | localStorage.setItem("theme-storage", mode); 3 | } 4 | 5 | // Functions needed for the theme toggle 6 | // 7 | 8 | function toggleTheme() { 9 | const currentTheme = getSavedTheme(); 10 | if (currentTheme === "light") { 11 | setTheme("dark"); 12 | updateItemToggleTheme(); 13 | } else if (currentTheme === "dark") { 14 | setTheme("auto"); 15 | updateItemToggleTheme(); 16 | } else { 17 | setTheme("light"); 18 | updateItemToggleTheme(); 19 | } 20 | } 21 | 22 | function updateItemToggleTheme() { 23 | let mode = getSavedTheme(); 24 | 25 | const darkModeStyle = document.getElementById("darkModeStyle"); 26 | if (darkModeStyle) { 27 | if (mode === "dark" || (mode === "auto" && getSystemPrefersDark())) { 28 | darkModeStyle.disabled = false; 29 | } else { 30 | darkModeStyle.disabled = true; 31 | } 32 | } 33 | 34 | const sunIcon = document.getElementById("sun-icon"); 35 | const moonIcon = document.getElementById("moon-icon"); 36 | const autoIcon = document.getElementById("auto-icon"); 37 | if (sunIcon && moonIcon && autoIcon) { 38 | sunIcon.style.display = (mode === "light") ? "block" : "none"; 39 | moonIcon.style.display = (mode === "dark") ? "block" : "none"; 40 | autoIcon.style.display = (mode === "auto") ? "block" : "none"; 41 | 42 | if (mode === "auto") { 43 | autoIcon.style.filter = getSystemPrefersDark() ? "invert(1)" : "invert(0)"; 44 | } else { 45 | autoIcon.style.filter = "none"; 46 | } 47 | } 48 | 49 | let htmlElement = document.querySelector("html"); 50 | if (mode === "dark" || (mode === "auto" && getSystemPrefersDark())) { 51 | htmlElement.classList.remove("light") 52 | htmlElement.classList.add("dark") 53 | } else { 54 | htmlElement.classList.remove("dark") 55 | htmlElement.classList.add("light") 56 | } 57 | } 58 | 59 | function getSystemPrefersDark() { 60 | return window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches; 61 | } 62 | 63 | function getSavedTheme() { 64 | let currentTheme = localStorage.getItem("theme-storage"); 65 | if(!currentTheme) { 66 | currentTheme = getSystemPrefersDark() ? "dark" : "light"; 67 | } 68 | 69 | return currentTheme; 70 | } 71 | 72 | // Update the toggle theme on page load 73 | updateItemToggleTheme(); 74 | 75 | // Listen for system theme changes in auto mode 76 | if (window.matchMedia) { 77 | window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', function(e) { 78 | if (getSavedTheme() === "auto") { 79 | updateItemToggleTheme(); 80 | } 81 | }); 82 | } 83 | -------------------------------------------------------------------------------- /static/js/toc.js: -------------------------------------------------------------------------------- 1 | document.addEventListener("DOMContentLoaded", () => { 2 | let observer = new IntersectionObserver(handler, { 3 | threshold: [0], 4 | }); 5 | let paragraphs = [...document.querySelectorAll("section > *")]; 6 | let submenu = [...document.querySelectorAll(".toc a")]; 7 | 8 | function previousHeaderId(e) { 9 | for (; e && !e.matches("h1, h2, h3, h4"); ) e = e.previousElementSibling; 10 | return e?.id; 11 | } 12 | let paragraphMenuMap = paragraphs.reduce((e, t) => { 13 | let n = previousHeaderId(t); 14 | if (((t.previousHeader = n), n)) { 15 | let t = submenu.find((e) => decodeURIComponent(e.hash) === "#" + n); 16 | e[n] = t; 17 | } 18 | return e; 19 | }, {}); 20 | 21 | paragraphs.forEach((e) => observer.observe(e)); 22 | let selection; 23 | function handler(e) { 24 | selection = (selection || e).map( 25 | (t) => e.find((e) => e.target === t.target) || t, 26 | ); 27 | for (s of selection) 28 | s.isIntersecting || 29 | paragraphMenuMap[ 30 | s.target.previousHeader 31 | ]?.parentElement.classList.remove("selected", "parent"); 32 | for (s of selection) 33 | if (s.isIntersecting) { 34 | let e = paragraphMenuMap[s.target.previousHeader]?.closest("li"); 35 | if ((e?.classList.add("selected"), e === void 0)) continue; 36 | // Find the anchor element within the list item 37 | let t = e.querySelector("a"); 38 | if (t) { 39 | t.scrollIntoView({ 40 | block: "nearest", 41 | inline: "nearest", 42 | }); 43 | } 44 | for (; e; ) { 45 | e?.classList.add("parent"), (e = e.parentElement.closest("li")); 46 | } 47 | } 48 | } 49 | }); 50 | -------------------------------------------------------------------------------- /static/processed_images/default.0b1c95d25c30c7b7.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/not-matthias/apollo/6ae2c5ad18987891460c0b9908797f9a6e0cb8f6/static/processed_images/default.0b1c95d25c30c7b7.webp -------------------------------------------------------------------------------- /static/processed_images/project-1.c835c9e2f29627ee.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/not-matthias/apollo/6ae2c5ad18987891460c0b9908797f9a6e0cb8f6/static/processed_images/project-1.c835c9e2f29627ee.webp -------------------------------------------------------------------------------- /templates/404.html: -------------------------------------------------------------------------------- 1 | {% extends "page.html" %} 2 | 3 | {% block main_content %} 4 |
5 | {{ post_macros::page_header(title="404") }} 6 | Page not found :( 7 |
8 | {% endblock main_content %} 9 | -------------------------------------------------------------------------------- /templates/_giscus_script.html: -------------------------------------------------------------------------------- 1 | 3 | -------------------------------------------------------------------------------- /templates/base.html: -------------------------------------------------------------------------------- 1 | {% import "macros/macros.html" as post_macros %} 2 | 3 | 4 | 5 | 6 | {% include "partials/header.html" %} 7 | 8 | 9 |
10 | {% block left_content %} 11 | {% endblock left_content %} 12 |
13 | 14 |
15 | {% include "partials/nav.html" %} 16 | 17 | {# Post page is the default #} 18 | {% block main_content %} 19 | Nothing here?! 20 | {% endblock main_content %} 21 | 22 | {% if page.extra.comment is defined %} 23 | {% set show_comment = page.extra.comment %} 24 | {% else %} 25 | {% set show_comment = false %} 26 | {% endif %} 27 | 28 | {% if show_comment %} 29 |
30 |
31 | {% include "_giscus_script.html" %} 32 | {% endif %} 33 |
34 | 35 |
36 | {% block right_content %} 37 | {% endblock right_content %} 38 |
39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /templates/homepage.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% macro home_page(section) %} 4 |
5 |
6 |
7 | {{ post_macros::page_header(title=section.title) }} 8 | {{ section.content | safe }} 9 |
10 |
11 |
12 | {% endmacro home_page %} 13 | 14 | {% block main_content %} 15 | {{ self::home_page(section=section) }} 16 | {% endblock main_content %} 17 | -------------------------------------------------------------------------------- /templates/index.html: -------------------------------------------------------------------------------- 1 | {% extends "section.html" %} 2 | -------------------------------------------------------------------------------- /templates/macros/components.html: -------------------------------------------------------------------------------- 1 | {% macro icon_button(href, text, icon="", icon_svg="", class="", target="_blank", rel="noopener", disabled=false, icon_path="icons/social/") %} 2 | {% if disabled %} 3 | 4 | {% if icon %} 5 | {{ icon }} 6 | {% elif icon_svg %} 7 | {{ icon_svg | safe }} 8 | {% endif %} 9 | {{ text }} 10 | 11 | {% else %} 12 | 16 | {% if icon %} 17 | {{ icon }} 18 | {% elif icon_svg %} 19 | {{ icon_svg | safe }} 20 | {% endif %} 21 | {{ text }} 22 | 23 | {% endif %} 24 | {% endmacro icon_button %} 25 | -------------------------------------------------------------------------------- /templates/macros/macros.html: -------------------------------------------------------------------------------- 1 | {% macro list_post(page) %} 2 |
  • 3 |
    4 |
    5 | 6 | 7 |
    8 |

    9 | {{ page.title }} 10 | 11 | {% if page.draft %} 12 | DRAFT 13 | {% endif %} 14 |

    15 | 16 |
    17 |
    18 | {% if page.description %} 19 | {{ page.description }} 20 | {% elif page.summary %} 21 | {{ page.summary | safe }}… 22 | {% else %} 23 | {% set hide_read_more = true %} 24 | {% endif %} 25 |
    26 | 27 | {% if not hide_read_more %} 28 | Read more ⟶ 29 | {% endif %} 30 |
    31 |
    32 |
    33 |
    34 |
  • 35 | {% endmacro list_post %} 36 | 37 | {% macro list_posts(pages) %} 38 | 43 | {% endmacro list_posts %} 44 | 45 | {% macro page_header(title) %} 46 | {% if title %} 47 | 50 | {% endif %} 51 | {% endmacro page_header %} 52 | 53 | {% macro site_title() %} 54 | {{ config.title | default(value="Home") }} 55 | {% endmacro %} 56 | -------------------------------------------------------------------------------- /templates/page.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% macro content(page) %} 4 |
    5 |
    6 |
    7 |
    8 | {#

    {{ page.title }}

    #} 9 | {{ post_macros::page_header(title=page.title) }} 10 | 11 |
    12 | {% if page.date %} 13 | Posted on 14 | {% endif %} 15 | 16 | 17 | {% if page.updated %} 18 | :: Updated on 19 | {% endif %} 20 | 21 | {% if page.extra.read_time %} 22 | :: Min Read 23 | {% endif %} 24 | 25 | {% if config.extra.word_count and page.word_count %} 26 | :: {{ page.word_count }} Words 27 | {% endif %} 28 | 29 | {# View the page on GitHub #} 30 | {% if page.extra.repo_view | default(value=config.extra.repo_view) | default(value=false) %} 31 | {# Use the page's repo_url if defined, otherwise use the global edit_repo_url #} 32 | {% if page.extra.repo_url is defined %} 33 | {% set repo_url = page.extra.repo_url %} 34 | {% elif config.extra.repo_url is defined %} 35 | {% set repo_url = config.extra.repo_url %} 36 | {% else %} 37 | {% set repo_url = false %} 38 | {% endif %} 39 | 40 | {% if repo_url %} 41 | {% set final_url = repo_url ~ page.relative_path %} 42 | :: Source Code 43 | {% endif %} 44 | {% endif %} 45 | 46 | {# Inline display of authors directly after the date #} 47 | {% if page.taxonomies and page.taxonomies.authors %} 48 | :: 49 | 50 | {%- for author in page.taxonomies.authors %} 51 | 53 | {% endfor %} 54 | 55 | {% endif %} 56 | 57 | {# Inline display of tags directly after the authors #} 58 | {% if page.taxonomies and page.taxonomies.tags %} 59 | :: 60 | 61 | {%- for tag in page.taxonomies.tags %} 62 | 64 | {% endfor %} 65 | 66 | {% endif %} 67 | 68 | {% if page.draft %} 69 | DRAFT 70 | {% endif %} 71 | 72 |
    73 |
    74 | 75 | {% if page.extra.tldr %} 76 |
    77 | tl;dr: 78 | {{ page.extra.tldr }} 79 |
    80 | {% endif %} 81 | 82 |
    83 | {{ page.content | safe }} 84 |
    85 |
    86 |
    87 |
    88 | {% endmacro content %} 89 | 90 | 91 | {% block main_content %} 92 | {{ self::content(page=page) }} 93 | {% endblock main_content %} 94 | 95 | {% block right_content %} 96 | {# Optional table of contents #} 97 | {% if config.extra.toc | default(value=false) and page.toc %} 98 | {% include "partials/toc.html" %} 99 | {% endif %} 100 | {% endblock right_content %} 101 | -------------------------------------------------------------------------------- /templates/partials/nav.html: -------------------------------------------------------------------------------- 1 | 98 | -------------------------------------------------------------------------------- /templates/partials/toc.html: -------------------------------------------------------------------------------- 1 |
    2 |
    3 | Table of Contents 4 |
    5 | 34 |
    35 | -------------------------------------------------------------------------------- /templates/section.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block main_content %} 4 | {% if section.extra.section_path -%} 5 | {% set section = get_section(path=section.extra.section_path) %} 6 | {% endif -%} 7 | 8 | {% block title %} 9 | {{ post_macros::page_header(title=section.title) }} 10 | {% endblock title %} 11 | 12 | {% if section.content %} 13 |
    14 | {{ section.content | safe }} 15 |
    16 | {% endif %} 17 | 18 | {% block post_list %} 19 |
    20 | {%- if paginator %} 21 | {%- set show_pages = paginator.pages -%} 22 | {% else %} 23 | {%- set show_pages = section.pages -%} 24 | {% endif -%} 25 | 26 | {{ post_macros::list_posts(pages=show_pages) }} 27 |
    28 | {% endblock post_list %} 29 | 30 | {% if paginator %} 31 | 44 | {% endif %} 45 | {% endblock main_content %} 46 | -------------------------------------------------------------------------------- /templates/shortcodes/character.html: -------------------------------------------------------------------------------- 1 | {% set character_name = name | default(value="hooded") %} 2 | {% set character_text = body | default(value="") %} 3 | {% set character_type = type | default(value="comment") %} 4 | {% set character_position = position | default(value="right") %} 5 | {% set character_image = image | default(value="") %} 6 | 7 |
    8 |
    9 | {% if character_image and character_image != "" %} 10 | {{ character_name }} 14 | {% elif character_name == "hooded" %} 15 | hooded 19 | {% else %} 20 | {{ character_name }} 21 | {% endif %} 22 |
    23 |
    24 |
    25 | {{ character_text | markdown | safe }} 26 |
    27 |
    28 |
    29 | -------------------------------------------------------------------------------- /templates/shortcodes/image.html: -------------------------------------------------------------------------------- 1 | {% set image_meta = get_image_metadata(path=path) %} 2 | 3 | {% set width = width | default(value=image_meta.width) %} 4 | {% set height = width | default(value=image_meta.height) %} 5 | {% set op = op | default(value='fit_width') %} 6 | {% set format = format | default(value='avif') %} 7 | {% set quality = quality | default(value=80) %} 8 | {% set img = resize_image(path=path, width=width, height=height, op=op, format=format, quality=quality) %} 9 | 10 | {% set loading = loading | default(value='lazy') %} 11 | {% set decoding = decoding | default(value='async') %} 12 | {% set style = style | default(value='') %} 13 | {{ alt }} 20 | -------------------------------------------------------------------------------- /templates/shortcodes/mermaid.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
    4 |   {{ body | safe }}
    5 | 
    6 | -------------------------------------------------------------------------------- /templates/shortcodes/note.html: -------------------------------------------------------------------------------- 1 |
    2 | {% if clickable | default(value=false) %} 3 | 14 | 15 | {% if hidden | default(value=false) %} 16 | 39 | -------------------------------------------------------------------------------- /templates/taxonomy_list.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block main_content %} 4 |
    5 | {% block title %} 6 | {{ post_macros::page_header(title=taxonomy.name | capitalize) }} 7 | {% endblock title %} 8 | 9 |
    10 | {% set sort_by = config.extra.taxonomies.sort_by | default(value="name") %} 11 | {% set terms = terms | default(value=[]) | sort(attribute=sort_by) %} 12 | 13 | {% if config.extra.taxonomies.reverse | default(value=false) %} 14 | {% set terms = terms | reverse %} 15 | {% endif %} 16 | 17 |
      18 | {%- for term in terms %} 19 |
      20 |

      21 | {{ term.name }} 22 |

      23 | 24 | {{ term.page_count }} page{{ term.page_count | pluralize }} 25 |
      26 | {% endfor -%} 27 |
    28 |
    29 |
    30 | {% endblock main_content %} 31 | -------------------------------------------------------------------------------- /templates/taxonomy_single.html: -------------------------------------------------------------------------------- 1 | {% extends "index.html" %} 2 | 3 | {% macro list_tag_posts(pages, tag_name=false) %} 4 | {% if tag_name %} 5 | 8 | {% else %} 9 | 12 | {% endif %} 13 | 14 |
    15 | {{ post_macros::list_posts(pages=pages) }} 16 |
    17 | {% endmacro %} 18 | 19 | {% block main_content %} 20 | {{ self::list_tag_posts(pages=term.pages, tag_name=term.name) }} 21 | {% endblock main_content %} 22 | -------------------------------------------------------------------------------- /tests/content/blog-posts.spec.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from '@playwright/test'; 2 | 3 | test.describe('Blog Posts', () => { 4 | test('posts page displays list and individual posts open', async ({ page }) => { 5 | await page.goto('/posts'); 6 | await page.waitForLoadState('domcontentloaded'); 7 | 8 | // Check for posts listing - posts use list-item class 9 | const posts = page.locator('.list-item, .card'); 10 | const postCount = await posts.count(); 11 | expect(postCount).toBeGreaterThan(0); 12 | 13 | // Verify post has title and link - title is in h1.title 14 | const titleLink = posts.first().locator('h1.title a, h1.card-title a').first(); 15 | await expect(titleLink).toBeVisible(); 16 | 17 | // Open first post 18 | await titleLink.click(); 19 | await expect(page).toHaveURL(/\/posts\//); 20 | 21 | // Verify post content exists 22 | const content = page.locator('article, main, .content').first(); 23 | await expect(content).toBeVisible(); 24 | 25 | // Check for content elements 26 | const contentElements = content.locator('p, h1, h2, h3, h4, h5, h6, ul, ol, pre'); 27 | expect(await contentElements.count()).toBeGreaterThan(0); 28 | }); 29 | 30 | test('post metadata and tags work', async ({ page }) => { 31 | await page.goto('/posts'); 32 | await page.waitForLoadState('domcontentloaded'); 33 | 34 | const firstPostLink = page.locator('.list-item h1.title a, .card h1.card-title a').first(); 35 | await firstPostLink.click(); 36 | 37 | // Check for metadata - using .meta class 38 | const metadata = page.locator('.meta, time, .post-date').first(); 39 | if (await metadata.isVisible()) { 40 | const text = await metadata.textContent(); 41 | if (text) { 42 | expect(text).toMatch(/\d{4}/); // Should contain a year 43 | } 44 | } 45 | 46 | // Test tags if present 47 | const tags = page.locator('.tags a, .tag'); 48 | const tagCount = await tags.count(); 49 | 50 | if (tagCount > 0) { 51 | const firstTag = tags.first(); 52 | await firstTag.click(); 53 | await expect(page).toHaveURL(/\/tags\//); 54 | } 55 | }); 56 | 57 | test('pagination works correctly', async ({ page }) => { 58 | await page.goto('/posts'); 59 | await page.waitForLoadState('domcontentloaded'); 60 | 61 | // Look for next link 62 | const nextLink = page.locator('.pagination a, .page-link').filter({ hasText: /next|→/i }).first(); 63 | 64 | if (await nextLink.isVisible()) { 65 | await nextLink.click(); 66 | 67 | // Should still be on posts page with posts displayed 68 | await expect(page).toHaveURL(/\/posts/); 69 | const posts = page.locator('.list-item, .card'); 70 | expect(await posts.count()).toBeGreaterThan(0); 71 | } 72 | }); 73 | }); 74 | -------------------------------------------------------------------------------- /tests/content/configuration.spec.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from '@playwright/test'; 2 | 3 | test.describe('Configuration Features', () => { 4 | test.beforeEach(async ({ page }) => { 5 | await page.goto('/'); 6 | await page.waitForLoadState('domcontentloaded'); 7 | }); 8 | 9 | test('search functionality works when enabled', async ({ page }) => { 10 | const searchButton = page.locator('[data-search], .search-button, button:has-text("Search")').first(); 11 | 12 | if (await searchButton.isVisible()) { 13 | await searchButton.click(); 14 | 15 | const searchModal = page.locator('.search-modal, #search-modal, [role="dialog"]').first(); 16 | await expect(searchModal).toBeVisible(); 17 | 18 | // Test search input 19 | const modalSearchInput = searchModal.locator('input').first(); 20 | await modalSearchInput.fill('test'); 21 | } 22 | }); 23 | 24 | test('theme switching functionality works', async ({ page }) => { 25 | const themeToggle = page.locator('.theme-toggle, .theme-switcher, button[aria-label*="theme"]').first(); 26 | 27 | if (await themeToggle.isVisible()) { 28 | const initialClass = await page.evaluate(() => document.documentElement.className); 29 | 30 | await themeToggle.click(); 31 | 32 | const newClass = await page.evaluate(() => document.documentElement.className); 33 | expect(newClass).not.toBe(initialClass); 34 | } 35 | }); 36 | 37 | test('table of contents is generated when enabled', async ({ page }) => { 38 | await page.goto('/posts'); 39 | await page.waitForLoadState('domcontentloaded'); 40 | 41 | const postLinks = page.locator('a[href*="/posts/"]').first(); 42 | if (await postLinks.isVisible()) { 43 | await postLinks.click(); 44 | 45 | const toc = page.locator('.toc, .table-of-contents, #toc, #table-of-contents').first(); 46 | 47 | if (await toc.isVisible()) { 48 | const tocLinks = toc.locator('a'); 49 | const linkCount = await tocLinks.count(); 50 | expect(linkCount).toBeGreaterThan(0); 51 | 52 | // Test clicking a TOC link 53 | await tocLinks.first().click(); 54 | } 55 | } 56 | }); 57 | 58 | test('social media links are configured', async ({ page }) => { 59 | const socialLinks = page.locator('a[href*="github"], a[href*="twitter"], a[href*="linkedin"], .social-links a, .socials a'); 60 | 61 | if (await socialLinks.count() > 0) { 62 | const firstLink = socialLinks.first(); 63 | const href = await firstLink.getAttribute('href'); 64 | 65 | expect(href).toBeTruthy(); 66 | expect(href).toMatch(/^https?:\/\//); 67 | } 68 | }); 69 | 70 | test('RSS feed links are available', async ({ page }) => { 71 | const feedLinks = await page.evaluate(() => { 72 | const links = document.querySelectorAll('link[type*="rss"], link[type*="atom"]'); 73 | return Array.from(links).map(link => ({ 74 | type: link.getAttribute('type'), 75 | href: link.getAttribute('href'), 76 | })); 77 | }); 78 | 79 | if (feedLinks.length > 0) { 80 | const response = await page.request.get(feedLinks[0].href); 81 | expect(response.status()).toBe(200); 82 | } 83 | }); 84 | }); 85 | -------------------------------------------------------------------------------- /tests/content/fonts.spec.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from '@playwright/test'; 2 | 3 | test.describe('Font Loading and Display', () => { 4 | test.beforeEach(async ({ page }) => { 5 | await page.goto('/'); 6 | await page.waitForLoadState('domcontentloaded'); 7 | }); 8 | 9 | test('font files are accessible', async ({ page, request }) => { 10 | const baseUrl = page.url().replace(/\/$/, ''); 11 | 12 | // Test critical font files 13 | const fontTests = [ 14 | '/fonts/zed-fonts/ZedTextL-Regular.woff2', 15 | '/fonts/JetbrainsMono/JetBrainsMono-Regular.ttf', 16 | ]; 17 | 18 | for (const fontPath of fontTests) { 19 | const response = await request.get(`${baseUrl}${fontPath}`); 20 | expect(response.status()).toBe(200); 21 | expect(response.headers()['content-type']).toContain('font'); 22 | } 23 | }); 24 | 25 | test('code elements use monospace fonts', async ({ page }) => { 26 | // Navigate to a page with code 27 | await page.goto('/posts'); 28 | await page.waitForLoadState('domcontentloaded'); 29 | 30 | const firstPostLink = page.locator('article a, .post a, .post-title a').first(); 31 | if (await firstPostLink.isVisible()) { 32 | await firstPostLink.click(); 33 | 34 | const codeElements = page.locator('code, pre').first(); 35 | if (await codeElements.count() > 0) { 36 | const codeFontFamily = await codeElements.evaluate(el => 37 | getComputedStyle(el).fontFamily 38 | ); 39 | 40 | // Code should use JetBrains Mono or monospace fallback 41 | expect(codeFontFamily).toMatch(/Jetbrains Mono|monospace/i); 42 | } 43 | } 44 | }); 45 | }); 46 | -------------------------------------------------------------------------------- /tests/content/pages-and-features.spec.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from '@playwright/test'; 2 | 3 | test.describe('Pages and Features', () => { 4 | test('homepage loads correctly', async ({ page }) => { 5 | await page.goto('/'); 6 | await page.waitForLoadState('domcontentloaded'); 7 | 8 | // Homepage should have main content 9 | const main = page.locator('main'); 10 | await expect(main).toBeVisible(); 11 | 12 | // Should have some content 13 | const content = page.locator('article, .body, main'); 14 | await expect(content.first()).toBeVisible(); 15 | }); 16 | 17 | test('projects page loads and displays projects', async ({ page }) => { 18 | await page.goto('/projects'); 19 | await page.waitForLoadState('domcontentloaded'); 20 | 21 | // Page should have a heading or main content 22 | const main = page.locator('main'); 23 | await expect(main).toBeVisible(); 24 | 25 | // Look for project items (cards or list items) 26 | const projects = page.locator('.card, .list-item'); 27 | const projectCount = await projects.count(); 28 | 29 | if (projectCount > 0) { 30 | // Each project should have a title 31 | const title = projects.first().locator('h1, h2, h3').first(); 32 | await expect(title).toBeVisible(); 33 | } 34 | }); 35 | 36 | test('tags page loads and displays tags', async ({ page }) => { 37 | await page.goto('/tags'); 38 | await page.waitForLoadState('domcontentloaded'); 39 | 40 | // Page should have main content 41 | const main = page.locator('main'); 42 | await expect(main).toBeVisible(); 43 | 44 | // Look for tags 45 | const tags = page.locator('a[href*="/tags/"]'); 46 | const tagCount = await tags.count(); 47 | 48 | if (tagCount > 0) { 49 | expect(tagCount).toBeGreaterThan(0); 50 | 51 | // Tags should be clickable 52 | const firstTag = tags.first(); 53 | const href = await firstTag.getAttribute('href'); 54 | expect(href).toBeTruthy(); 55 | } 56 | }); 57 | 58 | test('tag filtering works correctly', async ({ page }) => { 59 | await page.goto('/tags'); 60 | await page.waitForLoadState('domcontentloaded'); 61 | 62 | const tags = page.locator('a[href*="/tags/"]'); 63 | const tagCount = await tags.count(); 64 | 65 | if (tagCount > 0) { 66 | const firstTag = tags.first(); 67 | await firstTag.click(); 68 | 69 | // Should be on a tag-specific page 70 | await expect(page).toHaveURL(/\/tags\//); 71 | 72 | // Should show posts with that tag 73 | const posts = page.locator('.list-item, .card'); 74 | const postCount = await posts.count(); 75 | expect(postCount).toBeGreaterThan(0); 76 | } 77 | }); 78 | 79 | test('search functionality works', async ({ page }) => { 80 | await page.goto('/'); 81 | await page.waitForLoadState('domcontentloaded'); 82 | 83 | // Click search button to open modal 84 | const searchButton = page.locator('#search-button'); 85 | if (await searchButton.isVisible()) { 86 | await searchButton.click(); 87 | 88 | // Wait for search modal to appear 89 | const searchModal = page.locator('#searchModal'); 90 | await expect(searchModal).toBeVisible(); 91 | 92 | // Look for search input in the modal 93 | const searchInput = page.locator('#searchInput'); 94 | if (await searchInput.isVisible()) { 95 | await searchInput.fill('test'); 96 | 97 | // Should show search results container 98 | const resultsContainer = page.locator('#results-container'); 99 | await expect(resultsContainer).toBeVisible(); 100 | } 101 | } 102 | }); 103 | 104 | test('404 page exists and is functional', async ({ page }) => { 105 | // Try to navigate to a non-existent page 106 | const response = await page.goto('/this-page-does-not-exist'); 107 | 108 | if (response) { 109 | // Should return 404 status 110 | expect(response.status()).toBe(404); 111 | } 112 | 113 | await page.waitForLoadState('domcontentloaded'); 114 | 115 | // Should show 404 page content 116 | const content = page.locator('main, article, .content').first(); 117 | await expect(content).toBeVisible(); 118 | 119 | // Should have some indication this is an error page 120 | const text = await page.textContent('body'); 121 | expect(text?.toLowerCase()).toMatch(/404|not found|error/); 122 | }); 123 | 124 | test('RSS/feed link exists', async ({ page }) => { 125 | await page.goto('/'); 126 | await page.waitForLoadState('domcontentloaded'); 127 | 128 | // Look for RSS feed link in head 129 | const headFeedLink = await page.locator('link[type="application/rss+xml"], link[type="application/atom+xml"]').count(); 130 | const visibleFeedLink = await page.locator('a[href*="rss"], a[href*="feed"], a[href*="atom.xml"]').count(); 131 | 132 | expect(headFeedLink + visibleFeedLink).toBeGreaterThan(0); 133 | }); 134 | }); 135 | -------------------------------------------------------------------------------- /tests/helpers.ts: -------------------------------------------------------------------------------- 1 | import { Page, expect } from '@playwright/test'; 2 | 3 | /** 4 | * Helper functions for common test operations 5 | */ 6 | 7 | export class TestHelpers { 8 | constructor(private page: Page) {} 9 | 10 | /** 11 | * Wait for page to be fully loaded and ready 12 | * Optimized version - use domcontentloaded instead of networkidle 13 | */ 14 | async waitForPageReady() { 15 | await this.page.waitForLoadState('domcontentloaded'); 16 | } 17 | 18 | /** 19 | * Get the current theme from the document 20 | */ 21 | async getCurrentTheme(): Promise { 22 | return await this.page.evaluate(() => { 23 | // Apollo uses localStorage to store theme 24 | const theme = localStorage.getItem('theme-storage'); 25 | if (theme) return theme; 26 | 27 | // Fallback to class detection 28 | const html = document.documentElement; 29 | if (html.classList.contains('dark')) return 'dark'; 30 | if (html.classList.contains('light')) return 'light'; 31 | return 'auto'; 32 | }); 33 | } 34 | 35 | /** 36 | * Click the theme toggle button 37 | */ 38 | async toggleTheme() { 39 | const prevTheme = await this.getCurrentTheme(); 40 | await this.page.click('#dark-mode-toggle'); 41 | 42 | // Wait for the theme to change (class on changes) 43 | await this.page.waitForFunction( 44 | (prev) => { 45 | const html = document.documentElement; 46 | const current = html.classList.contains('dark') ? 'dark' : 47 | html.classList.contains('light') ? 'light' : 'auto'; 48 | return current !== prev; 49 | }, 50 | prevTheme 51 | ); 52 | } 53 | 54 | /** 55 | * Verify navigation menu exists and has expected links 56 | */ 57 | async verifyNavigationMenu() { 58 | const nav = this.page.locator('nav'); 59 | await expect(nav).toBeVisible(); 60 | 61 | // Check for main navigation links 62 | await expect(this.page.locator('nav a[href="/posts"]')).toBeVisible(); 63 | await expect(this.page.locator('nav a[href="/projects"]')).toBeVisible(); 64 | await expect(this.page.locator('nav a[href="/tags"]')).toBeVisible(); 65 | } 66 | 67 | /** 68 | * Verify social media links are present 69 | */ 70 | async verifySocialLinks() { 71 | const socialLinks = this.page.locator('.socials .social'); 72 | const count = await socialLinks.count(); 73 | expect(count).toBeGreaterThan(0); 74 | 75 | // Verify each link has an icon 76 | for (let i = 0; i < count; i++) { 77 | const link = socialLinks.nth(i); 78 | await expect(link.locator('svg, img')).toBeVisible(); 79 | } 80 | } 81 | 82 | /** 83 | * Check if table of contents exists on the page 84 | */ 85 | async hasTableOfContents(): Promise { 86 | const toc = this.page.locator('.toc, #toc'); 87 | return await toc.isVisible(); 88 | } 89 | 90 | /** 91 | * Verify table of contents navigation 92 | */ 93 | async verifyTableOfContents() { 94 | const hasToc = await this.hasTableOfContents(); 95 | if (!hasToc) return; 96 | 97 | const tocLinks = this.page.locator('.toc a, #toc a'); 98 | const count = await tocLinks.count(); 99 | expect(count).toBeGreaterThan(0); 100 | 101 | // Test first TOC link 102 | if (count > 0) { 103 | const firstLink = tocLinks.first(); 104 | const href = await firstLink.getAttribute('href'); 105 | expect(href).toMatch(/^#.+/); // Should be an anchor link 106 | } 107 | } 108 | 109 | /** 110 | * Take a screenshot with consistent naming 111 | */ 112 | async takeScreenshot(name: string, options: { fullPage?: boolean } = {}) { 113 | const theme = await this.getCurrentTheme(); 114 | const screenshotName = `${name}-${theme}.png`; 115 | await this.page.screenshot({ 116 | path: `tests/screenshots/${screenshotName}`, 117 | fullPage: options.fullPage || false 118 | }); 119 | } 120 | 121 | /** 122 | * Click a TOC link and verify active state 123 | * Optimized version with faster waits 124 | */ 125 | async clickTocLinkAndVerifyActive(tocLink: any) { 126 | await tocLink.click(); 127 | 128 | // Wait for the link to have the active/current class 129 | await this.page.waitForFunction( 130 | (el) => el && /active|current/i.test(el.className), 131 | await tocLink.elementHandle() 132 | ); 133 | 134 | // Verify the link has active styling 135 | const linkClass = await tocLink.getAttribute('class'); 136 | if (linkClass) { 137 | expect(linkClass).toMatch(/active|current/i); 138 | } 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /tests/navigation/menu-navigation.spec.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from '@playwright/test'; 2 | 3 | test.describe('Navigation Menu', () => { 4 | test.beforeEach(async ({ page }) => { 5 | await page.goto('/'); 6 | await page.waitForLoadState('domcontentloaded'); 7 | }); 8 | 9 | test('navigation menu and core links work', async ({ page }) => { 10 | // Verify menu exists 11 | const nav = page.locator('nav'); 12 | await expect(nav).toBeVisible(); 13 | 14 | // Verify and test main links - be flexible with href matching 15 | const postsLink = page.locator('nav a').filter({ hasText: /posts/i }).first(); 16 | await expect(postsLink).toBeVisible(); 17 | await postsLink.click(); 18 | await expect(page).toHaveURL(/\/posts/); 19 | 20 | // Navigate to projects 21 | await page.locator('nav a').filter({ hasText: /projects/i }).first().click(); 22 | await expect(page).toHaveURL(/\/projects/); 23 | 24 | // Navigate to tags 25 | await page.locator('nav a').filter({ hasText: /tags/i }).first().click(); 26 | await expect(page).toHaveURL(/\/tags/); 27 | }); 28 | 29 | test('social media links are present', async ({ page }) => { 30 | const socialLinks = page.locator('.socials .social'); 31 | const count = await socialLinks.count(); 32 | 33 | if (count > 0) { 34 | expect(count).toBeGreaterThan(0); 35 | 36 | // Verify first social link has valid href 37 | const firstLink = socialLinks.first(); 38 | const href = await firstLink.getAttribute('href'); 39 | expect(href).toMatch(/^https?:\/\/.+/); 40 | } 41 | }); 42 | 43 | test('responsive navigation on mobile', async ({ page }) => { 44 | await page.setViewportSize({ width: 375, height: 667 }); 45 | 46 | // Navigation should still be accessible 47 | const nav = page.locator('nav'); 48 | await expect(nav).toBeVisible(); 49 | 50 | // Links should still work 51 | const postsLink = page.locator('nav a').filter({ hasText: /posts/i }).first(); 52 | if (await postsLink.isVisible()) { 53 | await expect(postsLink).toBeVisible(); 54 | } 55 | }); 56 | }); 57 | -------------------------------------------------------------------------------- /tests/visual/character-shortcodes-visual.spec.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from '@playwright/test'; 2 | 3 | test.describe('Visual Regression - Character Shortcodes Post', () => { 4 | test.skip(({ browserName }) => browserName === 'webkit', 'Skip visual tests on Safari mobile due to rendering variability'); 5 | 6 | test('character shortcodes post visual comparison - light theme', async ({ page }) => { 7 | test.setTimeout(45000); // Timeout for screenshot 8 | 9 | await page.goto('/posts/character-shortcodes'); 10 | await page.waitForLoadState('domcontentloaded'); 11 | 12 | // Set to light theme 13 | await page.evaluate(() => { 14 | document.documentElement.className = 'light'; 15 | }); 16 | 17 | // Wait for theme class to be applied 18 | await page.waitForFunction(() => { 19 | return document.documentElement.className === 'light'; 20 | }); 21 | 22 | // Wait a moment for CSS to apply 23 | await page.waitForTimeout(500); 24 | 25 | await expect(page).toHaveScreenshot('character-shortcodes-light.png'); 26 | }); 27 | 28 | test('character shortcodes post visual comparison - dark theme', async ({ page }) => { 29 | test.setTimeout(45000); // Timeout for screenshot 30 | 31 | await page.goto('/posts/character-shortcodes'); 32 | await page.waitForLoadState('domcontentloaded'); 33 | 34 | // Set to dark theme 35 | await page.evaluate(() => { 36 | document.documentElement.className = 'dark'; 37 | }); 38 | 39 | // Wait for theme class to be applied 40 | await page.waitForFunction(() => { 41 | return document.documentElement.className === 'dark'; 42 | }); 43 | 44 | // Wait a moment for CSS to apply 45 | await page.waitForTimeout(500); 46 | 47 | await expect(page).toHaveScreenshot('character-shortcodes-dark.png'); 48 | }); 49 | }); 50 | -------------------------------------------------------------------------------- /tests/visual/character-shortcodes-visual.spec.ts-snapshots/character-shortcodes-dark-Mobile-Chrome-linux.png: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:032661551cf0a92bc86a9c17dd1464876d8b1b82fcc1148621936eecbc4bd47c 3 | size 39108 4 | -------------------------------------------------------------------------------- /tests/visual/character-shortcodes-visual.spec.ts-snapshots/character-shortcodes-dark-chromium-linux.png: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:7cafb299ab62b42b4fc62ae22bbf895a38fc8d5513bff99def2f1f8ed8cdeed5 3 | size 51907 4 | -------------------------------------------------------------------------------- /tests/visual/character-shortcodes-visual.spec.ts-snapshots/character-shortcodes-light-Mobile-Chrome-linux.png: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:ca6a3743f47daafd0f3111923907c5a5e0693680a479aa5981a427440164e15b 3 | size 40731 4 | -------------------------------------------------------------------------------- /tests/visual/character-shortcodes-visual.spec.ts-snapshots/character-shortcodes-light-chromium-linux.png: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:11f127d25f38239b38637a423d025f4115a17421bc431a1fdb674420d6f2fa83 3 | size 52710 4 | -------------------------------------------------------------------------------- /tests/visual/projects-visual.spec.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from '@playwright/test'; 2 | 3 | test.describe('Visual Regression - Projects Page', () => { 4 | test.skip(({ browserName }) => browserName === 'webkit', 'Skip visual tests on Safari mobile due to rendering variability'); 5 | 6 | test('projects page visual comparison', async ({ page }) => { 7 | test.setTimeout(45000); // Timeout for screenshot 8 | 9 | await page.goto('/projects'); 10 | await page.waitForLoadState('domcontentloaded'); 11 | 12 | // Set to light theme 13 | await page.evaluate(() => { 14 | document.documentElement.className = 'light'; 15 | }); 16 | 17 | // Wait for theme class to be applied 18 | await page.waitForFunction(() => { 19 | return document.documentElement.className === 'light'; 20 | }); 21 | 22 | // Wait a moment for CSS to apply 23 | await page.waitForTimeout(500); 24 | 25 | await expect(page).toHaveScreenshot('projects-page.png'); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /tests/visual/projects-visual.spec.ts-snapshots/projects-page-Mobile-Chrome-linux.png: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:c4bf6ed7374fd0ea84f21af75b2d4a9b9d3fb5d38fe85398c1537b6572b34c1a 3 | size 173399 4 | -------------------------------------------------------------------------------- /tests/visual/projects-visual.spec.ts-snapshots/projects-page-Mobile-Safari-linux.png: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:35bac0def2240bd3d1074e60353e1bf0a1b1ae17d9031ea3b712b9d15347bfb3 3 | size 153763 4 | -------------------------------------------------------------------------------- /tests/visual/projects-visual.spec.ts-snapshots/projects-page-chromium-linux.png: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:8bb7ee3321425caa8d463d580d92365e4512fed29e91a40de38eb1899dd582b4 3 | size 455828 4 | -------------------------------------------------------------------------------- /tests/visual/responsive-visual.spec.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from '@playwright/test'; 2 | 3 | test.describe('Visual Regression - Responsive Design', () => { 4 | test('homepage responsive - desktop', async ({ page }) => { 5 | test.setTimeout(30000); // Increase timeout for screenshot 6 | 7 | await page.setViewportSize({ width: 1920, height: 1080 }); 8 | await page.goto('/'); 9 | await page.waitForLoadState('domcontentloaded'); 10 | 11 | // Wait for layout to stabilize after viewport change 12 | await page.waitForFunction(() => document.readyState === 'complete'); 13 | await page.waitForLoadState('networkidle'); 14 | 15 | await expect(page).toHaveScreenshot('homepage-desktop.png'); 16 | }); 17 | 18 | test('homepage responsive - tablet', async ({ page }) => { 19 | test.setTimeout(30000); // Increase timeout for screenshot 20 | 21 | await page.setViewportSize({ width: 768, height: 1024 }); 22 | await page.goto('/'); 23 | await page.waitForLoadState('domcontentloaded'); 24 | 25 | // Wait for layout to stabilize after viewport change 26 | await page.waitForFunction(() => document.readyState === 'complete'); 27 | await page.waitForLoadState('networkidle'); 28 | 29 | await expect(page).toHaveScreenshot('homepage-tablet.png'); 30 | }); 31 | 32 | test('homepage responsive - mobile', async ({ page }) => { 33 | test.setTimeout(30000); // Increase timeout for screenshot 34 | 35 | await page.setViewportSize({ width: 375, height: 667 }); 36 | await page.goto('/'); 37 | await page.waitForLoadState('domcontentloaded'); 38 | 39 | // Wait for layout to stabilize after viewport change 40 | await page.waitForFunction(() => document.readyState === 'complete'); 41 | await page.waitForLoadState('networkidle'); 42 | 43 | await expect(page).toHaveScreenshot('homepage-mobile.png'); 44 | }); 45 | }); 46 | -------------------------------------------------------------------------------- /tests/visual/responsive-visual.spec.ts-snapshots/homepage-desktop-Mobile-Chrome-linux.png: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:10a5bda4ef84dc3f1cf05853bbc7175780567bfc289366de0493bef56479ef0e 3 | size 75949 4 | -------------------------------------------------------------------------------- /tests/visual/responsive-visual.spec.ts-snapshots/homepage-desktop-Mobile-Safari-linux.png: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:81b60f1adc914daa55fba78b55eec2225cf56a9ef1bee3e40cfa7e297816efeb 3 | size 116065 4 | -------------------------------------------------------------------------------- /tests/visual/responsive-visual.spec.ts-snapshots/homepage-desktop-chromium-linux.png: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:24d5a6b56d307b6f4f505d42c6d46730a72d38f759c65494fd8195aac2e7ce25 3 | size 72195 4 | -------------------------------------------------------------------------------- /tests/visual/responsive-visual.spec.ts-snapshots/homepage-mobile-Mobile-Chrome-linux.png: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:142ff36a54db9afa075378f1c5f2e7a764eb80b512dc7bff4c77fa4a1bebbcc6 3 | size 26468 4 | -------------------------------------------------------------------------------- /tests/visual/responsive-visual.spec.ts-snapshots/homepage-mobile-Mobile-Safari-linux.png: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:9161b770e56166fe1da4e5b94e55794459c215643b1ed15a6b7d72ca05efc3ec 3 | size 48702 4 | -------------------------------------------------------------------------------- /tests/visual/responsive-visual.spec.ts-snapshots/homepage-mobile-chromium-linux.png: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:3a368275fe9b8cf23d8e95e69625bfdd3514cf3aae6c0599bc299eef7590f4b5 3 | size 26018 4 | -------------------------------------------------------------------------------- /tests/visual/responsive-visual.spec.ts-snapshots/homepage-tablet-Mobile-Chrome-linux.png: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:209cf2a14bcb554de500c967dcc4d9aa8857fbb20266160182e77e0ceb1a3def 3 | size 52010 4 | -------------------------------------------------------------------------------- /tests/visual/responsive-visual.spec.ts-snapshots/homepage-tablet-Mobile-Safari-linux.png: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:6e710d474ed42b0da0735b19b8c8e95b8e1dd433771e5ec60697eaeed5b114b5 3 | size 82691 4 | -------------------------------------------------------------------------------- /tests/visual/responsive-visual.spec.ts-snapshots/homepage-tablet-chromium-linux.png: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:266fff02fc701e4e5470188e80dc175a5fd46fd7ef52772d5bbfbcb9eb6b1a98 3 | size 50439 4 | -------------------------------------------------------------------------------- /tests/visual/responsive-visual.spec.ts-snapshots/navigation-mobile-chromium-linux.png: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:bfae41190f3c834bf01fa13de0fbb15e45a8c1c04f77abba77bc62b01701ae00 3 | size 4102 4 | -------------------------------------------------------------------------------- /tests/visual/responsive-visual.spec.ts-snapshots/posts-mobile-chromium-linux.png: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:717f6498cc8a1d28e763cc3c0418aafd91883132db65871d85f178081eac7ce0 3 | size 26370 4 | -------------------------------------------------------------------------------- /tests/visual/talks-visual.spec.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from '@playwright/test'; 2 | 3 | test.describe('Visual Regression - Talks Page', () => { 4 | test.skip(({ browserName }) => browserName === 'webkit', 'Skip visual tests on Safari mobile due to rendering variability'); 5 | 6 | test('talks page visual comparison', async ({ page }) => { 7 | test.setTimeout(45000); // Timeout for screenshot 8 | 9 | await page.goto('/talks'); 10 | await page.waitForLoadState('domcontentloaded'); 11 | 12 | // Set to light theme 13 | await page.evaluate(() => { 14 | document.documentElement.className = 'light'; 15 | }); 16 | 17 | // Wait for theme class to be applied 18 | await page.waitForFunction(() => { 19 | return document.documentElement.className === 'light'; 20 | }); 21 | 22 | // Wait a moment for CSS to apply 23 | await page.waitForTimeout(500); 24 | 25 | await expect(page).toHaveScreenshot('talks-page.png'); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /tests/visual/talks-visual.spec.ts-snapshots/talks-page-Mobile-Chrome-linux.png: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:cd4d2aff8725e116cf80da6cc6819fabcde932716bc4d2dd0206154e819b5da8 3 | size 186460 4 | -------------------------------------------------------------------------------- /tests/visual/talks-visual.spec.ts-snapshots/talks-page-Mobile-Safari-linux.png: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:3abe63ae2ca6ead9cd2ab228f94455c100d4ec22e203425426fb0d3e569ce184 3 | size 243926 4 | -------------------------------------------------------------------------------- /tests/visual/talks-visual.spec.ts-snapshots/talks-page-chromium-linux.png: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:9c2f707f7be2ffe44017a83a3c220fc042d6e92a6bcdb097866edd85fe7eb6a7 3 | size 338739 4 | -------------------------------------------------------------------------------- /tests/visual/theme-visual.spec.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from '@playwright/test'; 2 | 3 | test.describe('Visual Regression - Themes', () => { 4 | test('homepage visual comparison - light theme', async ({ page }) => { 5 | test.setTimeout(30000); // Increase timeout for screenshot 6 | 7 | await page.goto('/'); 8 | await page.waitForLoadState('domcontentloaded'); 9 | 10 | // Set to light theme 11 | await page.evaluate(() => { 12 | document.documentElement.className = 'light'; 13 | }); 14 | 15 | // Wait for theme class to be applied 16 | await page.waitForFunction(() => { 17 | return document.documentElement.className === 'light'; 18 | }); 19 | 20 | // Give a moment for CSS to apply 21 | await page.waitForLoadState('networkidle'); 22 | 23 | await expect(page).toHaveScreenshot('homepage-light.png'); 24 | }); 25 | 26 | test('homepage visual comparison - dark theme', async ({ page }) => { 27 | test.setTimeout(30000); // Increase timeout for screenshot 28 | 29 | await page.goto('/'); 30 | await page.waitForLoadState('domcontentloaded'); 31 | 32 | // Set to dark theme 33 | await page.evaluate(() => { 34 | document.documentElement.className = 'dark'; 35 | }); 36 | 37 | // Wait for theme class to be applied 38 | await page.waitForFunction(() => { 39 | return document.documentElement.className === 'dark'; 40 | }); 41 | 42 | // Give a moment for CSS to apply 43 | await page.waitForLoadState('networkidle'); 44 | 45 | await expect(page).toHaveScreenshot('homepage-dark.png'); 46 | }); 47 | }); 48 | -------------------------------------------------------------------------------- /tests/visual/theme-visual.spec.ts-snapshots/homepage-dark-Mobile-Chrome-linux.png: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:d0fb4f28aea18d298e311cda106e6c3c0202439b37ad2898cc981d96b669de35 3 | size 27311 4 | -------------------------------------------------------------------------------- /tests/visual/theme-visual.spec.ts-snapshots/homepage-dark-Mobile-Safari-linux.png: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:7cbe0e0ff667ed1657b9dc4e834cd2617e3bae99d4187cd9f955e897832c8f51 3 | size 48346 4 | -------------------------------------------------------------------------------- /tests/visual/theme-visual.spec.ts-snapshots/homepage-dark-chromium-linux.png: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:1e83e6c370fbb5551bebcb4bf0d4a0a77282d3b3e15c658ff70d3ae87b861fa1 3 | size 42737 4 | -------------------------------------------------------------------------------- /tests/visual/theme-visual.spec.ts-snapshots/homepage-light-Mobile-Chrome-linux.png: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:f0c6494708a2f86380717e0ea032af2dff3a0afdfe67de0698db504b133d0308 3 | size 29348 4 | -------------------------------------------------------------------------------- /tests/visual/theme-visual.spec.ts-snapshots/homepage-light-Mobile-Safari-linux.png: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:e51508301c344451a9d7ac45a35d2a17fc52c65f80a387522236dd416f1d2eba 3 | size 48948 4 | -------------------------------------------------------------------------------- /tests/visual/theme-visual.spec.ts-snapshots/homepage-light-chromium-linux.png: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:198269493f912c0ebe4e98c4429e5e3d90006817b9490b9702ec851a8f361b3b 3 | size 43036 4 | -------------------------------------------------------------------------------- /tests/visual/theme-visual.spec.ts-snapshots/posts-page-dark-chromium-linux.png: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:6e9d5aa074adb4302db3b200a6c5c1280327a522a757405d25cf5692a5a166a0 3 | size 37768 4 | -------------------------------------------------------------------------------- /tests/visual/theme-visual.spec.ts-snapshots/posts-page-light-chromium-linux.png: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:c75f25e4cce3b0e812283ee95f90ef9dafa7e9d014d3e81dc09a1faf4f904734 3 | size 38341 4 | -------------------------------------------------------------------------------- /theme.toml: -------------------------------------------------------------------------------- 1 | name = "apollo" 2 | description = "Modern and minimalistic blog theme" 3 | min_version = "0.14.0" 4 | license = "MIT" 5 | homepage = "https://github.com/not-matthias/apollo" 6 | demo = "https://not-matthias.github.io/apollo" 7 | 8 | # Any variable there can be overridden in the end user `config.toml` 9 | # You don't need to prefix variables by the theme name but as this will 10 | # be merged with user data, some kind of prefix or nesting is preferable 11 | # Use snake_casing to be consistent with the rest of Zola 12 | [extra] 13 | 14 | [author] 15 | name = "not-matthias" 16 | homepage = "https://github.com/not-matthias" 17 | -------------------------------------------------------------------------------- /treefmt.toml: -------------------------------------------------------------------------------- 1 | walk = "git" 2 | excludes = ["static/**", "content/*"] 3 | 4 | [formatter.nix] 5 | command = "alejandra" 6 | includes = ["*.nix"] 7 | 8 | [formatter.djlint] 9 | command = "djlint" 10 | options = [ 11 | "--preserve-blank-lines", 12 | "--line-break-after-multiline-tag", 13 | "--max-blank-lines", 14 | "2", 15 | "--format-css", 16 | "--format-js", 17 | "--close-void-tags", 18 | "--reformat", 19 | ] 20 | includes = ["*.html"] 21 | 22 | [formatter.scss] 23 | command = "prettier" 24 | includes = ["*.scss", "*.sass"] 25 | --------------------------------------------------------------------------------