tedemehrtens.de
March 10, 2026 · View on GitHub
Personal portfolio and blog of Tede Mehrtens. Built with Jekyll, hosted on Cloudflare Workers.
Tech Stack
- Jekyll 4.4 — static site generator
- Dart Sass via
jekyll-sass-converter— SCSS compilation - Cloudflare Workers Static Assets — global edge hosting
- Obsidian — writing environment (vault pointed at this repo)
Jekyll plugins: jekyll-feed, jekyll-paginate, jekyll-sitemap, jekyll-webp
Prerequisites
| Tool | Version | Purpose |
|---|---|---|
| Ruby | 3.3.x | Jekyll and bundler |
| Bundler | latest | Ruby dependency management |
| Node.js | 22+ | Linting tools |
| webp CLI | any | WebP image generation during build |
Install the webp CLI via your package manager:
# macOS
brew install webp
# Debian/Ubuntu
sudo apt-get install webp
Installation
git clone git@github.com:jantede/tedemehrtens.de.git
cd tedemehrtens.de
# Ruby dependencies
bundle install
# Node.js dev dependencies (linters)
npm install
Development
Start the local preview server:
bundle exec jekyll serve
The site is available at http://localhost:4000. Jekyll watches for file changes and rebuilds automatically. The _config.yml is not hot-reloaded — restart the server after changing it.
To preview future-dated posts and drafts:
bundle exec jekyll serve --future --drafts
Project Structure
.
├── _includes/ # Reusable HTML partials (header, navbar, footer, …)
├── _layouts/ # Page layout templates (default, post, tag, …)
├── _plugins/ # Custom Ruby plugins (tag page generator)
├── _posts/ # Blog posts (YYYY-MM-DD-title.md)
├── _sass/ # SCSS partials, one file per component
├── _templates/ # Post template for new entries
├── assets/
│ ├── css/ # SCSS entry point → compiled to portfolio.css
│ ├── img/ # Images (jekyll-webp generates .webp on build)
│ └── js/ # Client-side JavaScript
├── _config.yml # Jekyll site configuration
├── wrangler.toml # Cloudflare Workers config
└── _headers # Cloudflare cache and security headers
Writing a Blog Post
-
Copy
_templates/post.mdto_posts/YYYY-MM-DD-your-title.md -
Fill in the front matter:
--- layout: post title: Your Post Title date: 2026-01-01T12:00:00 description: A short description for SEO and post cards. tags: - tag-one - tag-two --- -
Write your content in Markdown below the front matter
-
Preview with
bundle exec jekyll serve --drafts
Notes:
- Future-dated posts (
future: falsein_config.yml) only go live once the scheduled build runs or you push manually - Tag pages are auto-generated by
_plugins/tag_generator.rb— no extra steps needed - Images dropped into
assets/img/get a WebP version generated on the next build - Never nest
<a>inside<a>— browsers silently eject inner anchors from the DOM
Theme System
The site has a 3-state theme: Auto (system default) → Light → Dark.
data-theme-modeon<html>tracks the selected modedata-themetracks the resolved colour (lightordark)- An inline script in
_includes/header.htmlapplies the theme before first paint to prevent flash
SCSS Conventions
- Entry point:
assets/css/portfolio.scss - Partials:
_sass/_*.scss— one file per component/section - Mixins:
_sass/_mixins.scss—glassmorphism,card-hover,page-heading, … - Variables:
_sass/_variables.scss— CSS custom properties for light and dark themes - Partials that use mixins:
@use 'mixins' as * - Mobile breakpoint:
max-width: 639px
Linting & Testing
Three linters are configured, all run via npm scripts:
npm run lint # run all linters
npm run lint:js # ESLint – assets/js/
npm run lint:html # htmlhint – _includes/, _layouts/, *.html
npm run lint:md # markdownlint-cli2 – _posts/, about.md, impressum.md
ESLint (eslint.config.mjs)
Covers assets/js/. Rules focus on correctness rather than style:
no-var,eqeqeq— prevent common JS pitfallsno-unused-vars,no-console— warn on leftover debug code- Browser globals included;
sourceType: "script"for IIFE-style files
htmlhint (.htmlhintrc)
Covers HTML templates. Several rules are intentionally disabled because Liquid template syntax conflicts with standard HTML parsing (doctype-first, tag-pair, spec-char-escape). The camelCase SVG attributes viewBox and preserveAspectRatio are whitelisted.
markdownlint (.markdownlint.json)
Covers blog posts and standalone Markdown pages. Disabled rules:
MD013(line length) — prose lines are long by natureMD033(inline HTML) — posts may use HTML for formattingMD041(first line must be H1) — posts start with YAML front matter
CI
GitHub Actions runs all three linters on every push to master and on every pull request (.github/workflows/lint.yml).
Deployment
Automatic (Cloudflare Git integration)
Push to master → Cloudflare builds and deploys automatically.
Cloudflare build command (configured in the dashboard):
apt-get install -y webp && bundle exec jekyll build
Scheduled builds
.github/workflows/scheduled-build.yml triggers every 8 hours. This is required for future-dated posts to go live on time without a manual push.
Manual deploy
bundle exec jekyll build
npx wrangler deploy
Requires CLOUDFLARE_API_TOKEN and CLOUDFLARE_ACCOUNT_ID to be set as environment variables.
Contributing
This is a personal site, but corrections and suggestions are welcome.
- Fork the repository and create a feature branch from
master - Install dependencies:
bundle install && npm install - Make your changes
- Lint before committing:
npm run lint - Open a pull request — Cloudflare will deploy a preview automatically and post the URL in the PR
Commit Style
type(scope): short description
feat(post): add article about X
fix(navbar): correct mobile breakpoint
style(scss): adjust hero spacing
chore(deps): update jekyll to 4.4.1
Branch Naming
Use descriptive names: feat/dark-mode-improvements, fix/tag-page-layout, post/new-article-title.
License
The code (layouts, includes, SCSS, JavaScript) is available under the MIT License.
Content (blog posts, images) is copyright Tede Mehrtens — all rights reserved.