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 plugins: jekyll-feed, jekyll-paginate, jekyll-sitemap, jekyll-webp

Prerequisites

ToolVersionPurpose
Ruby3.3.xJekyll and bundler
BundlerlatestRuby dependency management
Node.js22+Linting tools
webp CLIanyWebP 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

  1. Copy _templates/post.md to _posts/YYYY-MM-DD-your-title.md

  2. 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
    ---
    
  3. Write your content in Markdown below the front matter

  4. Preview with bundle exec jekyll serve --drafts

Notes:

  • Future-dated posts (future: false in _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) → LightDark.

  • data-theme-mode on <html> tracks the selected mode
  • data-theme tracks the resolved colour (light or dark)
  • An inline script in _includes/header.html applies 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.scssglassmorphism, 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 pitfalls
  • no-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 nature
  • MD033 (inline HTML) — posts may use HTML for formatting
  • MD041 (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.

  1. Fork the repository and create a feature branch from master
  2. Install dependencies: bundle install && npm install
  3. Make your changes
  4. Lint before committing: npm run lint
  5. 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.