Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 52 additions & 0 deletions .github/workflows/build-deploy-jekyll.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
name: Deploy Jekyll with GitHub Pages dependencies preinstalled

on:
# Runs on pushes targeting the default branch
push:
branches:
- main
- feat/jekyll

# Allows you to run this workflow manually from the Actions tab
workflow_dispatch:

# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages
permissions:
contents: read
pages: write
id-token: write

# Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued.
# However, do NOT cancel in-progress runs as we want to allow these production deployments to complete.
concurrency:
group: "pages"
cancel-in-progress: false

jobs:
# Build job
build:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Pages
uses: actions/configure-pages@v5
- name: Build with Jekyll
uses: actions/jekyll-build-pages@v1
with:
source: ./
destination: ./_site
- name: Upload artifact
uses: actions/upload-pages-artifact@v3

# Deployment job
deploy:
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
runs-on: ubuntu-latest
needs: build
steps:
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v5
11 changes: 11 additions & 0 deletions Gemfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
source "https://rubygems.org"

gem "jekyll", "~> 4.3"

# Uncomment instead if deploying via GitHub Pages' built-in build:
# gem "github-pages", group: :jekyll_plugins

platforms :mingw, :x64_mingw, :mswin, :jruby do
gem "tzinfo", ">= 1", "< 3"
gem "tzinfo-data"
end
16 changes: 16 additions & 0 deletions _config.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
title: CookieBytes
description: Software that's baked to perfection. CookieBytes builds digital products with warmth, precision, and a little love.
url: "https://cookiebytes.pt"
baseurl: ""
lang: en

permalink: /blog/:title/

markdown: kramdown
kramdown:
input: GFM

exclude:
- Gemfile
- Gemfile.lock
- README.md
4 changes: 4 additions & 0 deletions _includes/cookie-logo.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{% assign s = include.size | default: 32 %}<svg width="{{ s }}" height="{{ s }}" viewBox="0 0 80 80" xmlns="http://www.w3.org/2000/svg">
<defs><mask id="ck-{{ include.id | default: 'logo' }}"><rect width="80" height="80" fill="white"/><ellipse cx="56" cy="10" rx="6" ry="7" fill="black"/><ellipse cx="67" cy="13" rx="5.5" ry="6.5" fill="black"/><ellipse cx="75" cy="22" rx="5" ry="6" fill="black"/><ellipse cx="60" cy="22" rx="6" ry="7" fill="black"/><ellipse cx="71" cy="30" rx="5" ry="6" fill="black"/><ellipse cx="24" cy="70" rx="5.5" ry="6.5" fill="black"/><ellipse cx="13" cy="67" rx="5" ry="6" fill="black"/><ellipse cx="5" cy="58" rx="4.5" ry="5.5" fill="black"/><ellipse cx="20" cy="58" rx="5.5" ry="6.5" fill="black"/><ellipse cx="9" cy="50" rx="4.5" ry="5.5" fill="black"/></mask></defs>
<g mask="url(#ck-{{ include.id | default: 'logo' }})"><circle cx="40" cy="40" r="36" fill="#C97B3A"/><circle cx="40" cy="40" r="30" fill="#D4893F"/><circle cx="26" cy="34" r="4.5" fill="#7A3E10"/><circle cx="46" cy="32" r="3.5" fill="#7A3E10"/><circle cx="52" cy="50" r="4" fill="#7A3E10"/><circle cx="32" cy="52" r="3" fill="#7A3E10"/><circle cx="44" cy="56" r="3.5" fill="#7A3E10"/><circle cx="22" cy="50" r="2.5" fill="#7A3E10"/><circle cx="36" cy="44" r="3" fill="#7A3E10"/></g>
</svg>
14 changes: 14 additions & 0 deletions _includes/footer.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<footer>
<div class="footer-logo">
{% include cookie-logo.html size=28 id="footer" %}
CookieBytes
</div>
<div class="footer-tagline">// software with warmth</div>
<ul class="footer-links">
<li><a href="{{ '/' | relative_url }}#services">Services</a></li>
<li><a href="{{ '/' | relative_url }}#about">About</a></li>
<li><a href="{{ '/blog/' | relative_url }}">Blog</a></li>
<li><a href="{{ '/' | relative_url }}#contact">Contact</a></li>
</ul>
<div class="footer-copy">© {{ site.time | date: "%Y" }} CookieBytes · cookiebytes.pt · Cookie is a good girl. Billy is trying his best. Rocky, please get off the keyboard. Peggy knows.</div>
</footer>
12 changes: 12 additions & 0 deletions _includes/nav.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<nav>
<a href="{{ '/' | relative_url }}" class="nav-logo">
{% include cookie-logo.html size=32 id="nav" %}
CookieBytes
</a>
<ul class="nav-links">
<li><a href="{{ '/' | relative_url }}#services">Services</a></li>
<li><a href="{{ '/' | relative_url }}#about">About</a></li>
<li><a href="{{ '/blog/' | relative_url }}"{% if page.url contains '/blog' %} class="active"{% endif %}>Blog</a></li>
<li><a href="{{ '/' | relative_url }}#contact" class="nav-cta">Get in touch</a></li>
</ul>
</nav>
22 changes: 22 additions & 0 deletions _layouts/default.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<!DOCTYPE html>
<html lang="{{ site.lang | default: 'en' }}">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% if page.title %}{{ page.title }} · {{ site.title }}{% else %}{{ site.title }}{% endif %}</title>
<meta name="description" content="{{ page.description | default: site.description }}">
<link rel="icon" type="image/svg+xml" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 80 80'%3E%3Cdefs%3E%3Cmask id='m'%3E%3Crect width='80' height='80' fill='white'/%3E%3Cellipse cx='56' cy='10' rx='6' ry='7' fill='black'/%3E%3Cellipse cx='67' cy='13' rx='5.5' ry='6.5' fill='black'/%3E%3Cellipse cx='75' cy='22' rx='5' ry='6' fill='black'/%3E%3Cellipse cx='60' cy='22' rx='6' ry='7' fill='black'/%3E%3Cellipse cx='71' cy='30' rx='5' ry='6' fill='black'/%3E%3Cellipse cx='24' cy='70' rx='5.5' ry='6.5' fill='black'/%3E%3Cellipse cx='13' cy='67' rx='5' ry='6' fill='black'/%3E%3Cellipse cx='5' cy='58' rx='4.5' ry='5.5' fill='black'/%3E%3Cellipse cx='20' cy='58' rx='5.5' ry='6.5' fill='black'/%3E%3Cellipse cx='9' cy='50' rx='4.5' ry='5.5' fill='black'/%3E%3C/mask%3E%3C/defs%3E%3Cg mask='url(%23m)'%3E%3Ccircle cx='40' cy='40' r='36' fill='%23C97B3A'/%3E%3Ccircle cx='40' cy='40' r='30' fill='%23D4893F'/%3E%3Ccircle cx='26' cy='34' r='4.5' fill='%237A3E10'/%3E%3Ccircle cx='46' cy='32' r='3.5' fill='%237A3E10'/%3E%3Ccircle cx='52' cy='50' r='4' fill='%237A3E10'/%3E%3Ccircle cx='32' cy='52' r='3' fill='%237A3E10'/%3E%3Ccircle cx='44' cy='56' r='3.5' fill='%237A3E10'/%3E%3Ccircle cx='22' cy='50' r='2.5' fill='%237A3E10'/%3E%3Ccircle cx='36' cy='44' r='3' fill='%237A3E10'/%3E%3C/g%3E%3C/svg%3E">
<link href="https://fonts.googleapis.com/css2?family=Fraunces:wght@400;700;900&family=DM+Mono:wght@400;500&family=Outfit:wght@300;400;500;600&display=swap" rel="stylesheet">
<link rel="stylesheet" href="{{ '/assets/css/shared.css' | relative_url }}">
{% if page.extra_css %}{% for css in page.extra_css %}<link rel="stylesheet" href="{{ css | relative_url }}">
{% endfor %}{% endif %}</head>
<body>

{% include nav.html %}

{{ content }}

{% include footer.html %}

</body>
</html>
27 changes: 27 additions & 0 deletions _layouts/post.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
---
layout: default
extra_css:
- /assets/css/blog.css
---
<main class="article-wrap">
<a href="{{ '/blog/' | relative_url }}" class="back-link">← Back to the Cookie Jar</a>
<div class="post-meta">
<span class="post-tag tag-{{ page.category }}">{{ page.category }}</span>
<span class="post-date">{{ page.date | date: "%Y-%m-%d" }}</span>
<span class="post-read">{{ page.read_time }}</span>
</div>
<h1>{{ page.title }}</h1>
{% if page.lede %}<p class="lede">{{ page.lede }}</p>{% endif %}

<article class="cat-{{ page.category }}">
{{ content }}
</article>

<div class="author-box">
<div class="author-avatar avatar-{{ page.category }}">{{ page.author_emoji | default: '🍪' }}</div>
<div>
<strong>The CookieBytes team</strong>
<span>{{ page.author_note }}</span>
</div>
</div>
</main>
35 changes: 35 additions & 0 deletions _posts/2026-06-30-chaos-engineering-rocky.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
---
layout: post
title: What Rocky taught us about chaos engineering
description: Our Head of Chaos Engineering has deleted two Figma files and one production config. Here's how a cat made our deployment pipeline genuinely more resilient.
category: crew
emoji: 🐱
read_time: 5 min read
lede: Netflix has Chaos Monkey. We have a cat. Rocky's job title — Head of Chaos Engineering — started as a joke after he deleted a production config by sleeping on a keyboard. Then we realized he'd found a real gap, and the joke became a discipline.
author_emoji: 🐱
author_note: Reviewed by Rocky, who walked across the draft twice and deleted a paragraph. It was probably the weakest one.
---

## The incident

A Tuesday afternoon. Rocky settles onto a laptop, as cats do. Twelve minutes later, a config file in a production repo has been overwritten with what can only be described as `jjjjjjjjjjjjj4`, committed (autosave plus a very unfortunate keyboard shortcut), and deployed by our pipeline — which validated nothing.

The service went down. The rollback took 40 minutes because we'd never actually rehearsed one. Rocky slept through the entire recovery.

## What the cat exposed

Every part of that failure was ours, not his. The pipeline deployed config changes without schema validation. Nothing alerted us until users did. And our rollback was a wiki page last updated two quarters earlier. Rocky didn't break the system — he revealed it was already broken, cheaper than a real outage with a real client watching.

<div class="callout">🐾 Chaos engineering in one sentence: if a cat on a keyboard can take down production, production was the problem, not the cat.</div>

## What changed

Config files now pass schema validation before any deploy — a malformed value fails the pipeline, not the service. Deploys are staged with automatic rollback on failed health checks. And once a month we run a "Rocky drill": someone intentionally breaks a staging environment in an unannounced way, and we measure how long detection and recovery take. The first drill took us 35 minutes to even notice. We're now under four.

## The honest lesson

Most small teams skip resilience work because it never feels urgent — until the outage, when it's suddenly the only thing that matters. You don't need Netflix-scale tooling. You need validation gates, rehearsed rollbacks, and something unpredictable applying pressure. We happen to have that last part covered in-house, ginger, and fundamentally unrepentant.

<div class="divider">🍪 🍪 🍪</div>

Want your pipeline cat-proofed? [Talk to us](/#contact) — DevOps audits are one of our favorite jobs, and yes, Rocky consults.
41 changes: 41 additions & 0 deletions _posts/2026-07-07-home-assistant-automations.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
---
layout: post
title: Five Home Assistant automations we install in every smart home
description: After dozens of installs, these are the automations that clients actually keep using six months later — plus the ones we quietly stopped recommending.
category: tech
emoji: 🏠
read_time: 7 min read
lede: After dozens of Home Assistant installs, a pattern emerged — clients love automations for two weeks, then quietly disable most of them. These five are the ones that survive. They're boring, and that's exactly the point.
author_emoji: 🏠
author_note: Field-tested in real homes, including our own — where the leak sensor's first catch was Billy's water bowl.
---

## 1. Lights that follow the sun, not a schedule

Fixed-time lighting schedules break twice a year and feel wrong most evenings. Instead, we trigger evening lights on `sun.sun` elevation — typically when the sun drops below 1.5° — and ramp brightness gradually. Nobody ever notices this automation, which is the highest compliment an automation can get.

## 2. The "everyone left" reset

One automation, triggered when the last person's presence drops: lights off, thermostat to eco, media players paused, and a notification only if a window was left open. We use person entities grouped into a `zone.home` count rather than device trackers directly — phones lie, and Wi-Fi presence flaps when someone's phone sleeps.

<div class="callout">💡 The key detail: a 10-minute delay with a cancel condition. Without it, taking out the trash turns your house off.</div>

## 3. Morning modes, not morning alarms

Instead of "at 7:00 do X", we set up an `input_select` for house mode (sleeping, waking, day, evening, away). Automations react to the mode change, and the mode itself can be flipped by time, first motion in the kitchen, or manually from a dashboard. Decoupling "what happens" from "when it happens" is the single biggest maintainability win in Home Assistant.

## 4. Water and leak alerts that escalate

Leak sensors under the sink, behind the washing machine, near the water heater. First alert: phone notification. No acknowledgment in 5 minutes: notification to everyone in the house plus lights flash red. If a smart valve is installed, shut the water after 10. The escalation ladder matters — a single silent notification at 3 a.m. protects nothing.

## 5. The "is everything okay?" nightly digest

At 22:00 the house sends one message: doors locked or not, windows open or not, batteries below 15%, and any entity that's been `unavailable` for over an hour. One message, once a day. This replaces the dozen nagging notifications that make people mute the app entirely.

<div class="divider">🍪 🍪 🍪</div>

## What we stopped recommending

Motion-triggered lights in living rooms (great in hallways, infuriating on movie night), voice announcements for routine events (novelty wears off in days), and any automation that requires the client to remember it exists. If it needs a manual, it's a feature, not an automation.

Thinking about a Home Assistant setup? [We do installs](/#contact) — sensors, dashboards, escalation ladders and all.
31 changes: 31 additions & 0 deletions _posts/2026-07-14-hello-world.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
---
layout: post
title: Hello, world — the oven is officially open
description: Why a small software studio decided to start writing things down — what we're building, what we've learned, and why the blog is named after a cookie jar.
category: studio
emoji: 🍪
read_time: 4 min read
lede: Every software studio eventually writes a "hello world" blog post. This is ours. It only took us a year, three rebrands (thanks, Billy), and one cat-related data loss incident to get here.
author_emoji: 🍪
author_note: Written by humans. Reviewed by Peggy, who said nothing, which we're taking as approval.
---

## Why a blog?

We spend our days building mobile apps, APIs, infrastructure, and smart homes for other people. Along the way we accumulate opinions — about tools that are better than their documentation suggests, patterns that quietly save projects, and automations that clients actually use instead of politely ignoring.

Until now, those opinions lived in Slack threads and post-mortems. The Cookie Jar is where we'll put them instead: honest write-ups of what worked, what didn't, and what Rocky deleted along the way.

## What to expect

Three kinds of posts, roughly. **Tech** posts are the practical ones — Home Assistant setups, backend patterns, DevOps lessons from real projects. **Studio** posts are about how we work: process, tooling, the occasional strong opinion about estimates. And **Crew** posts are about the four animals who supervise everything we ship, because frankly they generate more content than the codebase does.

We aim for a post every couple of weeks. No SEO filler, no "10 reasons why" listicles, no AI-generated fluff about synergy. If we publish it, it's because we'd want to read it.

<div class="divider">🍪 🍪 🍪</div>

## The name

The Cookie Jar felt right. It's where the good stuff is kept, everyone sneaks into it when nobody's watching, and Cookie — the original good girl this whole studio is named after — strongly approves of anything jar-adjacent.

Got a topic you'd like us to cover? [Email us](mailto:hello@cookiebytes.pt). If it's a good one, we'll bake it.
Loading
Loading