├── .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 | 
11 |
12 |
13 |
14 |
15 | Light theme
16 |
17 | 
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 | 
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 |
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 |
6 | {% elif icon_svg %}
7 | {{ icon_svg | safe }}
8 | {% endif %}
9 | {{ text }}
10 |
11 | {% else %}
12 |
16 | {% if icon %}
17 |
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 |
34 |
35 | {% endmacro list_post %}
36 |
37 | {% macro list_posts(pages) %}
38 |
39 | {%- for page in pages %}
40 | {{ post_macros::list_post(page=page) }}
41 | {% endfor -%}
42 |
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 |
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 |
2 |
3 | {% if config.extra.logo %}
4 |
5 |
6 |
7 | {% else %}
8 |
{{ config.title }}
9 | {% endif %}
10 |
11 |
12 |
13 | {% if config.markdown.external_links_target_blank %}
14 | {% set link_attrs = "rel='me noopener' target='_blank'" %}
15 | {% else %}
16 | {% set link_attrs = "rel='me'" %}
17 | {% endif %}
18 | {% for social in config.extra.socials %}
19 |
20 |
22 |
23 | {% endfor %}
24 |
25 |
26 |
27 |
28 | {% for menu in config.extra.menu %}
29 |
{{ menu.name }}
30 | {% endfor %}
31 |
32 | {% if config.build_search_index %}
33 |
36 |
39 |
40 |
41 |
45 |
46 |
49 |
50 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 | No results
66 | 1 result
67 | $NUMBER results
68 |
69 |
70 |
71 |
72 |
73 |
74 | {% endif %}
75 |
76 | {% if config.extra.theme == "toggle" %}
77 |
80 |
81 |
85 |
89 |
90 |
91 |
92 |
95 | {% endif %}
96 |
97 |
98 |
--------------------------------------------------------------------------------
/templates/partials/toc.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | Table of Contents
4 |
5 |
6 | {% for h1 in page.toc %}
7 |
8 | {% set h1_index = loop.index %}
9 | {{ h1.title }}
10 |
11 | {% if h1.children %}
12 |
13 | {% for h2 in h1.children %}
14 | {% set h2_index = loop.index %}
15 |
16 | {{ h2.title }}
17 |
18 |
19 | {% if h2.children %}
20 |
21 | {% for h3 in h2.children %}
22 |
23 | {{ h3.title }}
24 |
25 | {% endfor %}
26 |
27 | {% endif %}
28 | {% endfor %}
29 |
30 | {% endif %}
31 |
32 | {% endfor %}
33 |
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 |
14 | {% elif character_name == "hooded" %}
15 |
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 |
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 |
4 | {% if center | default(value=false) %}
5 |
6 | {{ header | markdown | safe }}
7 |
8 | {% else %}
9 |
10 | {{ header | markdown | safe }}
11 |
12 | {% endif %}
13 |
14 |
15 | {% if hidden | default(value=false) %}
16 |
17 | {% else %}
18 |
19 | {% endif %}
20 | {{ body | markdown | safe }}
21 |
22 | {% else %}
23 |
34 |
35 | {{ body | markdown | safe }}
36 |
37 | {% endif %}
38 |
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 |
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 |
--------------------------------------------------------------------------------