Compare commits
309 Commits
alpha-v202
...
alpha-v202
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
76d8bba332 | ||
|
|
86853cf337 | ||
|
|
9a292d2476 | ||
|
|
4a22b02810 | ||
|
|
e4270cda3b | ||
|
|
9cb0de37a5 | ||
|
|
2a794cb373 | ||
|
|
324af6b271 | ||
|
|
926db0eeb5 | ||
|
|
b1a97834c9 | ||
|
|
da13cb38ac | ||
|
|
dbf8ba5f40 | ||
|
|
3e8a6e5d7c | ||
|
|
8141644729 | ||
|
|
0d88c75718 | ||
|
|
f8721f2a1b | ||
|
|
fb39c6679a | ||
|
|
f1bed131bf | ||
|
|
257cf6ed02 | ||
|
|
908daafaa1 | ||
|
|
d15437face | ||
|
|
30281f879d | ||
|
|
4ef6edfb10 | ||
|
|
918419e371 | ||
|
|
2704002973 | ||
|
|
9265b8f117 | ||
|
|
943fa854e1 | ||
|
|
a2f8ba72a2 | ||
|
|
1d546dde3d | ||
|
|
63877e61ec | ||
|
|
8896f1d451 | ||
|
|
f523099854 | ||
|
|
15f38f096f | ||
|
|
cdd2ddec6c | ||
|
|
292d3739df | ||
|
|
1136c1948e | ||
|
|
8406042db1 | ||
|
|
f07cbbe0eb | ||
|
|
768da9f955 | ||
|
|
f58e9d8930 | ||
|
|
133242eff5 | ||
|
|
aebd2bea4a | ||
|
|
60238fbe78 | ||
|
|
5336236e79 | ||
|
|
f9719f11b5 | ||
|
|
e4083ded1a | ||
|
|
65a89f102a | ||
|
|
5b34ec2980 | ||
|
|
e579979829 | ||
|
|
c7edc71a97 | ||
|
|
5fd8f8fb40 | ||
|
|
694419d442 | ||
|
|
e82fb2a5c7 | ||
|
|
39234dcf52 | ||
|
|
36e8a62284 | ||
|
|
d622b91b81 | ||
|
|
5a546b9a8b | ||
|
|
b8c1a094b6 | ||
|
|
c2c56cfca5 | ||
|
|
449a62e31f | ||
|
|
7d6fd5cc51 | ||
|
|
75ed6460ac | ||
|
|
a3096c596f | ||
|
|
6105c7527c | ||
|
|
92eee0d470 | ||
|
|
f14fda09d5 | ||
|
|
31d12d8cc8 | ||
|
|
6a002f0dc0 | ||
|
|
9d30d1165f | ||
|
|
b709acd58a | ||
|
|
080e3498a7 | ||
|
|
51010ca578 | ||
|
|
15bc3a4701 | ||
|
|
3ebfa642fa | ||
|
|
8207b87c33 | ||
|
|
602a099ac3 | ||
|
|
2315122c4a | ||
|
|
ab4fcdbaae | ||
|
|
a56cb8c287 | ||
|
|
b93e6d491a | ||
|
|
3152ff2c6e | ||
|
|
dd018cf98b | ||
|
|
d0d9d90096 | ||
|
|
944efada94 | ||
|
|
ef4ad256e3 | ||
|
|
c427cd2492 | ||
|
|
151e993ab7 | ||
|
|
60554d0b96 | ||
|
|
4929f11b6d | ||
|
|
3f4e5b585f | ||
|
|
cd49c81bd3 | ||
|
|
b9f3dc9e1a | ||
|
|
122aba7878 | ||
|
|
05896bde19 | ||
|
|
87a933f39c | ||
|
|
54c0efa3e4 | ||
|
|
bdde0c8943 | ||
|
|
a474303eb9 | ||
|
|
4701485ee5 | ||
|
|
135a8a0adf | ||
|
|
0f5d7d5340 | ||
|
|
d6b3c0aef3 | ||
|
|
f896c01829 | ||
|
|
c3cff0c461 | ||
|
|
8c7f9238cb | ||
|
|
38acebba7c | ||
|
|
622977aeb8 | ||
|
|
9ce1c6f854 | ||
|
|
46865638cd | ||
|
|
564ee0a387 | ||
|
|
11655c0283 | ||
|
|
c24fd30266 | ||
|
|
daa1e3e985 | ||
|
|
44c7f43e57 | ||
|
|
f2c83be4b3 | ||
|
|
6104402bfb | ||
|
|
b3744e3bf5 | ||
|
|
67c8598b87 | ||
|
|
52620c80dc | ||
|
|
7e386ff70c | ||
|
|
aba5550b8e | ||
|
|
309b816e6d | ||
|
|
f552d8abff | ||
|
|
9f6779e9f1 | ||
|
|
bddf5106bb | ||
|
|
ba5c165016 | ||
|
|
39415c350f | ||
|
|
499e481df8 | ||
|
|
843cedd14e | ||
|
|
05156ca793 | ||
|
|
2308b269c8 | ||
|
|
b7f482bf27 | ||
|
|
33d7404d6c | ||
|
|
0ecac2ab79 | ||
|
|
7c9f2b0dda | ||
|
|
24e317cfa6 | ||
|
|
66c183fa61 | ||
|
|
dacf9bb19b | ||
|
|
3c92c200ab | ||
|
|
61c3a3e21b | ||
|
|
2e7c94d159 | ||
|
|
98ade7411a | ||
|
|
18776341f8 | ||
|
|
57bbcdbaef | ||
|
|
42bb6c4b18 | ||
|
|
070cd3ac6d | ||
|
|
accc6270d3 | ||
|
|
941f12aa1b | ||
|
|
4710a2d0a9 | ||
|
|
65a421215a | ||
|
|
4ed0289989 | ||
|
|
4ad32faa7b | ||
|
|
c9623d5ca8 | ||
|
|
77286457bf | ||
|
|
5b8aa8da0d | ||
|
|
21b6d8984a | ||
|
|
11fc123492 | ||
|
|
d98fa94e14 | ||
|
|
add3c4075f | ||
|
|
fb0d550eb2 | ||
|
|
6adf7fd53b | ||
|
|
3b4d9599d2 | ||
|
|
bb1b18686f | ||
|
|
6dbcc334bf | ||
|
|
4944365ed7 | ||
|
|
c4c6bb6c57 | ||
|
|
35dd2dd47b | ||
|
|
89141bae14 | ||
|
|
2b9294f884 | ||
|
|
a756af54ec | ||
|
|
b01b156dfb | ||
|
|
2c9361d704 | ||
|
|
cea71a4fd9 | ||
|
|
8b73ef5836 | ||
|
|
a6a727a4c0 | ||
|
|
bf13eed3ab | ||
|
|
a27a70e552 | ||
|
|
ca92bf4f77 | ||
|
|
9fda0a67ec | ||
|
|
0ffb7c65a9 | ||
|
|
633d9f1496 | ||
|
|
ba3d413b94 | ||
|
|
f3286922ad | ||
|
|
b0b476c9e5 | ||
|
|
dac696b752 | ||
|
|
cda87ee6c0 | ||
|
|
33324a5c79 | ||
|
|
f28626bbfe | ||
|
|
1bc2e3d1e0 | ||
|
|
8e6bdab3e6 | ||
|
|
d52362ad9b | ||
|
|
bf442646f5 | ||
|
|
1b3a444368 | ||
|
|
c4da7f1f2f | ||
|
|
c5fe291a9a | ||
|
|
59268acd04 | ||
|
|
3e4e2ebdc6 | ||
|
|
05375258a2 | ||
|
|
0c0354bfb8 | ||
|
|
6023e95d5b | ||
|
|
88d5694353 | ||
|
|
db6b38843b | ||
|
|
ad9901bbf5 | ||
|
|
b785c53ef9 | ||
|
|
8ae1ade220 | ||
|
|
35b035643e | ||
|
|
d3d663af0b | ||
|
|
8f624350d8 | ||
|
|
0e54173834 | ||
|
|
a6f1524a78 | ||
|
|
4cd5baeb55 | ||
|
|
fb1265e088 | ||
|
|
ca89662231 | ||
|
|
97f285e3cb | ||
|
|
4bd4ef43dc | ||
|
|
be29880a3e | ||
|
|
7be5519f61 | ||
|
|
8cefbe949a | ||
|
|
dd0a55b8e8 | ||
|
|
610276b1bc | ||
|
|
e6c968e37e | ||
|
|
367a66f32a | ||
|
|
6a4046915c | ||
|
|
ec8b637c84 | ||
|
|
3ee42fb856 | ||
|
|
eacf05a9f8 | ||
|
|
a199fad803 | ||
|
|
e956c806e5 | ||
|
|
7a6e3863ad | ||
|
|
e0e2dc4063 | ||
|
|
0020ade6d7 | ||
|
|
503e482c7d | ||
|
|
0d0e6af4c9 | ||
|
|
dbaf361878 | ||
|
|
d3741c82f5 | ||
|
|
b90d2f8612 | ||
|
|
9536efee0e | ||
|
|
c86a6ab9c5 | ||
|
|
1d7c027700 | ||
|
|
74e50c64bb | ||
|
|
8b97ef711f | ||
|
|
08855cee92 | ||
|
|
03042bb49c | ||
|
|
cee4bef179 | ||
|
|
f223d4f654 | ||
|
|
2f693c4476 | ||
|
|
44f0c7cb74 | ||
|
|
ea14f1a389 | ||
|
|
db21ae4708 | ||
|
|
8019c4fb2d | ||
|
|
0d552e1370 | ||
|
|
977f5593fa | ||
|
|
261a5befc7 | ||
|
|
8d3cce7b49 | ||
|
|
e0594820f0 | ||
|
|
b98d41212e | ||
|
|
098dc421cb | ||
|
|
326ccb08e1 | ||
|
|
c2bbdb0085 | ||
|
|
039f9e447f | ||
|
|
8581a1b1e6 | ||
|
|
2c5cb593d3 | ||
|
|
348b627029 | ||
|
|
f5cc5ffb4f | ||
|
|
374b4f6686 | ||
|
|
36c20f34f4 | ||
|
|
8551d819bd | ||
|
|
d5497d7252 | ||
|
|
046afe0e67 | ||
|
|
cb1cc4b394 | ||
|
|
00cb938766 | ||
|
|
80c13272f3 | ||
|
|
9534449307 | ||
|
|
2d5361b2e9 | ||
|
|
b22da65d44 | ||
|
|
862af864d3 | ||
|
|
9ecefc90d5 | ||
|
|
9693fe24c9 | ||
|
|
92ea32c0b8 | ||
|
|
2e2bd2f3b3 | ||
|
|
3556d934ee | ||
|
|
feef938174 | ||
|
|
67a9930ea1 | ||
|
|
aed4d05f1d | ||
|
|
cac7f12c6d | ||
|
|
72b5b52140 | ||
|
|
7deeb49751 | ||
|
|
8c0a2e7ec8 | ||
|
|
c4e66dc74f | ||
|
|
c0bb722db5 | ||
|
|
bc11c869aa | ||
|
|
502ecf0572 | ||
|
|
8bbbba98b7 | ||
|
|
86adf860b5 | ||
|
|
cec1627a81 | ||
|
|
b00183419e | ||
|
|
ed9ceff1d2 | ||
|
|
e18491bcd3 | ||
|
|
fd0cfc7fb6 | ||
|
|
6541ce5755 | ||
|
|
4456801526 | ||
|
|
db7aaf1385 | ||
|
|
54b2616fa4 | ||
|
|
6a90e9f770 | ||
|
|
b79c78a5ba | ||
|
|
48a8882970 | ||
|
|
98276702a2 | ||
|
|
857d55a7cd | ||
|
|
b78dfad384 |
@@ -1,2 +1,2 @@
|
||||
HOTSPOT_THRESHOLD=9.86
|
||||
AVERAGE_THRESHOLD=9.7
|
||||
HOTSPOT_THRESHOLD=10.0
|
||||
AVERAGE_THRESHOLD=9.88
|
||||
|
||||
@@ -12,3 +12,7 @@ VITE_SENTRY_DSN=
|
||||
# PostHog (https://posthog.com → Project → Settings → Project API Key)
|
||||
VITE_POSTHOG_KEY=
|
||||
VITE_POSTHOG_HOST=https://eu.i.posthog.com
|
||||
|
||||
# Lara CLI (https://github.com/translated/lara-cli)
|
||||
LARA_ACCESS_KEY_ID=
|
||||
LARA_ACCESS_KEY_SECRET=
|
||||
|
||||
3
.github/FUNDING.yml
vendored
Normal file
3
.github/FUNDING.yml
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
# These are supported funding model platforms
|
||||
|
||||
custom: https://refactoring.fm/
|
||||
11
.github/workflows/README.md
vendored
11
.github/workflows/README.md
vendored
@@ -10,6 +10,7 @@ Il workflow `ci.yml` esegue i seguenti check automatici:
|
||||
|
||||
### 2. Test Coverage
|
||||
- Frontend: vitest con coverage reporting
|
||||
- Upload automatico su Codecov dai report LCOV frontend + Rust
|
||||
- Threshold configurabile in `vitest.config.ts`
|
||||
|
||||
### 3. Code Health (CodeScene)
|
||||
@@ -40,6 +41,11 @@ CODESCENE_PROJECT_ID=<your-project-id>
|
||||
Il PAT di CodeScene è lo stesso che usi localmente (~/.codescene/token).
|
||||
Il project ID lo trovi nella dashboard CodeScene.
|
||||
|
||||
### Codecov Setup
|
||||
- Installa/attiva il repo in Codecov una volta sola tramite GitHub App / import del repository.
|
||||
- Nessun `CODECOV_TOKEN` richiesto in GitHub Actions: `ci.yml` usa OIDC (`id-token: write` + `use_oidc: true`).
|
||||
- Il workflow carica `coverage/lcov.info` (Vitest) e `coverage/rust.lcov` (cargo-llvm-cov).
|
||||
|
||||
### Telemetry Secrets For Release Builds
|
||||
Aggiungi anche questi secrets per i workflow `release.yml` e `release-stable.yml`:
|
||||
|
||||
@@ -97,8 +103,11 @@ codescene delta-analysis --base-revision origin/main
|
||||
|
||||
## Workflow Triggers
|
||||
|
||||
- **Push**: su `main` e branch `experiment/*`
|
||||
- **Push**: su `main`
|
||||
- **Pull Request**: verso `main`
|
||||
- **Manuale**: `workflow_dispatch`
|
||||
|
||||
Nota: l'upload a Codecov gira su push a `main` e sulle PR dello stesso repository. Le PR da fork saltano l'upload per evitare problemi di permessi OIDC.
|
||||
|
||||
## Status Checks
|
||||
|
||||
|
||||
80
.github/workflows/ci.yml
vendored
80
.github/workflows/ci.yml
vendored
@@ -3,6 +3,13 @@ name: CI
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
branches: [main]
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write
|
||||
|
||||
env:
|
||||
# Bump this when Tauri/Rust target artifacts capture stale absolute paths.
|
||||
@@ -78,10 +85,23 @@ jobs:
|
||||
cargo llvm-cov \
|
||||
--manifest-path src-tauri/Cargo.toml \
|
||||
--ignore-filename-regex 'lib\.rs|main\.rs|menu\.rs' \
|
||||
--lcov \
|
||||
--output-path coverage/rust.lcov \
|
||||
--fail-under-lines 85
|
||||
# cargo-llvm-cov exits non-zero if line coverage drops below 85%
|
||||
# lib.rs/main.rs/menu.rs are Tauri boilerplate -- not meaningfully unit-testable.
|
||||
|
||||
- name: Upload coverage to Codecov
|
||||
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository
|
||||
uses: codecov/codecov-action@v5
|
||||
with:
|
||||
use_oidc: true
|
||||
fail_ci_if_error: true
|
||||
disable_search: true
|
||||
files: ./coverage/lcov.info,./coverage/rust.lcov
|
||||
verbose: true
|
||||
# OIDC avoids long-lived CODECOV_TOKEN secrets.
|
||||
|
||||
# ── 3. Code Health (CodeScene — Hotspot + Average Code Health gates) ──
|
||||
# Enforces minimum floors on BOTH hotspot and average code health.
|
||||
# Thresholds come from .codescene-thresholds so CI and local hooks match.
|
||||
@@ -147,3 +167,63 @@ jobs:
|
||||
|
||||
- name: Format check (Rust)
|
||||
run: cargo fmt --manifest-path=src-tauri/Cargo.toml -- --check
|
||||
|
||||
linux-build:
|
||||
name: Linux build verification
|
||||
runs-on: ubuntu-22.04
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Install Tauri Linux system dependencies
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y \
|
||||
libwebkit2gtk-4.1-dev \
|
||||
libsoup-3.0-dev \
|
||||
libxdo-dev \
|
||||
libssl-dev \
|
||||
libayatana-appindicator3-dev \
|
||||
librsvg2-dev \
|
||||
patchelf \
|
||||
build-essential \
|
||||
file
|
||||
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 10
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '22'
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Setup Rust
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
components: clippy
|
||||
|
||||
- name: Cache Rust dependencies
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/registry
|
||||
~/.cargo/git
|
||||
src-tauri/target
|
||||
key: ${{ runner.os }}-cargo-${{ env.RUST_TARGET_CACHE_VERSION }}-${{ hashFiles('src-tauri/Cargo.lock') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-cargo-${{ env.RUST_TARGET_CACHE_VERSION }}-
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Frontend build
|
||||
run: pnpm build
|
||||
|
||||
- name: Cargo check
|
||||
run: cargo check --manifest-path=src-tauri/Cargo.toml
|
||||
|
||||
- name: Clippy
|
||||
run: cargo clippy --manifest-path=src-tauri/Cargo.toml -- -D warnings
|
||||
|
||||
520
.github/workflows/release-stable.yml
vendored
520
.github/workflows/release-stable.yml
vendored
@@ -50,7 +50,7 @@ jobs:
|
||||
echo "### Stable version: \`$DISPLAY_VERSION\`" >> "$GITHUB_STEP_SUMMARY"
|
||||
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
# Phase 2: Build each architecture in parallel
|
||||
# Phase 2: Build release bundles in parallel
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
build:
|
||||
name: Build (${{ matrix.arch }})
|
||||
@@ -62,6 +62,8 @@ jobs:
|
||||
include:
|
||||
- arch: aarch64
|
||||
target: aarch64-apple-darwin
|
||||
- arch: x86_64
|
||||
target: x86_64-apple-darwin
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
@@ -127,6 +129,95 @@ jobs:
|
||||
security set-key-partition-list -S apple-tool:,apple: -s -k "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
|
||||
echo "KEYCHAIN_PATH=$KEYCHAIN_PATH" >> "$GITHUB_ENV"
|
||||
|
||||
- name: Validate telemetry env
|
||||
env:
|
||||
VITE_SENTRY_DSN: ${{ secrets.VITE_SENTRY_DSN }}
|
||||
SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
|
||||
VITE_POSTHOG_KEY: ${{ secrets.VITE_POSTHOG_KEY }}
|
||||
VITE_POSTHOG_HOST: ${{ secrets.VITE_POSTHOG_HOST }}
|
||||
run: |
|
||||
python3 <<'PY'
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
from urllib.parse import urlparse
|
||||
|
||||
DISALLOWED_PLACEHOLDERS = {
|
||||
"",
|
||||
"-",
|
||||
"_",
|
||||
"false",
|
||||
"true",
|
||||
"null",
|
||||
"undefined",
|
||||
"none",
|
||||
"disabled",
|
||||
}
|
||||
|
||||
def normalize(name: str) -> str:
|
||||
value = os.getenv(name, "").strip()
|
||||
if len(value) >= 2 and value[0] == value[-1] and value[0] in ("'", '"'):
|
||||
value = value[1:-1].strip()
|
||||
return value
|
||||
|
||||
def normalize_http_like(value: str) -> str:
|
||||
if "://" in value:
|
||||
return value
|
||||
return f"https://{value}"
|
||||
|
||||
def normalize_hostname(hostname: str) -> str:
|
||||
normalized = hostname.strip().rstrip('.').lower()
|
||||
if normalized.startswith('[') and normalized.endswith(']'):
|
||||
normalized = normalized[1:-1]
|
||||
return normalized
|
||||
|
||||
def is_ip_address(hostname: str) -> bool:
|
||||
if re.fullmatch(r"(?:\d{1,3}\.){3}\d{1,3}", hostname):
|
||||
return all(0 <= int(part) <= 255 for part in hostname.split('.'))
|
||||
return ':' in hostname and re.fullmatch(r"[\da-f:]+", hostname, re.IGNORECASE) is not None
|
||||
|
||||
def is_allowed_hostname(hostname: str) -> bool:
|
||||
normalized = normalize_hostname(hostname)
|
||||
if not normalized or normalized in DISALLOWED_PLACEHOLDERS:
|
||||
return False
|
||||
if normalized == 'localhost':
|
||||
return True
|
||||
return '.' in normalized or is_ip_address(normalized)
|
||||
|
||||
def is_http_url(value: str) -> bool:
|
||||
parsed = urlparse(normalize_http_like(value))
|
||||
return parsed.scheme in {"http", "https"} and is_allowed_hostname(parsed.hostname or "")
|
||||
|
||||
values = {
|
||||
name: normalize(name)
|
||||
for name in (
|
||||
"VITE_SENTRY_DSN",
|
||||
"SENTRY_DSN",
|
||||
"VITE_POSTHOG_KEY",
|
||||
"VITE_POSTHOG_HOST",
|
||||
)
|
||||
}
|
||||
errors = []
|
||||
|
||||
for name in ("VITE_SENTRY_DSN", "SENTRY_DSN", "VITE_POSTHOG_HOST"):
|
||||
value = values[name]
|
||||
if value.lower() in DISALLOWED_PLACEHOLDERS:
|
||||
errors.append(f"{name} must be set to a real value, not a placeholder")
|
||||
elif not is_http_url(value):
|
||||
errors.append(f"{name} must be a valid http(s) URL with a non-placeholder host")
|
||||
|
||||
if values["VITE_POSTHOG_KEY"].lower() in DISALLOWED_PLACEHOLDERS:
|
||||
errors.append("VITE_POSTHOG_KEY must be set to a real project API key, not a placeholder")
|
||||
|
||||
if errors:
|
||||
print("Telemetry env validation failed:", file=sys.stderr)
|
||||
for error in errors:
|
||||
print(f"- {error}", file=sys.stderr)
|
||||
raise SystemExit(1)
|
||||
|
||||
print("Telemetry env validation passed.")
|
||||
PY
|
||||
|
||||
- name: Build Tauri app (with signing + notarization)
|
||||
env:
|
||||
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
|
||||
@@ -138,6 +229,7 @@ jobs:
|
||||
APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
|
||||
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
|
||||
VITE_SENTRY_DSN: ${{ secrets.VITE_SENTRY_DSN }}
|
||||
VITE_SENTRY_RELEASE: ${{ needs.version.outputs.version }}
|
||||
SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
|
||||
VITE_POSTHOG_KEY: ${{ secrets.VITE_POSTHOG_KEY }}
|
||||
VITE_POSTHOG_HOST: ${{ secrets.VITE_POSTHOG_HOST }}
|
||||
@@ -160,12 +252,245 @@ jobs:
|
||||
src-tauri/target/${{ matrix.target }}/release/bundle/macos/*.app.tar.gz.sig
|
||||
retention-days: 1
|
||||
|
||||
build-linux:
|
||||
name: Build (linux-x86_64)
|
||||
needs: version
|
||||
runs-on: ubuntu-22.04
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Install Tauri Linux system dependencies
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y \
|
||||
libwebkit2gtk-4.1-dev \
|
||||
libsoup-3.0-dev \
|
||||
libxdo-dev \
|
||||
libssl-dev \
|
||||
libayatana-appindicator3-dev \
|
||||
librsvg2-dev \
|
||||
curl \
|
||||
wget \
|
||||
patchelf \
|
||||
build-essential \
|
||||
file
|
||||
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 10
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '22'
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Setup Rust
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
targets: x86_64-unknown-linux-gnu
|
||||
|
||||
- name: Cache Rust dependencies
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/registry
|
||||
~/.cargo/git
|
||||
src-tauri/target
|
||||
key: ${{ runner.os }}-release-cargo-x86_64-unknown-linux-gnu-${{ env.RUST_TARGET_CACHE_VERSION }}-${{ hashFiles('src-tauri/Cargo.lock') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-release-cargo-x86_64-unknown-linux-gnu-${{ env.RUST_TARGET_CACHE_VERSION }}-
|
||||
|
||||
- name: Install frontend dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Clear cached bundle artifacts
|
||||
run: |
|
||||
rm -rf src-tauri/target/x86_64-unknown-linux-gnu/release/bundle
|
||||
|
||||
- name: Set version
|
||||
run: |
|
||||
VERSION="${{ needs.version.outputs.version }}"
|
||||
jq --arg v "$VERSION" '.version = $v' src-tauri/tauri.conf.json > tmp.json && mv tmp.json src-tauri/tauri.conf.json
|
||||
sed -i "s/^version = \".*\"/version = \"$VERSION\"/" src-tauri/Cargo.toml
|
||||
|
||||
- name: Build Tauri app (Linux bundles)
|
||||
env:
|
||||
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
|
||||
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
|
||||
VITE_SENTRY_DSN: ${{ secrets.VITE_SENTRY_DSN }}
|
||||
VITE_SENTRY_RELEASE: ${{ needs.version.outputs.version }}
|
||||
SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
|
||||
VITE_POSTHOG_KEY: ${{ secrets.VITE_POSTHOG_KEY }}
|
||||
VITE_POSTHOG_HOST: ${{ secrets.VITE_POSTHOG_HOST }}
|
||||
run: |
|
||||
pnpm tauri build --target x86_64-unknown-linux-gnu --bundles deb,appimage
|
||||
|
||||
- name: Validate Linux bundles
|
||||
run: |
|
||||
shopt -s nullglob
|
||||
installers=(
|
||||
src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/appimage/*.AppImage
|
||||
src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/deb/*.deb
|
||||
)
|
||||
signatures=(
|
||||
src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/appimage/*.AppImage.sig
|
||||
src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/appimage/*.AppImage.tar.gz.sig
|
||||
src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/deb/*.deb.sig
|
||||
)
|
||||
if [ ${#installers[@]} -eq 0 ]; then
|
||||
echo "::error::Linux build produced no AppImage or deb bundle."
|
||||
exit 1
|
||||
fi
|
||||
if [ ${#signatures[@]} -eq 0 ]; then
|
||||
echo "::error::Linux build produced no updater signature (.sig) artifact."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Upload Linux bundles
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: linux-x86_64-bundles
|
||||
path: |
|
||||
src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/deb/*.deb
|
||||
src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/deb/*.deb.sig
|
||||
src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/appimage/*.AppImage
|
||||
src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/appimage/*.AppImage.sig
|
||||
src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/appimage/*.AppImage.tar.gz
|
||||
src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/appimage/*.AppImage.tar.gz.sig
|
||||
if-no-files-found: error
|
||||
retention-days: 1
|
||||
|
||||
build-windows:
|
||||
name: Build (windows-x86_64)
|
||||
needs: version
|
||||
runs-on: windows-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 10
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '22'
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Setup Rust
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
targets: x86_64-pc-windows-msvc
|
||||
|
||||
- name: Cache Rust dependencies
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
~\.cargo\registry
|
||||
~\.cargo\git
|
||||
src-tauri\target
|
||||
key: ${{ runner.os }}-release-cargo-x86_64-pc-windows-msvc-${{ env.RUST_TARGET_CACHE_VERSION }}-${{ hashFiles('src-tauri/Cargo.lock') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-release-cargo-x86_64-pc-windows-msvc-${{ env.RUST_TARGET_CACHE_VERSION }}-
|
||||
|
||||
- name: Install frontend dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Clear cached Windows bundle artifacts
|
||||
shell: pwsh
|
||||
run: |
|
||||
Remove-Item -Recurse -Force -ErrorAction SilentlyContinue "src-tauri/target/x86_64-pc-windows-msvc/release/bundle"
|
||||
|
||||
- name: Set version
|
||||
shell: pwsh
|
||||
run: |
|
||||
$version = "${{ needs.version.outputs.version }}"
|
||||
$tauri = Get-Content "src-tauri/tauri.conf.json" | ConvertFrom-Json
|
||||
$tauri.version = $version
|
||||
$tauri | ConvertTo-Json -Depth 100 | Set-Content "src-tauri/tauri.conf.json"
|
||||
(Get-Content "src-tauri/Cargo.toml") -replace '^version = ".*"$', "version = `"$version`"" | Set-Content "src-tauri/Cargo.toml"
|
||||
|
||||
- name: Validate Windows release env
|
||||
shell: bash
|
||||
env:
|
||||
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
|
||||
TAURI_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
|
||||
run: |
|
||||
for name in TAURI_SIGNING_PRIVATE_KEY TAURI_KEY_PASSWORD; do
|
||||
if [ -z "${!name}" ]; then
|
||||
echo "::error::$name is required to build signed Windows updater artifacts."
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
- name: Build Tauri app (Windows bundles)
|
||||
env:
|
||||
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
|
||||
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
|
||||
VITE_SENTRY_DSN: ${{ secrets.VITE_SENTRY_DSN }}
|
||||
VITE_SENTRY_RELEASE: ${{ needs.version.outputs.version }}
|
||||
SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
|
||||
VITE_POSTHOG_KEY: ${{ secrets.VITE_POSTHOG_KEY }}
|
||||
VITE_POSTHOG_HOST: ${{ secrets.VITE_POSTHOG_HOST }}
|
||||
run: |
|
||||
pnpm tauri build --target x86_64-pc-windows-msvc --bundles nsis
|
||||
|
||||
- name: Validate Windows bundles
|
||||
shell: bash
|
||||
run: |
|
||||
shopt -s nullglob
|
||||
installers=(
|
||||
src-tauri/target/x86_64-pc-windows-msvc/release/bundle/nsis/*-setup.exe
|
||||
src-tauri/target/x86_64-pc-windows-msvc/release/bundle/msi/*.msi
|
||||
)
|
||||
signatures=(
|
||||
src-tauri/target/x86_64-pc-windows-msvc/release/bundle/nsis/*-setup.exe.sig
|
||||
src-tauri/target/x86_64-pc-windows-msvc/release/bundle/nsis/*.nsis.zip.sig
|
||||
src-tauri/target/x86_64-pc-windows-msvc/release/bundle/msi/*.msi.sig
|
||||
src-tauri/target/x86_64-pc-windows-msvc/release/bundle/msi/*.msi.zip.sig
|
||||
)
|
||||
if [ ${#installers[@]} -eq 0 ]; then
|
||||
echo "::error::Windows build produced no installable NSIS or MSI bundle."
|
||||
exit 1
|
||||
fi
|
||||
for installer in "${installers[@]}"; do
|
||||
if [[ "$(basename "$installer")" != *"${{ needs.version.outputs.version }}"* ]]; then
|
||||
echo "::error::Windows build produced an installer for a different version: $(basename "$installer")"
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
if [ ${#signatures[@]} -eq 0 ]; then
|
||||
echo "::error::Windows build produced no updater signature (.sig) artifact."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Upload Windows bundles
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: windows-x86_64-bundles
|
||||
path: |
|
||||
src-tauri/target/x86_64-pc-windows-msvc/release/bundle/nsis/*.exe
|
||||
src-tauri/target/x86_64-pc-windows-msvc/release/bundle/nsis/*.exe.sig
|
||||
src-tauri/target/x86_64-pc-windows-msvc/release/bundle/nsis/*.zip
|
||||
src-tauri/target/x86_64-pc-windows-msvc/release/bundle/nsis/*.zip.sig
|
||||
src-tauri/target/x86_64-pc-windows-msvc/release/bundle/msi/*.msi
|
||||
src-tauri/target/x86_64-pc-windows-msvc/release/bundle/msi/*.msi.sig
|
||||
src-tauri/target/x86_64-pc-windows-msvc/release/bundle/msi/*.zip
|
||||
src-tauri/target/x86_64-pc-windows-msvc/release/bundle/msi/*.zip.sig
|
||||
if-no-files-found: error
|
||||
retention-days: 1
|
||||
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
# Phase 3: Publish GitHub Release
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
release:
|
||||
name: GitHub Release (stable)
|
||||
needs: [version, build]
|
||||
needs: [version, build, build-linux, build-windows]
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
@@ -177,6 +502,54 @@ jobs:
|
||||
- name: Download all artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
|
||||
- name: Normalize macOS release artifact names
|
||||
run: |
|
||||
normalize_macos_artifacts() {
|
||||
local arch="$1"
|
||||
local normalized_updater="$2"
|
||||
local normalized_dmg="$3"
|
||||
local updater_dir="updater-${arch}"
|
||||
local updater_file
|
||||
updater_file=$(find "$updater_dir" -maxdepth 1 -name "*.app.tar.gz" -print -quit)
|
||||
if [ -z "$updater_file" ]; then
|
||||
echo "::error::Missing macOS updater artifact in ${updater_dir}" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
local sig_file="${updater_file}.sig"
|
||||
if [ ! -f "$sig_file" ]; then
|
||||
echo "::error::Missing macOS updater signature for ${updater_file}" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
local normalized_sig="${normalized_updater}.sig"
|
||||
if [ "$updater_file" != "$normalized_updater" ]; then
|
||||
mv "$updater_file" "$normalized_updater"
|
||||
fi
|
||||
if [ "$sig_file" != "$normalized_sig" ]; then
|
||||
mv "$sig_file" "$normalized_sig"
|
||||
fi
|
||||
|
||||
local dmg_dir="dmg-${arch}"
|
||||
local dmg_file
|
||||
dmg_file=$(find "$dmg_dir" -maxdepth 1 -name "*.dmg" -print -quit)
|
||||
if [ -z "$dmg_file" ]; then
|
||||
echo "::error::Missing macOS DMG artifact in ${dmg_dir}" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
if [ "$dmg_file" != "$normalized_dmg" ]; then
|
||||
mv "$dmg_file" "$normalized_dmg"
|
||||
fi
|
||||
}
|
||||
|
||||
normalize_macos_artifacts aarch64 \
|
||||
"updater-aarch64/Tolaria_${{ needs.version.outputs.version }}_macOS_Silicon.app.tar.gz" \
|
||||
"dmg-aarch64/Tolaria_${{ needs.version.outputs.version }}_macOS_Silicon.dmg"
|
||||
normalize_macos_artifacts x86_64 \
|
||||
"updater-x86_64/Tolaria_${{ needs.version.outputs.version }}_macOS_Intel.app.tar.gz" \
|
||||
"dmg-x86_64/Tolaria_${{ needs.version.outputs.version }}_macOS_Intel.dmg"
|
||||
|
||||
- name: Generate release notes
|
||||
run: |
|
||||
PREV_TAG=$(git tag --list 'stable-v*' --sort=-version:refname | grep -vx "${{ needs.version.outputs.tag }}" | head -n 1 || echo "")
|
||||
@@ -193,7 +566,7 @@ jobs:
|
||||
echo "---"
|
||||
echo "**Stable release — manually promoted from \`main\`**"
|
||||
echo ""
|
||||
echo "**Requires Apple Silicon (M1/M2/M3)**"
|
||||
echo "**Includes macOS (Apple Silicon and Intel), Windows x64, and Linux x64 bundles**"
|
||||
echo ""
|
||||
echo "*Built from \`$(git rev-parse --short ${{ needs.version.outputs.tag }})\` on $(date -u +%Y-%m-%d)*"
|
||||
} > release_notes.md
|
||||
@@ -206,8 +579,42 @@ jobs:
|
||||
REPO_NAME="${REPO#*/}"
|
||||
PAGES_URL="https://refactoringhq.github.io/${REPO_NAME}/"
|
||||
|
||||
ARM_SIG=$(cat updater-aarch64/*.app.tar.gz.sig)
|
||||
ARM_TARBALL=$(ls updater-aarch64/*.app.tar.gz | xargs basename)
|
||||
find_required() {
|
||||
local patterns=("$@")
|
||||
for pattern in "${patterns[@]}"; do
|
||||
set -- $pattern
|
||||
if [ -e "$1" ]; then
|
||||
printf '%s\n' "$1"
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
echo "::error::Missing required artifact matching one of: ${patterns[*]}" >&2
|
||||
return 1
|
||||
}
|
||||
|
||||
ARM_SIG_FILE=$(find_required "updater-aarch64/*.app.tar.gz.sig")
|
||||
ARM_UPDATER_FILE="${ARM_SIG_FILE%.sig}"
|
||||
ARM_SIG=$(cat "$ARM_SIG_FILE")
|
||||
ARM_TARBALL=$(basename "$ARM_UPDATER_FILE")
|
||||
ARM_DMG=$(basename "$(find_required "dmg-aarch64/*.dmg")")
|
||||
|
||||
INTEL_SIG_FILE=$(find_required "updater-x86_64/*.app.tar.gz.sig")
|
||||
INTEL_UPDATER_FILE="${INTEL_SIG_FILE%.sig}"
|
||||
INTEL_SIG=$(cat "$INTEL_SIG_FILE")
|
||||
INTEL_TARBALL=$(basename "$INTEL_UPDATER_FILE")
|
||||
INTEL_DMG=$(basename "$(find_required "dmg-x86_64/*.dmg")")
|
||||
|
||||
LINUX_SIG_FILE=$(find_required "linux-x86_64-bundles/*/*.AppImage.sig" "linux-x86_64-bundles/*/*.AppImage.tar.gz.sig" "linux-x86_64-bundles/*/*.deb.sig" "linux-x86_64-bundles/*.AppImage.sig" "linux-x86_64-bundles/*.AppImage.tar.gz.sig" "linux-x86_64-bundles/*.deb.sig")
|
||||
LINUX_UPDATER_FILE="${LINUX_SIG_FILE%.sig}"
|
||||
LINUX_SIG=$(cat "$LINUX_SIG_FILE")
|
||||
LINUX_UPDATER=$(basename "$LINUX_UPDATER_FILE")
|
||||
LINUX_DOWNLOAD=$(basename "$(find_required "linux-x86_64-bundles/*/*.AppImage" "linux-x86_64-bundles/*/*.deb" "linux-x86_64-bundles/*/*.AppImage.tar.gz" "linux-x86_64-bundles/*.AppImage" "linux-x86_64-bundles/*.deb" "linux-x86_64-bundles/*.AppImage.tar.gz")")
|
||||
|
||||
WINDOWS_SIG_FILE=$(find_required "windows-x86_64-bundles/*/*-setup.exe.sig" "windows-x86_64-bundles/*/*.msi.sig" "windows-x86_64-bundles/*/*.nsis.zip.sig" "windows-x86_64-bundles/*/*.msi.zip.sig" "windows-x86_64-bundles/*-setup.exe.sig" "windows-x86_64-bundles/*.msi.sig" "windows-x86_64-bundles/*.nsis.zip.sig" "windows-x86_64-bundles/*.msi.zip.sig")
|
||||
WINDOWS_UPDATER_FILE="${WINDOWS_SIG_FILE%.sig}"
|
||||
WINDOWS_SIG=$(cat "$WINDOWS_SIG_FILE")
|
||||
WINDOWS_UPDATER=$(basename "$WINDOWS_UPDATER_FILE")
|
||||
WINDOWS_DOWNLOAD=$(basename "$(find_required "windows-x86_64-bundles/*/*-setup.exe" "windows-x86_64-bundles/*/*.msi" "windows-x86_64-bundles/*/*.nsis.zip" "windows-x86_64-bundles/*/*.msi.zip" "windows-x86_64-bundles/*-setup.exe" "windows-x86_64-bundles/*.msi" "windows-x86_64-bundles/*.nsis.zip" "windows-x86_64-bundles/*.msi.zip")")
|
||||
|
||||
cat > stable-latest.json << EOF
|
||||
{
|
||||
@@ -217,7 +624,23 @@ jobs:
|
||||
"platforms": {
|
||||
"darwin-aarch64": {
|
||||
"signature": "${ARM_SIG}",
|
||||
"url": "https://github.com/${REPO}/releases/download/${TAG}/${ARM_TARBALL}"
|
||||
"url": "https://github.com/${REPO}/releases/download/${TAG}/${ARM_TARBALL}",
|
||||
"dmg_url": "https://github.com/${REPO}/releases/download/${TAG}/${ARM_DMG}"
|
||||
},
|
||||
"darwin-x86_64": {
|
||||
"signature": "${INTEL_SIG}",
|
||||
"url": "https://github.com/${REPO}/releases/download/${TAG}/${INTEL_TARBALL}",
|
||||
"dmg_url": "https://github.com/${REPO}/releases/download/${TAG}/${INTEL_DMG}"
|
||||
},
|
||||
"linux-x86_64": {
|
||||
"signature": "${LINUX_SIG}",
|
||||
"url": "https://github.com/${REPO}/releases/download/${TAG}/${LINUX_UPDATER}",
|
||||
"download_url": "https://github.com/${REPO}/releases/download/${TAG}/${LINUX_DOWNLOAD}"
|
||||
},
|
||||
"windows-x86_64": {
|
||||
"signature": "${WINDOWS_SIG}",
|
||||
"url": "https://github.com/${REPO}/releases/download/${TAG}/${WINDOWS_UPDATER}",
|
||||
"download_url": "https://github.com/${REPO}/releases/download/${TAG}/${WINDOWS_DOWNLOAD}"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -236,6 +659,33 @@ jobs:
|
||||
dmg-aarch64/*.dmg
|
||||
updater-aarch64/*.app.tar.gz
|
||||
updater-aarch64/*.app.tar.gz.sig
|
||||
dmg-x86_64/*.dmg
|
||||
updater-x86_64/*.app.tar.gz
|
||||
updater-x86_64/*.app.tar.gz.sig
|
||||
linux-x86_64-bundles/*.deb
|
||||
linux-x86_64-bundles/*.deb.sig
|
||||
linux-x86_64-bundles/*.AppImage
|
||||
linux-x86_64-bundles/*.AppImage.sig
|
||||
linux-x86_64-bundles/*.AppImage.tar.gz
|
||||
linux-x86_64-bundles/*.AppImage.tar.gz.sig
|
||||
linux-x86_64-bundles/*/*.deb
|
||||
linux-x86_64-bundles/*/*.deb.sig
|
||||
linux-x86_64-bundles/*/*.AppImage
|
||||
linux-x86_64-bundles/*/*.AppImage.sig
|
||||
linux-x86_64-bundles/*/*.AppImage.tar.gz
|
||||
linux-x86_64-bundles/*/*.AppImage.tar.gz.sig
|
||||
windows-x86_64-bundles/*.exe
|
||||
windows-x86_64-bundles/*.exe.sig
|
||||
windows-x86_64-bundles/*.msi
|
||||
windows-x86_64-bundles/*.msi.sig
|
||||
windows-x86_64-bundles/*.zip
|
||||
windows-x86_64-bundles/*.zip.sig
|
||||
windows-x86_64-bundles/*/*.exe
|
||||
windows-x86_64-bundles/*/*.exe.sig
|
||||
windows-x86_64-bundles/*/*.msi
|
||||
windows-x86_64-bundles/*/*.msi.sig
|
||||
windows-x86_64-bundles/*/*.zip
|
||||
windows-x86_64-bundles/*/*.zip.sig
|
||||
stable-latest.json
|
||||
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
@@ -253,67 +703,29 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Bun
|
||||
uses: oven-sh/setup-bun@v2
|
||||
with:
|
||||
bun-version: latest
|
||||
|
||||
- name: Build release history page
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
mkdir -p _site/alpha _site/stable
|
||||
gh api repos/${{ github.repository }}/releases --paginate > _site/releases.json
|
||||
gh api -H "Accept: application/vnd.github.html+json" repos/${{ github.repository }}/releases --paginate > _site/releases.json
|
||||
PAGES_URL="https://refactoringhq.github.io/${GITHUB_REPOSITORY#*/}"
|
||||
|
||||
curl -fsSL "${PAGES_URL}/alpha/latest.json" -o _site/alpha/latest.json || echo '{}' > _site/alpha/latest.json
|
||||
gh release download --repo ${{ github.repository }} "${{ needs.version.outputs.tag }}" --pattern "stable-latest.json" --output _site/stable/latest.json || echo '{}' > _site/stable/latest.json
|
||||
bun scripts/build-release-download-page.ts --latest-json _site/stable/latest.json --releases-json _site/releases.json --output-file _site/stable/download/index.html
|
||||
bun scripts/build-release-history-page.ts --releases-json _site/releases.json --output-file _site/index.html
|
||||
mkdir -p _site/download
|
||||
cp _site/stable/download/index.html _site/download/index.html
|
||||
|
||||
cp _site/alpha/latest.json _site/latest.json
|
||||
cp _site/alpha/latest.json _site/latest-canary.json
|
||||
|
||||
cat > _site/index.html << 'HTMLEOF'
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Tolaria — Release History</title>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; background: #F7F6F3; color: #37352F; line-height: 1.6; padding: 2rem; max-width: 720px; margin: 0 auto; }
|
||||
h1 { font-size: 1.75rem; font-weight: 600; margin-bottom: 0.5rem; }
|
||||
.subtitle { color: #787774; margin-bottom: 2rem; }
|
||||
.release { background: #fff; border: 1px solid #E9E9E7; border-radius: 8px; padding: 1.25rem 1.5rem; margin-bottom: 1rem; }
|
||||
.release h2 { font-size: 1.125rem; font-weight: 600; margin-bottom: 0.25rem; }
|
||||
.release .meta { font-size: 0.8125rem; color: #787774; margin-bottom: 0.75rem; }
|
||||
.release .body { font-size: 0.875rem; white-space: pre-wrap; }
|
||||
.release .downloads { margin-top: 0.75rem; display: flex; gap: 0.5rem; flex-wrap: wrap; }
|
||||
.release .downloads a { display: inline-block; padding: 0.375rem 0.75rem; background: #155DFF; color: #fff; border-radius: 6px; text-decoration: none; font-size: 0.8125rem; font-weight: 500; }
|
||||
.release .downloads a:hover { background: #1248CC; }
|
||||
.alpha { border-left: 3px solid #f59e0b; }
|
||||
.empty { color: #787774; text-align: center; padding: 3rem; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Tolaria Release History</h1>
|
||||
<p class="subtitle">Alpha builds update on every push to main. Stable builds appear when a stable-vYYYY.M.D tag is promoted.</p>
|
||||
<div id="releases"></div>
|
||||
<script>
|
||||
fetch('releases.json').then(r=>r.json()).then(releases=>{
|
||||
const el=document.getElementById('releases');
|
||||
if(!releases.length){el.innerHTML='<p class="empty">No releases yet.</p>';return;}
|
||||
releases.forEach(r=>{
|
||||
const date=new Date(r.published_at).toLocaleDateString('en-US',{year:'numeric',month:'long',day:'numeric'});
|
||||
const dmgs=(r.assets||[]).filter(a=>a.name.endsWith('.dmg'));
|
||||
const links=dmgs.map(a=>'<a href="'+a.browser_download_url+'">'+a.name+'</a>').join('');
|
||||
const body=(r.body||'').replace(/</g,'<').replace(/>/g,'>');
|
||||
const div=document.createElement('div');
|
||||
div.className='release'+(r.prerelease?' alpha':'');
|
||||
div.innerHTML='<h2>'+(r.name||r.tag_name)+'</h2><div class="meta">'+date+' · '+r.tag_name+(r.prerelease?' · <strong>Alpha</strong>':'')+'</div><div class="body">'+body+'</div>'+(links?'<div class="downloads">'+links+'</div>':'');
|
||||
el.appendChild(div);
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
HTMLEOF
|
||||
|
||||
- name: Deploy to GitHub Pages
|
||||
uses: peaceiris/actions-gh-pages@v4
|
||||
with:
|
||||
|
||||
539
.github/workflows/release.yml
vendored
539
.github/workflows/release.yml
vendored
@@ -42,6 +42,21 @@ jobs:
|
||||
output = subprocess.check_output(command, text=True).strip()
|
||||
return [line for line in output.splitlines() if line]
|
||||
|
||||
alpha_pattern = re.compile(r"^alpha-v(\d{4}\.\d{1,2}\.\d{1,2})-alpha\.(\d+)$")
|
||||
|
||||
def parse_alpha_tag(tag: str) -> tuple[str, int] | None:
|
||||
match = alpha_pattern.fullmatch(tag)
|
||||
if not match:
|
||||
return None
|
||||
calendar_version, sequence = match.groups()
|
||||
return calendar_version, int(sequence)
|
||||
|
||||
def alpha_version(calendar_version: str, sequence: int) -> str:
|
||||
return f"{calendar_version}-alpha.{sequence}"
|
||||
|
||||
def alpha_tag(calendar_version: str, sequence: int) -> str:
|
||||
return f"alpha-v{calendar_version}-alpha.{sequence:04d}"
|
||||
|
||||
existing_tags = [
|
||||
tag for tag in lines(["git", "tag", "--points-at", "HEAD"])
|
||||
if tag.startswith("alpha-v")
|
||||
@@ -49,7 +64,8 @@ jobs:
|
||||
|
||||
if existing_tags:
|
||||
tag = existing_tags[0]
|
||||
version = tag.removeprefix("alpha-v")
|
||||
parsed = parse_alpha_tag(tag)
|
||||
version = alpha_version(*parsed) if parsed is not None else tag.removeprefix("alpha-v")
|
||||
else:
|
||||
today = datetime.now(timezone.utc).date()
|
||||
stable_date = None
|
||||
@@ -71,8 +87,8 @@ jobs:
|
||||
calendar_version = f"{alpha_date.year}.{alpha_date.month}.{alpha_date.day}"
|
||||
sequence = len(lines(["git", "tag", "--list", f"alpha-v{calendar_version}-alpha.*"])) + 1
|
||||
|
||||
version = f"{calendar_version}-alpha.{sequence}"
|
||||
tag = f"alpha-v{version}"
|
||||
version = alpha_version(calendar_version, sequence)
|
||||
tag = alpha_tag(calendar_version, sequence)
|
||||
|
||||
display_match = re.fullmatch(r"(\d{4})\.(\d{1,2})\.(\d{1,2})-alpha\.(\d+)", version)
|
||||
display_version = (
|
||||
@@ -105,6 +121,8 @@ jobs:
|
||||
include:
|
||||
- arch: aarch64
|
||||
target: aarch64-apple-darwin
|
||||
- arch: x86_64
|
||||
target: x86_64-apple-darwin
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
@@ -170,6 +188,95 @@ jobs:
|
||||
security set-key-partition-list -S apple-tool:,apple: -s -k "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
|
||||
echo "KEYCHAIN_PATH=$KEYCHAIN_PATH" >> "$GITHUB_ENV"
|
||||
|
||||
- name: Validate telemetry env
|
||||
env:
|
||||
VITE_SENTRY_DSN: ${{ secrets.VITE_SENTRY_DSN }}
|
||||
SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
|
||||
VITE_POSTHOG_KEY: ${{ secrets.VITE_POSTHOG_KEY }}
|
||||
VITE_POSTHOG_HOST: ${{ secrets.VITE_POSTHOG_HOST }}
|
||||
run: |
|
||||
python3 <<'PY'
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
from urllib.parse import urlparse
|
||||
|
||||
DISALLOWED_PLACEHOLDERS = {
|
||||
"",
|
||||
"-",
|
||||
"_",
|
||||
"false",
|
||||
"true",
|
||||
"null",
|
||||
"undefined",
|
||||
"none",
|
||||
"disabled",
|
||||
}
|
||||
|
||||
def normalize(name: str) -> str:
|
||||
value = os.getenv(name, "").strip()
|
||||
if len(value) >= 2 and value[0] == value[-1] and value[0] in ("'", '"'):
|
||||
value = value[1:-1].strip()
|
||||
return value
|
||||
|
||||
def normalize_http_like(value: str) -> str:
|
||||
if "://" in value:
|
||||
return value
|
||||
return f"https://{value}"
|
||||
|
||||
def normalize_hostname(hostname: str) -> str:
|
||||
normalized = hostname.strip().rstrip('.').lower()
|
||||
if normalized.startswith('[') and normalized.endswith(']'):
|
||||
normalized = normalized[1:-1]
|
||||
return normalized
|
||||
|
||||
def is_ip_address(hostname: str) -> bool:
|
||||
if re.fullmatch(r"(?:\d{1,3}\.){3}\d{1,3}", hostname):
|
||||
return all(0 <= int(part) <= 255 for part in hostname.split('.'))
|
||||
return ':' in hostname and re.fullmatch(r"[\da-f:]+", hostname, re.IGNORECASE) is not None
|
||||
|
||||
def is_allowed_hostname(hostname: str) -> bool:
|
||||
normalized = normalize_hostname(hostname)
|
||||
if not normalized or normalized in DISALLOWED_PLACEHOLDERS:
|
||||
return False
|
||||
if normalized == 'localhost':
|
||||
return True
|
||||
return '.' in normalized or is_ip_address(normalized)
|
||||
|
||||
def is_http_url(value: str) -> bool:
|
||||
parsed = urlparse(normalize_http_like(value))
|
||||
return parsed.scheme in {"http", "https"} and is_allowed_hostname(parsed.hostname or "")
|
||||
|
||||
values = {
|
||||
name: normalize(name)
|
||||
for name in (
|
||||
"VITE_SENTRY_DSN",
|
||||
"SENTRY_DSN",
|
||||
"VITE_POSTHOG_KEY",
|
||||
"VITE_POSTHOG_HOST",
|
||||
)
|
||||
}
|
||||
errors = []
|
||||
|
||||
for name in ("VITE_SENTRY_DSN", "SENTRY_DSN", "VITE_POSTHOG_HOST"):
|
||||
value = values[name]
|
||||
if value.lower() in DISALLOWED_PLACEHOLDERS:
|
||||
errors.append(f"{name} must be set to a real value, not a placeholder")
|
||||
elif not is_http_url(value):
|
||||
errors.append(f"{name} must be a valid http(s) URL with a non-placeholder host")
|
||||
|
||||
if values["VITE_POSTHOG_KEY"].lower() in DISALLOWED_PLACEHOLDERS:
|
||||
errors.append("VITE_POSTHOG_KEY must be set to a real project API key, not a placeholder")
|
||||
|
||||
if errors:
|
||||
print("Telemetry env validation failed:", file=sys.stderr)
|
||||
for error in errors:
|
||||
print(f"- {error}", file=sys.stderr)
|
||||
raise SystemExit(1)
|
||||
|
||||
print("Telemetry env validation passed.")
|
||||
PY
|
||||
|
||||
- name: Build Tauri app (with signing + notarization)
|
||||
env:
|
||||
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
|
||||
@@ -181,6 +288,7 @@ jobs:
|
||||
APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
|
||||
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
|
||||
VITE_SENTRY_DSN: ${{ secrets.VITE_SENTRY_DSN }}
|
||||
VITE_SENTRY_RELEASE: ${{ needs.version.outputs.version }}
|
||||
SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
|
||||
VITE_POSTHOG_KEY: ${{ secrets.VITE_POSTHOG_KEY }}
|
||||
VITE_POSTHOG_HOST: ${{ secrets.VITE_POSTHOG_HOST }}
|
||||
@@ -198,13 +306,245 @@ jobs:
|
||||
src-tauri/target/${{ matrix.target }}/release/bundle/macos/*.app.tar.gz.sig
|
||||
retention-days: 1
|
||||
|
||||
build-linux:
|
||||
name: Build (linux-x86_64)
|
||||
needs: version
|
||||
runs-on: ubuntu-22.04
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Install Tauri Linux system dependencies
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y \
|
||||
libwebkit2gtk-4.1-dev \
|
||||
libsoup-3.0-dev \
|
||||
libxdo-dev \
|
||||
libssl-dev \
|
||||
libayatana-appindicator3-dev \
|
||||
librsvg2-dev \
|
||||
curl \
|
||||
wget \
|
||||
patchelf \
|
||||
build-essential \
|
||||
file
|
||||
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 10
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '22'
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Setup Rust
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
targets: x86_64-unknown-linux-gnu
|
||||
|
||||
- name: Cache Rust dependencies
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/registry
|
||||
~/.cargo/git
|
||||
src-tauri/target
|
||||
key: ${{ runner.os }}-release-cargo-x86_64-unknown-linux-gnu-${{ env.RUST_TARGET_CACHE_VERSION }}-${{ hashFiles('src-tauri/Cargo.lock') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-release-cargo-x86_64-unknown-linux-gnu-${{ env.RUST_TARGET_CACHE_VERSION }}-
|
||||
|
||||
- name: Install frontend dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Clear cached bundle artifacts
|
||||
run: |
|
||||
rm -rf src-tauri/target/x86_64-unknown-linux-gnu/release/bundle
|
||||
|
||||
- name: Set version
|
||||
run: |
|
||||
VERSION="${{ needs.version.outputs.version }}"
|
||||
jq --arg v "$VERSION" '.version = $v' src-tauri/tauri.conf.json > tmp.json && mv tmp.json src-tauri/tauri.conf.json
|
||||
sed -i "s/^version = \".*\"/version = \"$VERSION\"/" src-tauri/Cargo.toml
|
||||
|
||||
- name: Build Tauri app (Linux bundles)
|
||||
env:
|
||||
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
|
||||
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
|
||||
VITE_SENTRY_DSN: ${{ secrets.VITE_SENTRY_DSN }}
|
||||
VITE_SENTRY_RELEASE: ${{ needs.version.outputs.version }}
|
||||
SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
|
||||
VITE_POSTHOG_KEY: ${{ secrets.VITE_POSTHOG_KEY }}
|
||||
VITE_POSTHOG_HOST: ${{ secrets.VITE_POSTHOG_HOST }}
|
||||
run: |
|
||||
pnpm tauri build --target x86_64-unknown-linux-gnu --bundles deb,appimage
|
||||
|
||||
- name: Validate Linux bundles
|
||||
run: |
|
||||
shopt -s nullglob
|
||||
installers=(
|
||||
src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/appimage/*.AppImage
|
||||
src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/deb/*.deb
|
||||
)
|
||||
signatures=(
|
||||
src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/appimage/*.AppImage.sig
|
||||
src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/appimage/*.AppImage.tar.gz.sig
|
||||
src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/deb/*.deb.sig
|
||||
)
|
||||
if [ ${#installers[@]} -eq 0 ]; then
|
||||
echo "::error::Linux build produced no AppImage or deb bundle."
|
||||
exit 1
|
||||
fi
|
||||
if [ ${#signatures[@]} -eq 0 ]; then
|
||||
echo "::error::Linux build produced no updater signature (.sig) artifact."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Upload Linux bundles
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: linux-x86_64-bundles
|
||||
path: |
|
||||
src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/deb/*.deb
|
||||
src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/deb/*.deb.sig
|
||||
src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/appimage/*.AppImage
|
||||
src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/appimage/*.AppImage.sig
|
||||
src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/appimage/*.AppImage.tar.gz
|
||||
src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/appimage/*.AppImage.tar.gz.sig
|
||||
if-no-files-found: error
|
||||
retention-days: 1
|
||||
|
||||
build-windows:
|
||||
name: Build (windows-x86_64)
|
||||
needs: version
|
||||
runs-on: windows-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 10
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '22'
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Setup Rust
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
targets: x86_64-pc-windows-msvc
|
||||
|
||||
- name: Cache Rust dependencies
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
~\.cargo\registry
|
||||
~\.cargo\git
|
||||
src-tauri\target
|
||||
key: ${{ runner.os }}-release-cargo-x86_64-pc-windows-msvc-${{ env.RUST_TARGET_CACHE_VERSION }}-${{ hashFiles('src-tauri/Cargo.lock') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-release-cargo-x86_64-pc-windows-msvc-${{ env.RUST_TARGET_CACHE_VERSION }}-
|
||||
|
||||
- name: Install frontend dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Clear cached Windows bundle artifacts
|
||||
shell: pwsh
|
||||
run: |
|
||||
Remove-Item -Recurse -Force -ErrorAction SilentlyContinue "src-tauri/target/x86_64-pc-windows-msvc/release/bundle"
|
||||
|
||||
- name: Set version
|
||||
shell: pwsh
|
||||
run: |
|
||||
$version = "${{ needs.version.outputs.version }}"
|
||||
$tauri = Get-Content "src-tauri/tauri.conf.json" | ConvertFrom-Json
|
||||
$tauri.version = $version
|
||||
$tauri | ConvertTo-Json -Depth 100 | Set-Content "src-tauri/tauri.conf.json"
|
||||
(Get-Content "src-tauri/Cargo.toml") -replace '^version = ".*"$', "version = `"$version`"" | Set-Content "src-tauri/Cargo.toml"
|
||||
|
||||
- name: Validate Windows release env
|
||||
shell: bash
|
||||
env:
|
||||
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
|
||||
TAURI_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
|
||||
run: |
|
||||
for name in TAURI_SIGNING_PRIVATE_KEY TAURI_KEY_PASSWORD; do
|
||||
if [ -z "${!name}" ]; then
|
||||
echo "::error::$name is required to build signed Windows updater artifacts."
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
- name: Build Tauri app (Windows bundles)
|
||||
env:
|
||||
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
|
||||
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
|
||||
VITE_SENTRY_DSN: ${{ secrets.VITE_SENTRY_DSN }}
|
||||
VITE_SENTRY_RELEASE: ${{ needs.version.outputs.version }}
|
||||
SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
|
||||
VITE_POSTHOG_KEY: ${{ secrets.VITE_POSTHOG_KEY }}
|
||||
VITE_POSTHOG_HOST: ${{ secrets.VITE_POSTHOG_HOST }}
|
||||
run: |
|
||||
pnpm tauri build --target x86_64-pc-windows-msvc --bundles nsis
|
||||
|
||||
- name: Validate Windows bundles
|
||||
shell: bash
|
||||
run: |
|
||||
shopt -s nullglob
|
||||
installers=(
|
||||
src-tauri/target/x86_64-pc-windows-msvc/release/bundle/nsis/*-setup.exe
|
||||
src-tauri/target/x86_64-pc-windows-msvc/release/bundle/msi/*.msi
|
||||
)
|
||||
signatures=(
|
||||
src-tauri/target/x86_64-pc-windows-msvc/release/bundle/nsis/*-setup.exe.sig
|
||||
src-tauri/target/x86_64-pc-windows-msvc/release/bundle/nsis/*.nsis.zip.sig
|
||||
src-tauri/target/x86_64-pc-windows-msvc/release/bundle/msi/*.msi.sig
|
||||
src-tauri/target/x86_64-pc-windows-msvc/release/bundle/msi/*.msi.zip.sig
|
||||
)
|
||||
if [ ${#installers[@]} -eq 0 ]; then
|
||||
echo "::error::Windows build produced no installable NSIS or MSI bundle."
|
||||
exit 1
|
||||
fi
|
||||
for installer in "${installers[@]}"; do
|
||||
if [[ "$(basename "$installer")" != *"${{ needs.version.outputs.version }}"* ]]; then
|
||||
echo "::error::Windows build produced an installer for a different version: $(basename "$installer")"
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
if [ ${#signatures[@]} -eq 0 ]; then
|
||||
echo "::error::Windows build produced no updater signature (.sig) artifact."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Upload Windows bundles
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: windows-x86_64-bundles
|
||||
path: |
|
||||
src-tauri/target/x86_64-pc-windows-msvc/release/bundle/nsis/*.exe
|
||||
src-tauri/target/x86_64-pc-windows-msvc/release/bundle/nsis/*.exe.sig
|
||||
src-tauri/target/x86_64-pc-windows-msvc/release/bundle/nsis/*.zip
|
||||
src-tauri/target/x86_64-pc-windows-msvc/release/bundle/nsis/*.zip.sig
|
||||
src-tauri/target/x86_64-pc-windows-msvc/release/bundle/msi/*.msi
|
||||
src-tauri/target/x86_64-pc-windows-msvc/release/bundle/msi/*.msi.sig
|
||||
src-tauri/target/x86_64-pc-windows-msvc/release/bundle/msi/*.zip
|
||||
src-tauri/target/x86_64-pc-windows-msvc/release/bundle/msi/*.zip.sig
|
||||
if-no-files-found: error
|
||||
retention-days: 1
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
# Phase 3: Publish GitHub Release
|
||||
# No lipo/re-signing — use the per-arch artifacts directly
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
release:
|
||||
name: GitHub Release (alpha)
|
||||
needs: [version, build]
|
||||
needs: [version, build, build-linux, build-windows]
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
@@ -216,9 +556,60 @@ jobs:
|
||||
- name: Download all artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
|
||||
- name: Normalize macOS updater artifact names
|
||||
run: |
|
||||
normalize_updater() {
|
||||
local arch="$1"
|
||||
local normalized_updater="$2"
|
||||
local artifact_dir="updater-${arch}"
|
||||
local updater_file
|
||||
updater_file=$(find "$artifact_dir" -maxdepth 1 -name "*.app.tar.gz" -print -quit)
|
||||
if [ -z "$updater_file" ]; then
|
||||
echo "::error::Missing macOS updater artifact in ${artifact_dir}" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
local sig_file="${updater_file}.sig"
|
||||
if [ ! -f "$sig_file" ]; then
|
||||
echo "::error::Missing macOS updater signature for ${updater_file}" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
local normalized_sig="${normalized_updater}.sig"
|
||||
if [ "$updater_file" != "$normalized_updater" ]; then
|
||||
mv "$updater_file" "$normalized_updater"
|
||||
fi
|
||||
if [ "$sig_file" != "$normalized_sig" ]; then
|
||||
mv "$sig_file" "$normalized_sig"
|
||||
fi
|
||||
}
|
||||
|
||||
normalize_updater aarch64 "updater-aarch64/Tolaria_${{ needs.version.outputs.version }}_macOS_Silicon.app.tar.gz"
|
||||
normalize_updater x86_64 "updater-x86_64/Tolaria_${{ needs.version.outputs.version }}_macOS_Intel.app.tar.gz"
|
||||
|
||||
- name: Generate release notes
|
||||
run: |
|
||||
PREV_TAG=$(git tag --list 'alpha-v*' --sort=-version:refname | head -n 1 || echo "")
|
||||
PREV_TAG=$(python3 <<'PY'
|
||||
import re
|
||||
import subprocess
|
||||
|
||||
current_tag = '${{ needs.version.outputs.tag }}'
|
||||
pattern = re.compile(r'^alpha-v(\d{4})\.(\d{1,2})\.(\d{1,2})-alpha\.(\d+)$')
|
||||
|
||||
output = subprocess.check_output(['git', 'tag', '--list', 'alpha-v*'], text=True).strip()
|
||||
tags = [line for line in output.splitlines() if line and line != current_tag]
|
||||
|
||||
parsed_tags = []
|
||||
for tag in tags:
|
||||
match = pattern.fullmatch(tag)
|
||||
if not match:
|
||||
continue
|
||||
year, month, day, sequence = map(int, match.groups())
|
||||
parsed_tags.append(((year, month, day, sequence), tag))
|
||||
|
||||
print(max(parsed_tags)[1] if parsed_tags else '')
|
||||
PY
|
||||
)
|
||||
if [ -z "$PREV_TAG" ]; then
|
||||
NOTES=$(git log --oneline --no-merges -20)
|
||||
else
|
||||
@@ -232,7 +623,7 @@ jobs:
|
||||
echo "---"
|
||||
echo "**Alpha build — updated on every push to \`main\`**"
|
||||
echo ""
|
||||
echo "**Requires Apple Silicon (M1/M2/M3)**"
|
||||
echo "**Includes macOS (Apple Silicon and Intel), Linux x64, and Windows x64 bundles**"
|
||||
echo ""
|
||||
echo "*Built from \`$(git rev-parse --short HEAD)\` on $(date -u +%Y-%m-%d)*"
|
||||
} > release_notes.md
|
||||
@@ -245,8 +636,38 @@ jobs:
|
||||
REPO_NAME="${REPO#*/}"
|
||||
PAGES_URL="https://refactoringhq.github.io/${REPO_NAME}/"
|
||||
|
||||
ARM_SIG=$(cat updater-aarch64/*.app.tar.gz.sig)
|
||||
ARM_TARBALL=$(ls updater-aarch64/*.app.tar.gz | xargs basename)
|
||||
find_required() {
|
||||
for pattern in "$@"; do
|
||||
set -- $pattern
|
||||
if [ -e "$1" ]; then
|
||||
printf '%s\n' "$1"
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
ARM_SIG_FILE=$(find_required "updater-aarch64/*.app.tar.gz.sig")
|
||||
ARM_UPDATER_FILE="${ARM_SIG_FILE%.sig}"
|
||||
ARM_SIG=$(cat "$ARM_SIG_FILE")
|
||||
ARM_UPDATER=$(basename "$ARM_UPDATER_FILE")
|
||||
|
||||
INTEL_SIG_FILE=$(find_required "updater-x86_64/*.app.tar.gz.sig")
|
||||
INTEL_UPDATER_FILE="${INTEL_SIG_FILE%.sig}"
|
||||
INTEL_SIG=$(cat "$INTEL_SIG_FILE")
|
||||
INTEL_UPDATER=$(basename "$INTEL_UPDATER_FILE")
|
||||
|
||||
LINUX_SIG_FILE=$(find_required "linux-x86_64-bundles/*/*.AppImage.sig" "linux-x86_64-bundles/*/*.AppImage.tar.gz.sig" "linux-x86_64-bundles/*/*.deb.sig" "linux-x86_64-bundles/*.AppImage.sig" "linux-x86_64-bundles/*.AppImage.tar.gz.sig" "linux-x86_64-bundles/*.deb.sig")
|
||||
LINUX_UPDATER_FILE="${LINUX_SIG_FILE%.sig}"
|
||||
LINUX_SIG=$(cat "$LINUX_SIG_FILE")
|
||||
LINUX_UPDATER=$(basename "$LINUX_UPDATER_FILE")
|
||||
LINUX_DOWNLOAD=$(basename "$(find_required "linux-x86_64-bundles/*/*.AppImage" "linux-x86_64-bundles/*/*.deb" "linux-x86_64-bundles/*/*.AppImage.tar.gz" "linux-x86_64-bundles/*.AppImage" "linux-x86_64-bundles/*.deb" "linux-x86_64-bundles/*.AppImage.tar.gz")")
|
||||
|
||||
WINDOWS_SIG_FILE=$(find_required "windows-x86_64-bundles/*/*-setup.exe.sig" "windows-x86_64-bundles/*/*.msi.sig" "windows-x86_64-bundles/*/*.nsis.zip.sig" "windows-x86_64-bundles/*/*.msi.zip.sig" "windows-x86_64-bundles/*-setup.exe.sig" "windows-x86_64-bundles/*.msi.sig" "windows-x86_64-bundles/*.nsis.zip.sig" "windows-x86_64-bundles/*.msi.zip.sig")
|
||||
WINDOWS_UPDATER_FILE="${WINDOWS_SIG_FILE%.sig}"
|
||||
WINDOWS_SIG=$(cat "$WINDOWS_SIG_FILE")
|
||||
WINDOWS_UPDATER=$(basename "$WINDOWS_UPDATER_FILE")
|
||||
WINDOWS_DOWNLOAD=$(basename "$(find_required "windows-x86_64-bundles/*/*-setup.exe" "windows-x86_64-bundles/*/*.msi" "windows-x86_64-bundles/*/*.nsis.zip" "windows-x86_64-bundles/*/*.msi.zip" "windows-x86_64-bundles/*-setup.exe" "windows-x86_64-bundles/*.msi" "windows-x86_64-bundles/*.nsis.zip" "windows-x86_64-bundles/*.msi.zip")")
|
||||
|
||||
cat > alpha-latest.json << EOF
|
||||
{
|
||||
@@ -256,7 +677,23 @@ jobs:
|
||||
"platforms": {
|
||||
"darwin-aarch64": {
|
||||
"signature": "${ARM_SIG}",
|
||||
"url": "https://github.com/${REPO}/releases/download/${TAG}/${ARM_TARBALL}"
|
||||
"url": "https://github.com/${REPO}/releases/download/${TAG}/${ARM_UPDATER}",
|
||||
"download_url": "https://github.com/${REPO}/releases/download/${TAG}/${ARM_UPDATER}"
|
||||
},
|
||||
"darwin-x86_64": {
|
||||
"signature": "${INTEL_SIG}",
|
||||
"url": "https://github.com/${REPO}/releases/download/${TAG}/${INTEL_UPDATER}",
|
||||
"download_url": "https://github.com/${REPO}/releases/download/${TAG}/${INTEL_UPDATER}"
|
||||
},
|
||||
"linux-x86_64": {
|
||||
"signature": "${LINUX_SIG}",
|
||||
"url": "https://github.com/${REPO}/releases/download/${TAG}/${LINUX_UPDATER}",
|
||||
"download_url": "https://github.com/${REPO}/releases/download/${TAG}/${LINUX_DOWNLOAD}"
|
||||
},
|
||||
"windows-x86_64": {
|
||||
"signature": "${WINDOWS_SIG}",
|
||||
"url": "https://github.com/${REPO}/releases/download/${TAG}/${WINDOWS_UPDATER}",
|
||||
"download_url": "https://github.com/${REPO}/releases/download/${TAG}/${WINDOWS_DOWNLOAD}"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -274,6 +711,32 @@ jobs:
|
||||
files: |
|
||||
updater-aarch64/*.app.tar.gz
|
||||
updater-aarch64/*.app.tar.gz.sig
|
||||
updater-x86_64/*.app.tar.gz
|
||||
updater-x86_64/*.app.tar.gz.sig
|
||||
linux-x86_64-bundles/*.deb
|
||||
linux-x86_64-bundles/*.deb.sig
|
||||
linux-x86_64-bundles/*.AppImage
|
||||
linux-x86_64-bundles/*.AppImage.sig
|
||||
linux-x86_64-bundles/*.AppImage.tar.gz
|
||||
linux-x86_64-bundles/*.AppImage.tar.gz.sig
|
||||
linux-x86_64-bundles/*/*.deb
|
||||
linux-x86_64-bundles/*/*.deb.sig
|
||||
linux-x86_64-bundles/*/*.AppImage
|
||||
linux-x86_64-bundles/*/*.AppImage.sig
|
||||
linux-x86_64-bundles/*/*.AppImage.tar.gz
|
||||
linux-x86_64-bundles/*/*.AppImage.tar.gz.sig
|
||||
windows-x86_64-bundles/*.exe
|
||||
windows-x86_64-bundles/*.exe.sig
|
||||
windows-x86_64-bundles/*.msi
|
||||
windows-x86_64-bundles/*.msi.sig
|
||||
windows-x86_64-bundles/*.zip
|
||||
windows-x86_64-bundles/*.zip.sig
|
||||
windows-x86_64-bundles/*/*.exe
|
||||
windows-x86_64-bundles/*/*.exe.sig
|
||||
windows-x86_64-bundles/*/*.msi
|
||||
windows-x86_64-bundles/*/*.msi.sig
|
||||
windows-x86_64-bundles/*/*.zip
|
||||
windows-x86_64-bundles/*/*.zip.sig
|
||||
alpha-latest.json
|
||||
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
@@ -291,67 +754,29 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Bun
|
||||
uses: oven-sh/setup-bun@v2
|
||||
with:
|
||||
bun-version: latest
|
||||
|
||||
- name: Build release history page
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
mkdir -p _site/alpha _site/stable
|
||||
gh api repos/${{ github.repository }}/releases --paginate > _site/releases.json
|
||||
gh api -H "Accept: application/vnd.github.html+json" repos/${{ github.repository }}/releases --paginate > _site/releases.json
|
||||
PAGES_URL="https://refactoringhq.github.io/${GITHUB_REPOSITORY#*/}"
|
||||
|
||||
gh release download --repo ${{ github.repository }} "${{ needs.version.outputs.tag }}" --pattern "alpha-latest.json" --output _site/alpha/latest.json || echo '{}' > _site/alpha/latest.json
|
||||
curl -fsSL "${PAGES_URL}/stable/latest.json" -o _site/stable/latest.json || echo '{}' > _site/stable/latest.json
|
||||
bun scripts/build-release-download-page.ts --latest-json _site/stable/latest.json --releases-json _site/releases.json --output-file _site/stable/download/index.html
|
||||
bun scripts/build-release-history-page.ts --releases-json _site/releases.json --output-file _site/index.html
|
||||
mkdir -p _site/download
|
||||
cp _site/stable/download/index.html _site/download/index.html
|
||||
|
||||
cp _site/alpha/latest.json _site/latest.json
|
||||
cp _site/alpha/latest.json _site/latest-canary.json
|
||||
|
||||
cat > _site/index.html << 'HTMLEOF'
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Tolaria — Release History</title>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; background: #F7F6F3; color: #37352F; line-height: 1.6; padding: 2rem; max-width: 720px; margin: 0 auto; }
|
||||
h1 { font-size: 1.75rem; font-weight: 600; margin-bottom: 0.5rem; }
|
||||
.subtitle { color: #787774; margin-bottom: 2rem; }
|
||||
.release { background: #fff; border: 1px solid #E9E9E7; border-radius: 8px; padding: 1.25rem 1.5rem; margin-bottom: 1rem; }
|
||||
.release h2 { font-size: 1.125rem; font-weight: 600; margin-bottom: 0.25rem; }
|
||||
.release .meta { font-size: 0.8125rem; color: #787774; margin-bottom: 0.75rem; }
|
||||
.release .body { font-size: 0.875rem; white-space: pre-wrap; }
|
||||
.release .downloads { margin-top: 0.75rem; display: flex; gap: 0.5rem; flex-wrap: wrap; }
|
||||
.release .downloads a { display: inline-block; padding: 0.375rem 0.75rem; background: #155DFF; color: #fff; border-radius: 6px; text-decoration: none; font-size: 0.8125rem; font-weight: 500; }
|
||||
.release .downloads a:hover { background: #1248CC; }
|
||||
.alpha { border-left: 3px solid #f59e0b; }
|
||||
.empty { color: #787774; text-align: center; padding: 3rem; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Tolaria Release History</h1>
|
||||
<p class="subtitle">Alpha builds update on every push to main. Stable builds appear when a stable-vYYYY.M.D tag is promoted.</p>
|
||||
<div id="releases"></div>
|
||||
<script>
|
||||
fetch('releases.json').then(r=>r.json()).then(releases=>{
|
||||
const el=document.getElementById('releases');
|
||||
if(!releases.length){el.innerHTML='<p class="empty">No releases yet.</p>';return;}
|
||||
releases.forEach(r=>{
|
||||
const date=new Date(r.published_at).toLocaleDateString('en-US',{year:'numeric',month:'long',day:'numeric'});
|
||||
const dmgs=(r.assets||[]).filter(a=>a.name.endsWith('.dmg'));
|
||||
const links=dmgs.map(a=>'<a href="'+a.browser_download_url+'">'+a.name+'</a>').join('');
|
||||
const body=(r.body||'').replace(/</g,'<').replace(/>/g,'>');
|
||||
const div=document.createElement('div');
|
||||
div.className='release'+(r.prerelease?' alpha':'');
|
||||
div.innerHTML='<h2>'+(r.name||r.tag_name)+'</h2><div class="meta">'+date+' · '+r.tag_name+(r.prerelease?' · <strong>Alpha</strong>':'')+'</div><div class="body">'+body+'</div>'+(links?'<div class="downloads">'+links+'</div>':'');
|
||||
el.appendChild(div);
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
HTMLEOF
|
||||
|
||||
- name: Deploy to GitHub Pages
|
||||
uses: peaceiris/actions-gh-pages@v4
|
||||
with:
|
||||
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -32,6 +32,7 @@ dist-ssr
|
||||
|
||||
# Demo vault and helper scripts
|
||||
demo-vault/
|
||||
generated-fixtures/
|
||||
select_demo_notes*.py
|
||||
final_selection.py
|
||||
|
||||
@@ -69,5 +70,6 @@ CODE-HEALTH-REPORT.md
|
||||
*.key.pub
|
||||
|
||||
# Local environment variables (never commit)
|
||||
.env
|
||||
.env.local
|
||||
.env.*.local
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# AGENTS.md — Tolaria App
|
||||
|
||||
> Quick links: [Project Spec](docs/PROJECT-SPEC.md) · [Architecture](docs/ARCHITECTURE.md) · [Abstractions](docs/ABSTRACTIONS.md) · [Wireframes](ui-design.pen)
|
||||
> Quick links: [Architecture](docs/ARCHITECTURE.md) · [Abstractions](docs/ABSTRACTIONS.md) · [Wireframes](ui-design.pen)
|
||||
|
||||
---
|
||||
|
||||
|
||||
49
CONTRIBUTING.md
Normal file
49
CONTRIBUTING.md
Normal file
@@ -0,0 +1,49 @@
|
||||
# Contributing to Tolaria
|
||||
|
||||
Thanks for being here! Tolaria is still early, and every bug report, idea, and contribution genuinely helps shape the app.
|
||||
|
||||
## 🗳️ Where to share what
|
||||
|
||||
To keep things clean:
|
||||
|
||||
- 🐛 Bugs → GitHub Issues
|
||||
- 💡 Feature requests / ideas → Canny • <https://tolaria.canny.io/>
|
||||
|
||||
If you have a feature idea, please check Canny first and upvote it if it already exists.
|
||||
|
||||
## 📥 Pull requests are welcome
|
||||
|
||||
PRs are very welcome.
|
||||
|
||||
A few things to keep in mind before opening one:
|
||||
|
||||
- Bug fixes are always great
|
||||
- Small improvements are great too
|
||||
- For bigger features, please check Canny first before building
|
||||
- Try to avoid things that are already marked **in progress**
|
||||
- Requests marked **planned** are usually great contribution targets
|
||||
- Keep PRs small, focused, and easy to review
|
||||
- Include a short explanation of the problem and your solution
|
||||
- Follow the dev process described in Tolaria’s `AGENTS.md` (tests, code health, etc.)
|
||||
- Avoid bundling unrelated refactors into the same PR
|
||||
|
||||
If you want to contribute a feature, the best place to start is here: <https://tolaria.canny.io/>
|
||||
|
||||
## 📋 What makes a good bug report
|
||||
|
||||
If you open a bug report on GitHub, it really helps to include:
|
||||
|
||||
- your Tolaria version
|
||||
- your OS version
|
||||
- steps to reproduce
|
||||
- what you expected to happen
|
||||
- what actually happened
|
||||
- screenshots or screen recordings if useful
|
||||
|
||||
The clearer the report, the easier it is for us to reproduce and fix it.
|
||||
|
||||
## 🙏 Thank you
|
||||
|
||||
Tolaria is getting better because people care enough to try it, report what’s broken, suggest what’s missing, and contribute improvements.
|
||||
|
||||
That means a lot. Thanks for helping build it.
|
||||
Binary file not shown.
151
README.md
151
README.md
@@ -1,101 +1,100 @@
|
||||
# Tolaria App
|
||||
 [](https://github.com/refactoringhq/tolaria/actions/workflows/ci.yml) [](https://github.com/refactoringhq/tolaria/actions/workflows/release.yml) [](https://codecov.io/gh/refactoringhq/tolaria) [](https://codescene.io/projects/76865)
|
||||
|
||||
Personal knowledge and life management desktop app built with Tauri v2 + React + TypeScript + BlockNote.
|
||||
# 💧 Tolaria
|
||||
|
||||
## Documentation
|
||||
Tolaria is a desktop app for Mac and Linux for managing **markdown knowledge bases**. People use it for a variety of use cases:
|
||||
|
||||
- 📐 [ARCHITECTURE.md](docs/ARCHITECTURE.md) — System design, tech stack, data flow
|
||||
- 🧩 [ABSTRACTIONS.md](docs/ABSTRACTIONS.md) — Core abstractions and models
|
||||
- 🚀 [GETTING-STARTED.md](docs/GETTING-STARTED.md) — How to navigate the codebase
|
||||
- 🎨 [THEMING.md](docs/THEMING.md) — Theme system and customization
|
||||
* Operate second brains and personal knowledge
|
||||
* Organize company docs as context for AI
|
||||
* Store OpenClaw/assistants memory and procedures
|
||||
|
||||
## Quick Start
|
||||
Personally, I use it to **run my life** (hey 👋 [Luca here](http://x.com/lucaronin)). I have a massive workspace of 10,000+ notes, which are the result of my [Refactoring](https://refactoring.fm/) work + a ton of personal journaling and *second braining*.
|
||||
|
||||
<img width="1000" height="656" alt="1776506856823-CleanShot_2026-04-18_at_12 06 57_2x" src="https://github.com/user-attachments/assets/8aeafb0a-b236-43c2-a083-ec111f903c38" />
|
||||
|
||||
## Walkthroughs
|
||||
|
||||
You can find some Loom walkthroughs below — they are short and to the point:
|
||||
- [How I Organize My Own Tolaria Workspace](https://www.loom.com/share/bb3aaffa238b4be0bd62e4464bca2528)
|
||||
- [My Inbox Workflow](https://www.loom.com/share/dffda263317b4fa8b47b59cdf9330571)
|
||||
- [How I Save Web Resources to Tolaria](https://www.loom.com/share/8a3c1776f801402ebbf4d7b0f31e9882)
|
||||
|
||||
## Principles
|
||||
|
||||
- 📑 **Files-first** — Your notes are plain markdown files. They're portable, work with any editor, and require no export step. Your data belongs to you, not to any app.
|
||||
- 🔌 **Git-first** — Every vault is a git repository. You get full version history, the ability to use any git remote, and zero dependency on Tolaria servers.
|
||||
- 🛜 **Offline-first, zero lock-in** — No accounts, no subscriptions, no cloud dependencies. Your vault works completely offline and always will. If you stop using Tolaria, you lose nothing.
|
||||
- 🔬 **Open source** — Tolaria is free and open source. I built this for [myself](https://x.com/lucaronin) and for sharing it with others.
|
||||
- 📋 **Standards-based** — Notes are markdown files with YAML frontmatter. No proprietary formats, no locked-in data. Everything works with standard tools if you decide to move away from Tolaria.
|
||||
- 🔍 **Types as lenses, not schemas** — Types in Tolaria are navigation aids, not enforcement mechanisms. There's no required fields, no validation, just helpful categories for finding notes.
|
||||
- 🪄**AI-first but not AI-only** — A vault of files works very well with AI agents, but you are free to use whatever you want. We support Claude Code and Codex CLI (for now), but you can edit the vault with any AI you want. We provide an AGENTS file for your agents to figure out.
|
||||
- ⌨️ **Keyboard-first** — Tolaria is designed for power-users who want to use keyboard as much as possible. A lot of how we designed the Editor and the Command Palette is based on this.
|
||||
- 💪 **Built from real use** — Tolaria was created for manage my personal vault of 10,000+ notes, and I use it every day. Every feature exists because it solved a real problem.
|
||||
|
||||
## Getting started
|
||||
|
||||
Download the [latest release here](https://github.com/refactoringhq/tolaria/releases/latest/download/Tolaria.app.tar.gz).
|
||||
|
||||
When you open Tolaria for the first time you get the chance of cloning the [getting started vault](https://github.com/refactoringhq/tolaria-getting-started) — which gives you a walkthrough of the whole app.
|
||||
|
||||
## Open source and local setup
|
||||
|
||||
Tolaria is open source and built with Tauri, React, and TypeScript. If you want to run or contribute to the app locally, here is [how to get started](https://github.com/refactoringhq/tolaria/blob/main/docs/GETTING-STARTED.md). You can also find the gist below 👇
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- Node.js 20+
|
||||
- pnpm 8+
|
||||
- Rust (latest stable)
|
||||
- macOS (for development)
|
||||
- Rust stable
|
||||
- macOS or Linux for development
|
||||
|
||||
### Setup
|
||||
#### Linux system dependencies
|
||||
|
||||
Tauri 2 on Linux requires WebKit2GTK 4.1 and GTK 3:
|
||||
|
||||
- Arch / Manjaro:
|
||||
```bash
|
||||
sudo pacman -S --needed webkit2gtk-4.1 base-devel curl wget file openssl \
|
||||
appmenu-gtk-module libappindicator-gtk3 librsvg
|
||||
```
|
||||
- Debian / Ubuntu (22.04+):
|
||||
```bash
|
||||
sudo apt install libwebkit2gtk-4.1-dev build-essential curl wget file \
|
||||
libxdo-dev libssl-dev libayatana-appindicator3-dev librsvg2-dev \
|
||||
libsoup-3.0-dev patchelf
|
||||
```
|
||||
- Fedora 38+:
|
||||
```bash
|
||||
sudo dnf install webkit2gtk4.1-devel openssl-devel curl wget file \
|
||||
libappindicator-gtk3-devel librsvg2-devel
|
||||
```
|
||||
|
||||
The bundled MCP server still spawns the system `node` binary at runtime on Linux, so install Node from your distro package manager if you want the external AI tooling flow.
|
||||
|
||||
### Quick start
|
||||
|
||||
```bash
|
||||
# Install dependencies
|
||||
pnpm install
|
||||
|
||||
# Run dev server
|
||||
pnpm dev
|
||||
```
|
||||
|
||||
# Open in browser (mock mode)
|
||||
open http://localhost:5173
|
||||
Open `http://localhost:5173` for the browser-based mock mode, or run the native desktop app with:
|
||||
|
||||
# Or run in Tauri
|
||||
```bash
|
||||
pnpm tauri dev
|
||||
```
|
||||
|
||||
### Testing
|
||||
## Tech Docs
|
||||
|
||||
```bash
|
||||
# Frontend tests
|
||||
pnpm test
|
||||
- 📐 [ARCHITECTURE.md](docs/ARCHITECTURE.md) — System design, tech stack, data flow
|
||||
- 🧩 [ABSTRACTIONS.md](docs/ABSTRACTIONS.md) — Core abstractions and models
|
||||
- 🚀 [GETTING-STARTED.md](docs/GETTING-STARTED.md) — How to navigate the codebase
|
||||
- 📚 [ADRs](docs/adr) — Architecture Decision Records
|
||||
|
||||
# Backend tests
|
||||
cargo test
|
||||
## Security
|
||||
|
||||
# Coverage
|
||||
pnpm test:coverage
|
||||
|
||||
# E2E tests
|
||||
pnpm test:e2e
|
||||
```
|
||||
|
||||
### Code Quality
|
||||
|
||||
```bash
|
||||
# Lint
|
||||
pnpm lint
|
||||
|
||||
# Rust checks
|
||||
cargo clippy
|
||||
cargo fmt --check
|
||||
|
||||
# CodeScene (via Claude Code)
|
||||
claude 'Check code health with CodeScene MCP'
|
||||
```
|
||||
|
||||
## Development Workflow
|
||||
|
||||
See [AGENTS.md](AGENTS.md) for coding guidelines and workflow. [CLAUDE.md](CLAUDE.md) remains as a compatibility shim for Claude Code.
|
||||
|
||||
**Key principles:**
|
||||
- Small, atomic commits
|
||||
- Test as you go
|
||||
- Visual verification mandatory
|
||||
- Documentation updated with code changes
|
||||
|
||||
## CI/CD
|
||||
|
||||
GitHub Actions runs on every push to `main`:
|
||||
- ✅ Tests (frontend + Rust)
|
||||
- 📊 Coverage (70% threshold)
|
||||
- 🎨 Lint & format
|
||||
- ⚠️ Documentation check
|
||||
|
||||
See [.github/SETUP.md](.github/SETUP.md) for CI/CD configuration.
|
||||
|
||||
## Git Hooks
|
||||
|
||||
Husky installs the git hooks from `.husky/` during `pnpm install`. The repo enforces `main`-only commits and pushes; see [.github/HOOKS.md](.github/HOOKS.md) for details.
|
||||
If you believe you have found a security issue, please report it privately as described in [SECURITY.md](./SECURITY.md).
|
||||
|
||||
## License
|
||||
|
||||
Tolaria is licensed under the GNU Affero General Public License, version 3 or any later version (AGPL-3.0-or-later).
|
||||
|
||||
See [LICENSE](LICENSE) for the full license text.
|
||||
|
||||
## Trademarks
|
||||
|
||||
The Tolaria name and logo are not licensed under the AGPL.
|
||||
|
||||
See [trademarks.md](trademarks.md) for the trademark policy.
|
||||
Tolaria is licensed under AGPL-3.0-or-later. The Tolaria name and logo remain covered by the project’s trademark policy.
|
||||
|
||||
55
SECURITY.md
Normal file
55
SECURITY.md
Normal file
@@ -0,0 +1,55 @@
|
||||
# Security Policy
|
||||
|
||||
Thanks for helping keep Tolaria safe.
|
||||
|
||||
If you believe you have found a security vulnerability, **please do not open a public GitHub issue**. Report it privately instead.
|
||||
|
||||
## Supported versions
|
||||
|
||||
We currently support security fixes for:
|
||||
|
||||
| Version | Supported |
|
||||
| --- | --- |
|
||||
| Latest stable release | ✅ |
|
||||
| `main` branch | Best effort |
|
||||
| Older releases / prereleases | ❌ |
|
||||
|
||||
## Reporting a vulnerability
|
||||
|
||||
Please email **luca@refactoring.club** with the subject line **`[Tolaria Security]`**.
|
||||
|
||||
Include as much of the following as you can:
|
||||
|
||||
- a short description of the issue
|
||||
- reproduction steps or a proof of concept
|
||||
- affected version / commit, if known
|
||||
- impact assessment
|
||||
- any suggested mitigation
|
||||
|
||||
If the issue involves sensitive user data, credentials, or a working exploit, keep the report private and do not post details publicly.
|
||||
|
||||
## What to expect
|
||||
|
||||
We will try to:
|
||||
|
||||
- acknowledge receipt within a few business days
|
||||
- reproduce and assess the report
|
||||
- work on a fix or mitigation if the issue is valid
|
||||
- coordinate public disclosure after users have had a reasonable chance to update
|
||||
|
||||
## Disclosure guidelines
|
||||
|
||||
Please give us a reasonable amount of time to investigate and ship a fix before publishing details.
|
||||
|
||||
We appreciate responsible disclosure and good-faith research.
|
||||
|
||||
## Out of scope
|
||||
|
||||
The following are generally out of scope unless they demonstrate a real security impact:
|
||||
|
||||
- missing best-practice headers or hardening with no practical exploit
|
||||
- self-XSS or editor behavior that requires unrealistic user actions
|
||||
- reports that only affect unsupported old builds
|
||||
- purely theoretical issues with no plausible attack path
|
||||
|
||||
If you are unsure whether something qualifies, please still report it privately.
|
||||
55
demo-vault-v2/.fixture-manifest.json
Normal file
55
demo-vault-v2/.fixture-manifest.json
Normal file
@@ -0,0 +1,55 @@
|
||||
{
|
||||
"name": "Tolaria QA fixture",
|
||||
"purpose": "Curated local vault for native QA and developer flows. This is not the public Getting Started starter vault.",
|
||||
"large_fixture": {
|
||||
"generator": "python3 scripts/generate_demo_vault.py",
|
||||
"default_output": "generated-fixtures/demo-vault-large"
|
||||
},
|
||||
"scenarios": [
|
||||
{
|
||||
"id": "exact-match-search",
|
||||
"reason": "Quick Open should rank the exact title 'Writing' above prefix matches.",
|
||||
"files": [
|
||||
"topic-writing.md",
|
||||
"writing-for-clarity-vs-writing-for-credit.md",
|
||||
"writing-weekly-rhythm.md"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "relationship-rendering",
|
||||
"reason": "Relationship keys should render in the inspector instead of as plain properties.",
|
||||
"files": [
|
||||
"responsibility-sponsorships.md",
|
||||
"measure-sponsorship-mrr.md",
|
||||
"measure-close-rate.md",
|
||||
"procedure-quarterly-sponsor-outreach.md",
|
||||
"procedure-sponsor-onboarding.md",
|
||||
"24q4-laputa-start.md",
|
||||
"24q4.md",
|
||||
"person-luca-rossi.md"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "project-navigation",
|
||||
"reason": "Projects, quarters, and a saved view give keyboard QA a compact but representative browsing path.",
|
||||
"files": [
|
||||
"24q4.md",
|
||||
"25q1.md",
|
||||
"25q2.md",
|
||||
"24q4-laputa-start.md",
|
||||
"25q1-laputa-v1.md",
|
||||
"25q2-laputa-v2.md",
|
||||
"views/active-projects.yml"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "attachment-rendering",
|
||||
"reason": "A note with a real binary attachment keeps image/block QA anchored to the fixture.",
|
||||
"files": [
|
||||
"laputa-qa-reference.md",
|
||||
"attachments/laputa-reference.png"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
17
demo-vault-v2/.gitignore
vendored
17
demo-vault-v2/.gitignore
vendored
@@ -1,16 +1,5 @@
|
||||
# Laputa app files (machine-specific, never commit)
|
||||
.laputa/settings.json
|
||||
|
||||
# macOS
|
||||
.git/
|
||||
.laputa/
|
||||
.laputa-index.json
|
||||
.DS_Store
|
||||
.AppleDouble
|
||||
.LSOverride
|
||||
|
||||
# Thumbnails
|
||||
._*
|
||||
|
||||
# Editors
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
*.swo
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
{
|
||||
"theme": null
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
---
|
||||
aliases: ["January 2024"]
|
||||
Is A: Month
|
||||
Created at: "2024-01-28"
|
||||
Belongs to: "[[24q1]]"
|
||||
Rating: "😄"
|
||||
---
|
||||
# January 2024
|
||||
|
||||
## Focus
|
||||
|
||||
- Set up [[24q1-launch-sponsorship-packages]] — research pricing, structure tiers, draft sponsor-facing materials
|
||||
- Begin planning [[24q1-podcast-season-1]] — outline first 6 episode topics, research recording setup
|
||||
- Map out cycling calendar for the year ([[24q1-plan-cycling-season]])
|
||||
|
||||
## Highlights
|
||||
|
||||
- Finalized the three-tier sponsorship model (Gold, Silver, Bronze) with [[person-matteo-cellini]] — spent two full days on pricing research and competitive analysis
|
||||
- Ordered podcast equipment: Shure SM7B, Focusrite Scarlett, pop filter — the home studio is starting to take shape
|
||||
- Newsletter hit 35.4k subscribers, steady organic growth from December's end-of-year content
|
||||
- Read "The Hard Thing About Hard Things" and "Measure What Matters" — both immediately applicable to how I think about Refactoring's goals
|
||||
- Registered for Nove Colli (May) and submitted lottery application for Maratona dles Dolomites (July)
|
||||
- Started base training: 380 km this month, mostly zone 2 rides on the trainer
|
||||
|
||||
## Reflections
|
||||
|
||||
January always feels like a slow month, but this one was quietly productive. The sponsorship package work was the kind of unglamorous, strategic task that doesn't produce visible output but sets up everything that follows. [[person-matteo-cellini]] and I went back and forth on pricing for days — we kept second-guessing whether Gold at EUR 2.8k/issue was too high. In the end, we decided to launch at that price and see what happens. Worst case, we adjust.
|
||||
|
||||
The podcast planning was fun but nerve-wracking. I've been talking about launching a podcast for over a year, and now it's actually happening. The equipment is here, the topics are outlined. No more excuses. February is recording month.
|
||||
@@ -1,29 +0,0 @@
|
||||
---
|
||||
aliases: ["February 2024"]
|
||||
Is A: Month
|
||||
Created at: "2024-02-28"
|
||||
Belongs to: "[[24q1]]"
|
||||
Rating: "🤩"
|
||||
---
|
||||
# February 2024
|
||||
|
||||
## Focus
|
||||
|
||||
- Record and edit first batch of podcast episodes ([[24q1-podcast-season-1]])
|
||||
- Launch [[24q1-launch-sponsorship-packages]] to existing sponsors and new prospects
|
||||
- Start [[24q1-redesign-newsletter-template]] with improved mobile layout
|
||||
|
||||
## Highlights
|
||||
|
||||
- Recorded episodes 1-4 of [[24q1-podcast-season-1]] in a single weekend marathon session — exhausting but exhilarating
|
||||
- Published Episode 1 ("Why Engineering Managers Fail") — 600 downloads in the first 48 hours, way above my modest expectations
|
||||
- [[24q1-launch-sponsorship-packages]] went live — sent the new pricing deck to 12 existing sponsors and 20 cold prospects
|
||||
- [[person-matteo-cellini]] closed the first Gold sponsor deal within a week of launch — a DevOps platform company, EUR 2.8k/issue for 4 issues
|
||||
- Newsletter grew to 36.1k subscribers — the podcast announcement email had a 52% open rate
|
||||
- First draft of [[24q1-redesign-newsletter-template]] ready — cleaner header, better CTA placement, responsive images
|
||||
|
||||
## Reflections
|
||||
|
||||
This was the month everything started to feel real. Hearing my own voice on the podcast for the first time was deeply uncomfortable — I hated it, edited obsessively, almost scrapped the whole thing. But then the downloads came in, and the feedback was overwhelmingly positive. People said things like "it feels like you're explaining this to me over coffee." That's exactly the vibe I was going for. Maybe my voice isn't so bad after all.
|
||||
|
||||
The sponsorship launch going well this quickly was a confidence boost. [[person-matteo-cellini]] deserves the credit — he positioned the packages perfectly and his follow-up game is relentless. Having a second Gold sponsor in the pipeline already makes the Q2 revenue forecast look strong. February was a month where preparation met opportunity. Best month in a while.
|
||||
@@ -1,29 +0,0 @@
|
||||
---
|
||||
aliases: ["March 2024"]
|
||||
Is A: Month
|
||||
Created at: "2024-03-28"
|
||||
Belongs to: "[[24q1]]"
|
||||
Rating: "😄"
|
||||
---
|
||||
# March 2024
|
||||
|
||||
## Focus
|
||||
|
||||
- Finalize [[24q1-redesign-newsletter-template]] and ship to production
|
||||
- Continue podcast momentum with episodes 3-4 release
|
||||
- Set up [[24q1-set-investing-framework]] — automate monthly contributions
|
||||
|
||||
## Highlights
|
||||
|
||||
- Shipped [[24q1-redesign-newsletter-template]] — [[measure-open-rate]] improved by 1.5pp in the first two weeks, click-through rate also up
|
||||
- Released episodes 3 and 4 of [[24q1-podcast-season-1]] — Episode 3 on "Technical Debt as a Strategic Tool" got shared widely on LinkedIn
|
||||
- Set up [[24q1-set-investing-framework]]: automated monthly DCA into VWCE (global ETF), topped up emergency fund to 6 months of expenses
|
||||
- Newsletter crossed 37k subscribers — strong organic growth from podcast cross-promotion
|
||||
- Got accepted for Maratona dles Dolomites via lottery — both gran fondos now confirmed for [[2024-complete-two-gran-fondos]]
|
||||
- Cycling: 420 km this month, first outdoor rides of the season — legs feel good after months on the trainer
|
||||
|
||||
## Reflections
|
||||
|
||||
March wrapped up Q1 nicely. The newsletter template redesign was one of those things that seems small but makes a real difference — the new layout just looks more professional, and the numbers back it up. Sometimes the boring improvements are the most impactful.
|
||||
|
||||
Getting into Maratona via lottery was a huge relief. I'd been anxious about it for weeks. Now both races are locked in and the training plan has clear milestones. The investing framework is the kind of "set it and forget it" system I love — one less thing to think about each month. [[24q1]] was a strong quarter overall. Time to execute in Q2.
|
||||
@@ -1,30 +0,0 @@
|
||||
---
|
||||
aliases: ["April 2024"]
|
||||
Is A: Month
|
||||
Created at: "2024-04-28"
|
||||
Belongs to: "[[24q2]]"
|
||||
Rating: "😄"
|
||||
---
|
||||
# April 2024
|
||||
|
||||
## Focus
|
||||
|
||||
- Kick off [[24q2-hire-editor]] search — post the role, screen candidates, run test edits
|
||||
- Start [[24q2-10-pillar-articles]] strategy — identify topics, outline first 3 articles
|
||||
- Begin building [[24q2-sponsor-crm]] in Notion
|
||||
|
||||
## Highlights
|
||||
|
||||
- Posted the freelance editor role on LinkedIn and two newsletter job boards — received 45 applications in two weeks
|
||||
- Shortlisted 5 editor candidates and ran paid test edits on a sample newsletter issue — two stood out clearly
|
||||
- Outlined the first 3 [[24q2-10-pillar-articles]]: "Staff Engineer vs. Manager," "How to Prioritize Technical Debt," and "The 1:1 Framework That Actually Works"
|
||||
- Started building [[24q2-sponsor-crm]] in Notion — pipeline view, deal stages, revenue tracking
|
||||
- [[person-paco-furiani]] took over invoicing — freed up ~3 hours/month of admin work
|
||||
- Newsletter at 38.5k subscribers — steady growth, no spikes
|
||||
- Cycling: ramping up volume — 480 km this month, including a 120 km long ride in Tuscany
|
||||
|
||||
## Reflections
|
||||
|
||||
A transitional month. Not flashy, but the groundwork being laid is important. The editor hire is going to be transformative if I get it right — spending Sunday evenings editing is my least favorite part of the workflow. The test edits revealed that most applicants can fix grammar but few can preserve voice. The two finalists both "got" the Refactoring tone, which is rare.
|
||||
|
||||
The pillar articles strategy feels right. Instead of grinding out a new topic every week, investing in fewer, deeper pieces that can rank on search and compound over time. It's a bet on long-term value over short-term output. [[person-paco-furiani]] quietly picking up the invoicing was a small moment that mattered more than it looked — the team is starting to function as a team, not just a collection of helpers.
|
||||
@@ -1,30 +0,0 @@
|
||||
---
|
||||
aliases: ["May 2024"]
|
||||
Is A: Month
|
||||
Created at: "2024-05-28"
|
||||
Belongs to: "[[24q2]]"
|
||||
Rating: "😐"
|
||||
---
|
||||
# May 2024
|
||||
|
||||
## Focus
|
||||
|
||||
- Finalize [[24q2-hire-editor]] — make the offer, onboard
|
||||
- Publish first batch of [[24q2-10-pillar-articles]]
|
||||
- Race Nove Colli ([[24q2-spring-gran-fondo]])
|
||||
|
||||
## Highlights
|
||||
|
||||
- Hired the editor — she starts full-time in June, doing first-pass editing on every newsletter issue
|
||||
- Published "Staff Engineer vs. Manager" — it went semi-viral on LinkedIn, drove 800+ new subscribers in a single week
|
||||
- Completed Nove Colli ([[24q2-spring-gran-fondo]]) in 7h42m — finished but bonked hard on the fourth climb due to heat
|
||||
- [[24q2-build-podcast-landing-page]] kicked off — basic wireframes done, episodes 1-6 now need a proper home
|
||||
- Newsletter hit 40.5k subscribers — the pillar article spike was significant
|
||||
|
||||
## Reflections
|
||||
|
||||
Mixed feelings this month. The editor hire is great news — having someone who can handle the first pass means I can focus on the ideas rather than the polish. But Nove Colli was humbling. The heat in Romagna was 34 degrees, and I didn't manage my nutrition well enough. Bonking on the fourth climb — legs completely empty, seeing spots — was scary. I sat on the side of the road for 10 minutes eating everything in my pockets before I could continue.
|
||||
|
||||
I finished, and that matters. But the time was disappointing. Lesson learned: nutrition strategy needs to be as planned as the training itself. The Maratona in July will be at altitude, which adds another variable. Need to take fueling much more seriously.
|
||||
|
||||
The "Staff Engineer vs. Manager" article reminded me why I do this. The number of engineers who DMed me saying "I needed to read this three years ago" — that's the impact I care about. More than subscriber counts.
|
||||
@@ -1,32 +0,0 @@
|
||||
---
|
||||
aliases: ["June 2024"]
|
||||
Is A: Month
|
||||
Created at: "2024-06-28"
|
||||
Belongs to: "[[24q2]]"
|
||||
Rating: "🤩"
|
||||
---
|
||||
# June 2024
|
||||
|
||||
## Focus
|
||||
|
||||
- Ship [[24q2-build-podcast-landing-page]] with full episode archive
|
||||
- Complete [[24q2-sponsor-crm]] and migrate all sponsor data
|
||||
- Ramp up training for Maratona dles Dolomites in July
|
||||
|
||||
## Highlights
|
||||
|
||||
- Launched [[24q2-build-podcast-landing-page]] — clean design, Apple/Spotify links, full show notes for all 8 episodes — podcast downloads crossed 5k/month
|
||||
- [[24q2-sponsor-crm]] completed and live — all pipeline data migrated from scattered spreadsheets, [[person-matteo-cellini]] now tracks everything in one place
|
||||
- Published 3 more [[24q2-10-pillar-articles]] including "How to Prioritize Technical Debt" (32k views)
|
||||
- Editor fully onboarded and in rhythm — first-pass drafts arriving every Monday morning, quality is excellent
|
||||
- Newsletter hit 42k subscribers — right on pace for [[2024-reach-50k-subscribers]]
|
||||
- [[measure-sponsorship-mrr]] reached EUR 8.2k — strong Q2 performance
|
||||
- Cycling: biggest training month yet — 620 km, including two back-to-back long rides simulating Maratona's profile
|
||||
|
||||
## Reflections
|
||||
|
||||
June was one of those months where everything clicks. The podcast landing page looks professional and is already converting visitors to subscribers. The sponsor CRM might be the most impactful internal tool we've built — no more "where's that sponsor contract?" emails. [[person-matteo-cellini]] went from managing sponsors in his head to having a proper pipeline view.
|
||||
|
||||
The editor hire is already paying off. She catches things I'd miss after staring at my own words for hours. The Monday morning routine of reviewing a clean draft instead of starting from a rough mess is a genuine quality-of-life improvement. My writing quality is actually better because I can focus on substance rather than mechanics.
|
||||
|
||||
Training is going well. The two back-to-back long rides (140 km + 120 km over a weekend) left me wrecked but confident. If I can do that in Tuscany's hills, the Dolomites are within reach. Bring on July.
|
||||
@@ -1,29 +0,0 @@
|
||||
---
|
||||
aliases: ["July 2024"]
|
||||
Is A: Month
|
||||
Created at: "2024-07-28"
|
||||
Belongs to: "[[24q3]]"
|
||||
Rating: "😄"
|
||||
---
|
||||
# July 2024
|
||||
|
||||
## Focus
|
||||
|
||||
- Race Maratona dles Dolomites — the big one for [[2024-complete-two-gran-fondos]]
|
||||
- Begin [[24q3-premium-tier]] planning — pricing, content strategy, platform selection
|
||||
- Launch [[24q3-summer-reading-sprint]] — target 8 books in July-August
|
||||
|
||||
## Highlights
|
||||
|
||||
- Completed Maratona dles Dolomites in 9h15m — Passo Giau at dawn was transcendent, Fedaia was brutal, but finished strong — [[2024-complete-two-gran-fondos]] achieved
|
||||
- Started planning [[24q3-premium-tier]]: researched Substack paid, Ghost memberships, and Stripe checkout — leaning toward custom Stripe integration for flexibility
|
||||
- Read "An Elegant Puzzle" and "Team Topologies" — both excellent, immediately useful for newsletter content
|
||||
- Newsletter at 43.5k subscribers — summer slowdown typical but growth still positive
|
||||
- Began outlining [[24q3-codemotion-talk]] abstract — submitted "Scaling Engineering Culture" as the proposed topic
|
||||
- [[measure-podcast-downloads]] hit 6.5k/month — organic growth without new episodes (between seasons)
|
||||
|
||||
## Reflections
|
||||
|
||||
Maratona was the cycling highlight of the year, maybe of my life so far. 138 km through the Dolomites, 4,200m of climbing, starting in the dark at 6am in Corvara. The Sella and Gardena passes were hard, Campolongo was deceptively steep, but Giau — Giau was magical. The sunrise hitting the peaks as I climbed the final switchbacks, the silence broken only by breathing and chain noise. I didn't care about my time. I was just there.
|
||||
|
||||
The post-race week was recovery — both physical and mental. After months of structured training and race anxiety, there's a strange emptiness when the goal is achieved. I channeled that into reading and planning the premium tier. Pricing is the hardest part: too cheap and it devalues the content, too expensive and the audience is too small to sustain it. Need to think about this more in August.
|
||||
@@ -1,29 +0,0 @@
|
||||
---
|
||||
aliases: ["August 2024"]
|
||||
Is A: Month
|
||||
Created at: "2024-08-28"
|
||||
Belongs to: "[[24q3]]"
|
||||
Rating: "🤩"
|
||||
---
|
||||
# August 2024
|
||||
|
||||
## Focus
|
||||
|
||||
- Build and launch [[24q3-premium-tier]] — set up payments, create first premium content
|
||||
- Continue [[24q3-summer-reading-sprint]] — deep reading weeks with lighter work schedule
|
||||
- Record first episodes of [[24q3-podcast-season-2]]
|
||||
|
||||
## Highlights
|
||||
|
||||
- Launched [[24q3-premium-tier]] at EUR 8/month — 120 subscribers in the first week, bumped to EUR 12/month after seeing strong demand
|
||||
- Premium content: published first deep-dive case study ("How Stripe Scales Engineering Teams") and announced monthly AMA calls
|
||||
- Finished [[24q3-summer-reading-sprint]]: 8 books total across July-August, including "The Ride of a Lifetime" and "Thinking in Systems"
|
||||
- Recorded episodes 13-16 of [[24q3-podcast-season-2]] — new guest interview format with [[person-sara-ricci]] coordinating guests
|
||||
- Newsletter at 44.8k — August is always slow, but premium launch generated a subscriber bump
|
||||
- [[person-matteo-cellini]] began outreach for [[24q3-new-sponsor-verticals]] — targeting cloud and DevOps companies
|
||||
|
||||
## Reflections
|
||||
|
||||
August is usually my favorite work month — lighter pace, longer mornings, room to think. This year it was productive in a different way. The premium tier launch felt risky: asking people to pay for something they'd been getting for free. But the response was encouraging. 120 subscribers in the first week validated the hypothesis. Bumping the price from EUR 8 to EUR 12 was a gut call based on [[person-matteo-cellini]]'s advice — "if people are subscribing this fast, you're underpriced." He was right.
|
||||
|
||||
The reading sprint was exactly what I needed. Eight books in two months, with time to actually think about each one. "Thinking in Systems" by Donella Meadows is probably the most underrated book in the business canon — it changed how I think about feedback loops in newsletter growth. Summer energy is real — I feel recharged and ready for an intense September.
|
||||
@@ -1,29 +0,0 @@
|
||||
---
|
||||
aliases: ["September 2024"]
|
||||
Is A: Month
|
||||
Created at: "2024-09-28"
|
||||
Belongs to: "[[24q3]]"
|
||||
Rating: "😄"
|
||||
---
|
||||
# September 2024
|
||||
|
||||
## Focus
|
||||
|
||||
- Deliver [[24q3-codemotion-talk]] at Codemotion Milan
|
||||
- Push premium tier to 300+ subscribers
|
||||
- Close Q3 strong on sponsor pipeline ([[24q3-new-sponsor-verticals]])
|
||||
|
||||
## Highlights
|
||||
|
||||
- Delivered [[24q3-codemotion-talk]] on "Scaling Engineering Culture" to ~400 attendees — standing ovation, 3 inbound sponsor inquiries within a week
|
||||
- [[24q3-premium-tier]] hit 320 paid subscribers — AMA calls proving to be the most valued feature
|
||||
- [[24q3-new-sponsor-verticals]]: [[person-matteo-cellini]] closed deals with two DevOps platform companies, diversifying the sponsor base
|
||||
- Released episodes 17-20 of [[24q3-podcast-season-2]] — guest episode with a VP Engineering at a Series D startup was the most downloaded yet
|
||||
- Newsletter hit 46k subscribers — Codemotion exposure plus podcast cross-promotion driving growth
|
||||
- [[measure-sponsorship-mrr]] reached EUR 10.1k — first time crossing EUR 10k
|
||||
|
||||
## Reflections
|
||||
|
||||
Codemotion was terrifying and wonderful. I spent the week before rehearsing obsessively, convinced I'd forget my lines or that the audience wouldn't care. None of that happened. The talk landed well, the Q&A was engaged, and the hallway conversations afterward were some of the best I've had this year. Three sponsors reaching out unsolicited — that's the power of being on stage in front of your target audience.
|
||||
|
||||
But September also felt like the start of a crunch period. Premium content, podcast production, conference prep, sponsor pipeline — the workload is stacking up. I'm starting to feel the edges of my capacity. [[person-paco-furiani]] flagged that I missed two invoicing deadlines because I was focused on the talk. He's right — I need to stay disciplined about the operational basics even when the creative work is exciting. Heading into Q4, I need to be honest about what I can sustain.
|
||||
@@ -1,28 +0,0 @@
|
||||
---
|
||||
aliases: ["October 2024"]
|
||||
Is A: Month
|
||||
Created at: "2024-10-28"
|
||||
Belongs to: "[[24q4]]"
|
||||
Rating: "😐"
|
||||
---
|
||||
# October 2024
|
||||
|
||||
## Focus
|
||||
|
||||
- Begin [[24q4-laputa-start]] — initial prototype of the PKM tool
|
||||
- Design [[24q4-annual-review-process]] template
|
||||
- Start planning [[24q4-black-friday-campaign]]
|
||||
|
||||
## Highlights
|
||||
|
||||
- Started [[24q4-laputa-start]]: Tauri v2 + React scaffold, basic markdown file reading, first pass at YAML frontmatter parsing — spending evenings and weekends on it
|
||||
- Drafted [[24q4-annual-review-process]] template with quarterly, goal, and project review sections
|
||||
- [[24q4-black-friday-campaign]] strategy defined: 30% off annual premium, referral bonuses, limited-time offers
|
||||
- Newsletter at 47.5k subscribers — growth steady but nothing spectacular
|
||||
- [[person-matteo-cellini]] started building the [[24q4-sponsor-dashboard]] concept — mockups in Figma
|
||||
|
||||
## Reflections
|
||||
|
||||
October was a grind. The post-Codemotion high wore off fast, replaced by the reality of a packed Q4. Starting Laputa was exciting but also a bit reckless — adding a software project on top of everything else. But I couldn't help it. The frustration with existing PKM tools had been building for months, and once I started coding, I couldn't stop. The first time I saw my vault rendered in the four-panel layout, I got that spark you only get when you're building something you genuinely need.
|
||||
|
||||
The newsletter growth plateauing at ~47k is worrying. We need a big push to hit [[2024-reach-50k-subscribers]] by year end. The Black Friday campaign is the obvious lever, but we need it to be genuinely compelling, not just another discount email. [[person-paco-furiani]] suggested adding a community element to the campaign — worth exploring. Energy-wise, I'm running at about 70%. Not burned out, but not firing on all cylinders either. Need to protect sleep and exercise.
|
||||
@@ -1,31 +0,0 @@
|
||||
---
|
||||
aliases: ["November 2024"]
|
||||
Is A: Month
|
||||
Created at: "2024-11-28"
|
||||
Belongs to: "[[24q4]]"
|
||||
Rating: "😄"
|
||||
---
|
||||
# November 2024
|
||||
|
||||
## Focus
|
||||
|
||||
- Execute [[24q4-black-friday-campaign]] — the big push to 50k subscribers
|
||||
- Ship [[24q4-sponsor-dashboard]] for sponsor self-service metrics
|
||||
- Continue Laputa development ([[24q4-laputa-start]])
|
||||
|
||||
## Highlights
|
||||
|
||||
- [[24q4-black-friday-campaign]] was a massive success: 2,800 new subscribers in 10 days, 180 free-to-premium conversions, best campaign ever
|
||||
- Newsletter crossed 50k subscribers on November 22nd — [[2024-reach-50k-subscribers]] achieved, two months ahead of my mental timeline
|
||||
- Shipped [[24q4-sponsor-dashboard]] — sponsors can now see impressions, clicks, and conversion data in real-time — [[person-matteo-cellini]] reports dramatically fewer support emails
|
||||
- Laputa ([[24q4-laputa-start]]) now has working search, type-based navigation, and basic frontmatter editing — using it for my own vault daily
|
||||
- [[measure-sponsorship-mrr]] hit EUR 11.2k — November renewals came in strong
|
||||
- Recorded end-of-year reflections episode for the podcast — raw and personal, felt right
|
||||
|
||||
## Reflections
|
||||
|
||||
Crossing 50k subscribers was the defining moment of the month. I remember refreshing the Substack dashboard at 11pm on a Friday, watching the number tick from 49,998 to 50,001. It sounds silly, but I actually teared up a little. Two years ago, 50k felt like a fantasy. The Black Friday campaign was the catalyst, but it only worked because of 11 months of consistent work before it.
|
||||
|
||||
The sponsor dashboard was [[person-matteo-cellini]]'s baby, and he executed it beautifully. Giving sponsors self-serve access to their data is the kind of product thinking that separates a media business from a newsletter. [[person-paco-furiani]] handled the backend automation — pulling data from Substack's API into a clean dashboard. Small team, big impact.
|
||||
|
||||
Laputa is becoming a real tool. I'm using it every day now, and the workflow of navigating my vault through types and relationships is already better than what I had in Obsidian. It's rough around the edges, but the bones are good. Heading into December with real momentum.
|
||||
@@ -1,32 +0,0 @@
|
||||
---
|
||||
aliases: ["December 2024"]
|
||||
Is A: Month
|
||||
Created at: "2024-12-28"
|
||||
Belongs to: "[[24q4]]"
|
||||
Rating: "😄"
|
||||
---
|
||||
# December 2024
|
||||
|
||||
## Focus
|
||||
|
||||
- Run [[24q4-annual-review-process]] — review all goals, projects, and metrics for [[2024]]
|
||||
- Complete [[24q4-cycling-year-review]] — analyze training data, plan 2025 cycling goals
|
||||
- Set [[2025]] goals and plan [[25q1]]
|
||||
|
||||
## Highlights
|
||||
|
||||
- Completed [[24q4-annual-review-process]]: reviewed all 4 quarters, 20 projects, and 5 annual goals — documented everything in structured templates
|
||||
- [[24q4-cycling-year-review]]: 6,200 km total, FTP up 12% YoY, 2 gran fondos completed — decided to target Stelvio for [[2025-ride-stelvio]]
|
||||
- Set [[2025]] goals: [[2025-reach-85k-subscribers]], [[2025-reach-22k-mrr]], [[2025-ship-laputa]], [[2025-ride-stelvio]], [[2025-read-20-books]]
|
||||
- Newsletter closed the year at 50.2k subscribers — slight post-Black-Friday churn, but net position is excellent
|
||||
- [[measure-sponsorship-mrr]] ended at EUR 11.4k — [[2024-double-revenue]] confirmed (was EUR 5.5k in January)
|
||||
- Read 2 books in December to finish at 26 total — [[2024-read-24-books]] exceeded
|
||||
- [[person-paco-furiani]] and I sketched out the 2025 operational calendar — quarterly planning cadence locked in
|
||||
|
||||
## Reflections
|
||||
|
||||
December is always reflective, and this one more than most. Sitting down to review [[2024]] in detail, I realized that every major goal was either hit or exceeded. That almost never happens. The newsletter from 35k to 50k. Revenue doubled. Podcast launched and growing. Two gran fondos. 26 books. It was a genuinely great year.
|
||||
|
||||
But the review also surfaced where I was lucky vs. where I was good. The Black Friday campaign was well-executed, but the timing was also favorable — low competition in the engineering leadership newsletter space. The pillar articles strategy worked partly because of SEO tailwinds. I don't want to mistake a rising tide for good swimming.
|
||||
|
||||
Looking ahead to [[2025]], the goals are deliberately ambitious: 85k subscribers, EUR 22k MRR, shipping Laputa as a product, riding Stelvio. These require a different gear than 2024. More team, more systems, less heroics. The cycling review was satisfying — seeing 6,200 km logged, the FTP curve rising, the race results improving — it's the same compounding principle as the newsletter, just applied to watts instead of words. Here's to [[2025]].
|
||||
@@ -1,32 +0,0 @@
|
||||
---
|
||||
aliases: ["Complete Two Gran Fondos"]
|
||||
Is A: Goal
|
||||
Belongs to: "[[2024]]"
|
||||
Status: Done
|
||||
---
|
||||
# Complete Two Gran Fondos
|
||||
|
||||
Cycling is a core part of how I manage energy and sustain focus for the business. Completing two gran fondos in 2024 was about proving that endurance fitness and a demanding content business can coexist, and about setting a physical benchmark for the year tied to [[responsibility-health-fitness]].
|
||||
|
||||
## Success criteria
|
||||
|
||||
- Finish at least two official gran fondo events (120+ km each)
|
||||
- Complete both without DNF, regardless of placement
|
||||
- Maintain consistent training volume of 400+ km/month during the build phase
|
||||
- No injuries that impact work capacity
|
||||
|
||||
## Key milestones
|
||||
|
||||
- Plan the cycling season and select target events during [[24q1-plan-cycling-season]]
|
||||
- Build base fitness through structured winter training (Jan-Mar)
|
||||
- Complete first gran fondo by end of Q2
|
||||
- Complete second gran fondo by end of Q3
|
||||
- Maintain [[measure-cycling-km-per-month]] above 400 km during peak training months
|
||||
|
||||
## Notes
|
||||
|
||||
- Completed both events successfully: Nove Colli in May and Maratona dles Dolomites in July.
|
||||
- Training volume averaged 480 km/month from February through July, well above target.
|
||||
- The structured training plan from [[24q1-plan-cycling-season]] was essential. Without it, the business would have crowded out ride time.
|
||||
- Key insight: blocking training rides on the calendar like meetings is non-negotiable. Treating fitness as optional leads to skipped sessions.
|
||||
- This goal set the foundation for the more ambitious [[2025-ride-stelvio]] target.
|
||||
@@ -1,34 +0,0 @@
|
||||
---
|
||||
aliases: ["Double Sponsorship Revenue"]
|
||||
Is A: Goal
|
||||
Belongs to: "[[2024]]"
|
||||
Status: Done
|
||||
---
|
||||
# Double Sponsorship Revenue
|
||||
|
||||
Revenue growth is the clearest signal that the newsletter business is becoming a sustainable long-term venture. Doubling MRR from ~7k to 14k+ in 2024 would prove that the sponsorship model scales with audience growth and that the business can support a small team without requiring a pivot to paid subscriptions or courses.
|
||||
|
||||
## Success criteria
|
||||
|
||||
- Reach 14,000 EUR MRR by December 2024
|
||||
- Maintain or increase average deal size (target: 1,200+ EUR per placement)
|
||||
- Onboard at least 6 new sponsors over the year
|
||||
- Achieve 80%+ renewal rate on existing sponsor contracts
|
||||
- Keep [[measure-close-rate]] above 30% on inbound leads
|
||||
|
||||
## Key milestones
|
||||
|
||||
- Launch restructured sponsorship packages with tiered pricing in [[24q1-launch-sponsorship-packages]]
|
||||
- Build a lightweight CRM to manage pipeline and renewals via [[24q2-sponsor-crm]]
|
||||
- Hit 10k MRR by end of Q2 (midpoint target)
|
||||
- Reach 12k MRR by end of Q3 with [[24q3-premium-tier]] upsells
|
||||
- Close Q4 at 14k+ MRR, with pipeline visibility into Q1 2025
|
||||
|
||||
## Notes
|
||||
|
||||
- Final MRR in December 2024: approximately 15,200 EUR. Goal exceeded by ~8%.
|
||||
- The tiered sponsorship packages from [[24q1-launch-sponsorship-packages]] were the single biggest driver. The premium tier accounted for 40% of new revenue.
|
||||
- [[24q2-sponsor-crm]] was simple (a Notion database) but effective. Having a structured pipeline view reduced missed follow-ups significantly.
|
||||
- Renewal rate landed at 78%, just below the 80% target. Two sponsors churned due to budget cuts, not dissatisfaction.
|
||||
- The subscriber growth from [[2024-reach-50k-subscribers]] directly enabled higher pricing. Audience size and engagement metrics are the foundation of sponsorship value.
|
||||
- This sets the trajectory for [[2025-reach-22k-mrr]], which will require both audience growth and continued pricing optimization.
|
||||
@@ -1,34 +0,0 @@
|
||||
---
|
||||
aliases: ["Launch Refactoring Podcast"]
|
||||
Is A: Goal
|
||||
Belongs to: "[[2024]]"
|
||||
Status: Done
|
||||
---
|
||||
# Launch Refactoring Podcast
|
||||
|
||||
The podcast is the biggest new distribution channel for Refactoring in 2024. Audio content reaches a different audience segment than the newsletter and deepens engagement with existing subscribers. A successful podcast also opens new sponsorship inventory, directly supporting [[2024-double-revenue]] and [[responsibility-sponsorships]].
|
||||
|
||||
## Success criteria
|
||||
|
||||
- Launch Season 1 by end of Q1 2024
|
||||
- Publish at least 20 episodes across two seasons in the year
|
||||
- Reach 1,000+ downloads per episode by Season 2
|
||||
- Secure at least 2 podcast-specific sponsors
|
||||
- Maintain a sustainable production cadence without degrading newsletter quality
|
||||
|
||||
## Key milestones
|
||||
|
||||
- Produce and launch Season 1 during [[24q1-podcast-season-1]] (8-10 episodes)
|
||||
- Establish production workflow with [[person-paco-furiani]] handling editing and distribution
|
||||
- Launch Season 2 during [[24q3-podcast-season-2]] with improved format based on Season 1 feedback
|
||||
- Track growth via [[measure-podcast-downloads]] and [[measure-podcast-episodes-per-month]]
|
||||
- Review analytics quarterly using [[procedure-podcast-analytics]]
|
||||
|
||||
## Notes
|
||||
|
||||
- Season 1 launched in March 2024 with 10 episodes. Season 2 launched in August with 12 episodes. Total: 22 episodes, exceeding the 20-episode target.
|
||||
- Downloads per episode grew from ~400 in Season 1 to ~1,300 by late Season 2. Crossed the 1,000 threshold in episode 16.
|
||||
- Secured 2 podcast sponsors by Q3, both cross-sold from existing newsletter sponsors. The podcast inventory added approximately 1,800 EUR/month to [[measure-sponsorship-mrr]].
|
||||
- Production workflow stabilized by episode 5. [[person-paco-furiani]] now handles end-to-end production with minimal input needed.
|
||||
- Interview format performed better than solo episodes for downloads. Solo episodes had higher completion rates.
|
||||
- The podcast is now a permanent channel. Planning [[25q2-podcast-season-3]] to continue momentum into 2025.
|
||||
@@ -1,34 +0,0 @@
|
||||
---
|
||||
aliases: ["Reach 50k Subscribers"]
|
||||
Is A: Goal
|
||||
Belongs to: "[[2024]]"
|
||||
Status: Done
|
||||
---
|
||||
# Reach 50k Subscribers
|
||||
|
||||
Subscriber count is the foundational growth metric for the newsletter business. Reaching 50,000 subscribers by end of 2024 would put Refactoring firmly in the top tier of engineering newsletters, enabling premium sponsorship pricing and establishing the audience base needed for future product launches. This goal is the north star for all [[responsibility-grow-newsletter]] efforts.
|
||||
|
||||
## Success criteria
|
||||
|
||||
- End 2024 with 50,000+ total newsletter subscribers
|
||||
- Maintain monthly net growth of 2,000+ subscribers
|
||||
- Keep unsubscribe rate below 1.5% per issue
|
||||
- Achieve organic growth rate of at least 60% (not purely paid acquisition)
|
||||
- Maintain [[measure-open-rate]] above 45% as the list scales
|
||||
|
||||
## Key milestones
|
||||
|
||||
- Cross 35,000 subscribers by end of Q1, validating early-year growth tactics
|
||||
- Launch [[24q2-10-pillar-articles]] to drive SEO-based organic acquisition
|
||||
- Cross 42,000 by mid-year, on pace for the 50k target
|
||||
- Experiment with [[24q4-linkedin-crossposting]] as an additional growth channel in Q4
|
||||
- Hit 50,000 by December with a sustained final push through [[responsibility-grow-newsletter]]
|
||||
|
||||
## Notes
|
||||
|
||||
- Final count: 53,000 subscribers by December 2024. Exceeded target by 6%.
|
||||
- Organic growth accounted for approximately 65% of new subscribers. Top sources: Twitter/X, SEO from pillar articles, and word-of-mouth referrals.
|
||||
- The [[24q2-10-pillar-articles]] project was the single biggest organic growth lever. Those 10 articles now account for ~30% of monthly organic signups.
|
||||
- [[measure-open-rate]] held at 47% even at scale, which is strong for a list this size. This is a key selling point for sponsors.
|
||||
- LinkedIn cross-posting ([[24q4-linkedin-crossposting]]) added ~150 subscribers directly but had an outsized brand awareness effect.
|
||||
- The path to [[2025-reach-85k-subscribers]] is clear but will require new channels. Organic growth from existing channels alone will not close the gap.
|
||||
@@ -1,33 +0,0 @@
|
||||
---
|
||||
aliases: ["Read 24 Books in 2024"]
|
||||
Is A: Goal
|
||||
Belongs to: "[[2024]]"
|
||||
Status: Behind
|
||||
---
|
||||
# Read 24 Books in 2024
|
||||
|
||||
Consistent reading is the primary input for original thinking and content quality. A target of 24 books (2 per month) ensures a steady flow of ideas feeding into newsletter essays, podcast topics, and [[responsibility-learning]]. Falling behind on reading directly correlates with weaker content output.
|
||||
|
||||
## Success criteria
|
||||
|
||||
- Read 24 books by December 31, 2024 (2 per month average)
|
||||
- Maintain at least 1 book per month even in busy periods
|
||||
- Track progress via [[measure-books-per-month]]
|
||||
- At least 50% should be non-fiction relevant to the newsletter (engineering, business, leadership)
|
||||
- Create at least 5 [[measure-evergreen-notes-created]] from each book
|
||||
|
||||
## Key milestones
|
||||
|
||||
- Establish the reading habit in Q1 with 6 books by March
|
||||
- Maintain pace through Q2 despite the busy season for [[24q2-10-pillar-articles]]
|
||||
- Reach 18 books by end of Q3
|
||||
- Close the year at 24 by maintaining 2/month in Q4
|
||||
|
||||
## Notes
|
||||
|
||||
- Final count: 18 books. Missed the 24-book target by 6 books (75% completion).
|
||||
- Q1 started strong at 7 books, ahead of pace. Q2 dropped to 3 books due to the pillar articles push and podcast launch.
|
||||
- Q3 recovered to 5 books. Q4 was only 3 books as year-end business priorities took over.
|
||||
- The shortfall was entirely a time management issue, not a motivation issue. During weeks with heavy content production, reading was the first thing to get cut.
|
||||
- Adjusted the 2025 target to 20 books ([[2025-read-20-books]]) to be more realistic given current business demands.
|
||||
- Key insight: audiobooks during cycling training significantly boosted volume. 6 of the 18 books were consumed this way. Will lean into this more in 2025.
|
||||
@@ -1,49 +0,0 @@
|
||||
---
|
||||
aliases: ["2024"]
|
||||
Is A: Year
|
||||
Created at: "2024-01-01"
|
||||
Has: ["[[24q1]]", "[[24q2]]", "[[24q3]]", "[[24q4]]"]
|
||||
---
|
||||
# 2024
|
||||
|
||||
## Theme
|
||||
|
||||
2024 was the growth year — the year Refactoring went from "successful newsletter" to "real business." At the start of January, I had ~35,000 subscribers, ad-hoc sponsor deals, no podcast, and a vague sense that this could become something bigger. By December, the newsletter had crossed 50k subscribers, revenue had more than doubled, the podcast was a genuine channel, and I had a small team helping me run things. The word that keeps coming back when I think about this year is *professionalization*.
|
||||
|
||||
But 2024 was also the year I stopped being just a content creator and started being a founder. Launching sponsorship packages, hiring an editor, building a sponsor CRM, creating a premium tier — these aren't creative acts, they're business-building acts. And toward the end of the year, starting [[24q4-laputa-start]] added a product dimension that I hadn't anticipated. I ended the year wearing more hats than ever, but feeling more focused than ever. Paradox noted.
|
||||
|
||||
## Highlights
|
||||
|
||||
- Newsletter grew from ~35k to 50k+ subscribers — [[2024-reach-50k-subscribers]] achieved in November
|
||||
- Revenue more than doubled: [[measure-sponsorship-mrr]] went from EUR 5.5k in January to EUR 11.4k in December — [[2024-double-revenue]] hit
|
||||
- Launched the podcast in [[24q1]] — grew from 0 to 8k+ downloads/month across two seasons ([[24q1-podcast-season-1]], [[24q3-podcast-season-2]])
|
||||
- Shipped [[24q3-premium-tier]] with 320+ paid subscribers by year end
|
||||
- Completed both gran fondos — Nove Colli ([[24q2-spring-gran-fondo]]) and Maratona dles Dolomites — [[2024-complete-two-gran-fondos]] done
|
||||
- Published [[24q2-10-pillar-articles]] including "Staff Engineer vs. Manager" (50k+ views)
|
||||
- Delivered [[24q3-codemotion-talk]] to ~400 attendees — first major conference appearance
|
||||
- Built the team: [[person-matteo-cellini]] on partnerships, [[person-paco-furiani]] on operations, freelance editor for content
|
||||
- [[24q4-black-friday-campaign]] drove 2,800 new subscribers in 10 days
|
||||
- Started building [[24q4-laputa-start]] — the PKM tool I'd been dreaming about for years
|
||||
|
||||
## By the numbers
|
||||
|
||||
- **Subscribers**: 35k to 50.2k (+43%)
|
||||
- **Sponsorship MRR**: EUR 5.5k to EUR 11.4k (+107%)
|
||||
- **Podcast downloads**: 0 to 8.2k/month (24 episodes across 2 seasons)
|
||||
- **Premium subscribers**: 0 to 320 (launched in [[24q3]])
|
||||
- **Books read**: 26 — [[2024-read-24-books]] exceeded by 2
|
||||
- **Cycling km**: 6,200 km — [[measure-cycling-km-per-month]] averaged ~517 km
|
||||
- **Gran fondos**: 2/2 — Nove Colli (7h42m), Maratona dles Dolomites (9h15m)
|
||||
- **Open rate**: [[measure-open-rate]] averaged 44.2%, up from 41.8% in 2023
|
||||
- **Conference talks**: 1 (Codemotion Milan)
|
||||
- **Total revenue**: ~EUR 155k (sponsorships + premium + speaking)
|
||||
|
||||
## Reflections
|
||||
|
||||
Looking back at 2024, the thing I'm most proud of isn't any single achievement — it's the system. At the start of the year, everything ran through me. By December, [[person-matteo-cellini]] was closing sponsor deals I never even saw, [[person-paco-furiani]] was handling invoicing and logistics autonomously, and the editor was shipping polished drafts on a weekly cadence. I built leverage, not just output.
|
||||
|
||||
The podcast was the surprise of the year. I'd been so anxious about launching it — imposter syndrome about my speaking voice, worry about production quality, fear that nobody would listen. But the format I landed on (20-minute focused deep dives) resonated immediately. Season 2 adding guest interviews was the right evolution. By December, the podcast was driving subscriber growth, sponsor interest, and content ideas in a way I hadn't anticipated. It's now an essential pillar, not a side experiment.
|
||||
|
||||
The premium tier launch in Q3 validated something I'd been unsure about: that people would pay for curated, structured content even when the free version was already high quality. The 320 subscribers at EUR 12/month represent a small but meaningful revenue stream — and more importantly, a direct relationship with my most engaged readers. The AMA calls are gold for understanding what engineering leaders actually struggle with.
|
||||
|
||||
If I'm being honest about what didn't go well: I pushed too hard in Q3. The combination of the premium launch, Codemotion, and Maratona training left me running on fumes by October. The Black Friday campaign in Q4 was successful but felt mechanical — I was going through motions rather than creating with energy. That's a warning sign I need to heed going into [[2025]]. The business can scale, but my energy can't scale linearly with it. I need to hire more, delegate more, and protect creative time more aggressively. The Laputa project ([[24q4-laputa-start]]) is a wildcard heading into next year — it could be a distraction or it could be the most important thing I build. We'll see.
|
||||
@@ -1,29 +0,0 @@
|
||||
---
|
||||
aliases: ["January 2025"]
|
||||
Is A: Month
|
||||
Created at: "2025-01-28"
|
||||
Belongs to: "[[25q1]]"
|
||||
Rating: "😄"
|
||||
---
|
||||
# January 2025
|
||||
|
||||
## Focus
|
||||
|
||||
- Begin [[25q1-laputa-v1]] development sprint — four-panel layout, markdown rendering, search
|
||||
- Plan [[25q1-rate-increase]] strategy with [[person-matteo-cellini]]
|
||||
- Start [[25q1-strength-program]] — 3x/week gym sessions
|
||||
|
||||
## Highlights
|
||||
|
||||
- Kicked off [[25q1-laputa-v1]] development with a focused 2-week sprint — four-panel layout working, markdown rendering with frontmatter extraction functional
|
||||
- Researched competitor sponsor rates and prepared [[25q1-rate-increase]] proposal with [[person-matteo-cellini]] — planning to raise Gold from EUR 2.8k to EUR 3.5k/issue
|
||||
- Started [[25q1-strength-program]]: working with a coach, 3x/week focusing on core stability and leg strength for cycling
|
||||
- Newsletter at 51.8k subscribers — steady organic growth to start the year
|
||||
- Read "Build" by Tony Fadell — excellent on product thinking, relevant to Laputa
|
||||
- Cycling: 320 km, mostly indoor on the trainer — January weather in northern Italy is brutal
|
||||
|
||||
## Reflections
|
||||
|
||||
January is always a reset month, and this one felt especially deliberate. Coming off the high of [[2024]], I wanted to start [[2025]] with intention rather than momentum alone. The Laputa development sprint was intense — coding every evening after the newsletter work was done, often until midnight. But seeing the four-panel layout come alive with my own vault data was deeply motivating. It's scratching an itch that's been bothering me for years.
|
||||
|
||||
The strength program is humbling. My first session, I could barely hold a plank for 60 seconds. Years of cycling have given me the cardiovascular fitness of someone 10 years younger and the upper body strength of someone 10 years older. The coach just smiled and said "we'll fix that." Three weeks in, everything hurts, but I can already feel the difference in posture and stability on the bike.
|
||||
@@ -1,29 +0,0 @@
|
||||
---
|
||||
aliases: ["February 2025"]
|
||||
Is A: Month
|
||||
Created at: "2025-02-28"
|
||||
Belongs to: "[[25q1]]"
|
||||
Rating: "🤩"
|
||||
---
|
||||
# February 2025
|
||||
|
||||
## Focus
|
||||
|
||||
- Execute [[25q1-rate-increase]] — communicate new pricing to sponsors
|
||||
- Launch [[25q1-referral-program]] — design tiers, set up tracking
|
||||
- Continue [[25q1-laputa-v1]] — add search and navigation features
|
||||
|
||||
## Highlights
|
||||
|
||||
- Executed [[25q1-rate-increase]]: communicated new Gold rate (EUR 3.5k/issue) to all sponsors — 9 out of 10 renewed without pushback, one negotiated a 6-month lock at the old rate
|
||||
- Launched [[25q1-referral-program]] with three tiers: 3 referrals (exclusive article), 10 referrals (premium trial), 25 referrals (1:1 call) — 1,100 referral subscribers in the first 3 weeks
|
||||
- [[25q1-laputa-v1]] now has full-text search, type-based sidebar navigation, and keyboard shortcuts — my daily driver for vault browsing
|
||||
- [[measure-sponsorship-mrr]] jumped to EUR 13.5k after rate increase — significant lift with minimal churn
|
||||
- Newsletter at 54.2k subscribers — referral program providing a noticeable acceleration
|
||||
- [[person-matteo-cellini]] onboarded a new enterprise sponsor (cloud infrastructure company) at the higher rate — first deal at the new pricing
|
||||
|
||||
## Reflections
|
||||
|
||||
This was the month I learned that raising prices is one of the highest-leverage moves in a business. We spent weeks agonizing over the rate increase, running scenarios, worrying about sponsor churn. And then... almost nothing happened. Sponsors said "sure, sounds fair" and renewed. One even said "honestly, I'm surprised you didn't raise earlier." That's the curse of the bootstrapper mindset — you're so grateful for every dollar that you forget to charge what you're worth.
|
||||
|
||||
The referral program is generating genuine excitement. Seeing readers actively sharing Refactoring with their colleagues because they want the rewards — it's word-of-mouth at scale. The 1:1 call tier at 25 referrals is clever because it creates a personal connection between me and my most engaged readers. Three people earned it in the first month, and those calls were some of the best conversations I've had about engineering leadership. February was excellent. Everything is tracking.
|
||||
@@ -1,30 +0,0 @@
|
||||
---
|
||||
aliases: ["March 2025"]
|
||||
Is A: Month
|
||||
Created at: "2025-03-28"
|
||||
Belongs to: "[[25q1]]"
|
||||
Rating: "😄"
|
||||
---
|
||||
# March 2025
|
||||
|
||||
## Focus
|
||||
|
||||
- Complete [[25q1-newsletter-seo-sprint]] — optimize top 15 articles for search
|
||||
- Ship [[25q1-laputa-v1]] as internal release — feature-complete for personal use
|
||||
- First outdoor rides of the season
|
||||
|
||||
## Highlights
|
||||
|
||||
- Completed [[25q1-newsletter-seo-sprint]]: optimized 15 pillar articles with updated titles, meta descriptions, internal linking, and structured data — organic traffic up 40% by month end
|
||||
- [[25q1-laputa-v1]] shipped as "internal v1" — search, navigation, frontmatter editing, and property panel all working — vault has 9,000+ files and performs well
|
||||
- Referral program ([[25q1-referral-program]]) running strong — 3,200 total referral subscribers in Q1
|
||||
- Newsletter hit 56k subscribers — [[25q1]] delivered well above expectations
|
||||
- [[25q1-strength-program]] paying off — can hold a 2-minute plank now, leg press up 40%, climbing power noticeably improved
|
||||
- First outdoor ride of the season: 85 km along the Po River, 12 degrees, absolutely beautiful
|
||||
- [[measure-sponsorship-mrr]] at EUR 14.2k — strong close to [[25q1]]
|
||||
|
||||
## Reflections
|
||||
|
||||
March was the month where Q1 came together. The SEO sprint was unglamorous work — rewriting titles, adding alt text to images, building internal link maps — but the results were immediate. One article ("How to Run Effective 1:1s") went from page 3 to the #2 result for its target keyword. That single article now brings in 200+ organic subscribers per month. The compounding effect of SEO on a content business is underrated.
|
||||
|
||||
Shipping Laputa v1 felt like a real milestone, even if I'm the only user. It's not pretty, it has bugs, and the graph view is more confusing than helpful. But it works. I browse my vault in it every day — types on the left, notes in the middle, properties on the right. The workflow is faster than Obsidian for my use case, and that's all the validation I need to keep building. Next up: bidirectional linking and a better graph. Closing [[25q1]] feeling strong and focused. [[25q2]] is going to be intense.
|
||||
@@ -1,31 +0,0 @@
|
||||
---
|
||||
aliases: ["April 2025"]
|
||||
Is A: Month
|
||||
Created at: "2025-04-28"
|
||||
Belongs to: "[[25q2]]"
|
||||
Rating: "😄"
|
||||
---
|
||||
# April 2025
|
||||
|
||||
## Focus
|
||||
|
||||
- Begin [[25q2-podcast-season-3]] production — new "Tactical Tuesday" format alongside regular episodes
|
||||
- Start [[25q2-laputa-v2]] development — bidirectional linking, graph view improvements
|
||||
- Plan [[25q2-team-retreat]] logistics for late May
|
||||
|
||||
## Highlights
|
||||
|
||||
- Recorded first 4 episodes of [[25q2-podcast-season-3]] including the debut "Tactical Tuesday" — 15-minute actionable episodes dropping every Tuesday
|
||||
- Started [[25q2-laputa-v2]] with bidirectional linking engine — the backlinks panel is already transforming how I navigate between notes
|
||||
- [[25q2-team-retreat]] booked: farmhouse outside Siena, 3 days in late May, agenda drafted with [[person-paco-furiani]]
|
||||
- Newsletter at 58.5k subscribers — growth accelerating, referral program still compounding
|
||||
- Published a deep-dive on "The Architecture of Engineering Onboarding" — 28k views, widely shared on HackerNews
|
||||
- Cycling: 480 km, first real outdoor training block — spring is here and the motivation is high
|
||||
|
||||
## Reflections
|
||||
|
||||
April felt like the start of the real work for [[25q2]]. The "Tactical Tuesday" format for the podcast was an experiment born from reader feedback — people kept saying "I love the deep dives but sometimes I just need a quick actionable takeaway." So we're doing both: deep dives on Thursdays, tactical tips on Tuesdays. Early numbers are promising — the short episodes have a higher completion rate.
|
||||
|
||||
The Laputa work on bidirectional linking was technically challenging but incredibly satisfying. Clicking on a note and seeing all the other notes that link to it, without having to manually maintain those connections — it's the kind of feature that makes you wonder how you ever worked without it. My vault is starting to feel alive, like a knowledge graph I can actually traverse.
|
||||
|
||||
Planning the retreat with [[person-paco-furiani]] was a reminder of how much the team dynamic matters. We're four people working remotely, mostly asynchronously. That works for execution, but not for alignment. Three days together in Tuscany should fix that.
|
||||
@@ -1,30 +0,0 @@
|
||||
---
|
||||
aliases: ["May 2025"]
|
||||
Is A: Month
|
||||
Created at: "2025-05-28"
|
||||
Belongs to: "[[25q2]]"
|
||||
Rating: "😐"
|
||||
---
|
||||
# May 2025
|
||||
|
||||
## Focus
|
||||
|
||||
- Execute [[25q2-team-retreat]] in Tuscany
|
||||
- Push toward [[25q2-reach-70k]] — growth sprint with coordinated content + referral push
|
||||
- Continue [[25q2-laputa-v2]] — graph view and frontmatter editing
|
||||
|
||||
## Highlights
|
||||
|
||||
- [[25q2-team-retreat]] in Siena was transformational — 3 days of strategy, bonding, and cooking together with [[person-matteo-cellini]], [[person-paco-furiani]], and [[person-sara-ricci]]
|
||||
- The community idea emerged from a dinner conversation at the retreat — [[person-sara-ricci]] pitched it, everyone immediately saw the potential
|
||||
- Newsletter at 64k subscribers — growth strong but the 70k target for June feels aggressive
|
||||
- [[25q2-laputa-v2]] graph view working but confusing at scale — need to rethink the visual density for 9,000+ nodes
|
||||
- Two newsletter issues underperformed — open rates dipped below 40% for the first time this year
|
||||
|
||||
## Reflections
|
||||
|
||||
The retreat was everything I hoped for and more. There's a moment on the second evening — the four of us on the terrace, bottles of Brunello open, whiteboarding H2 plans on a flip chart — where it hit me that this is a real team, not just a collection of freelancers. [[person-sara-ricci]]'s community pitch was brilliant in its simplicity: "Your readers already help each other in reply-all email chains. Give them a proper space to do it." That became [[25q3-community-launch]].
|
||||
|
||||
But May was also the first month this year where I felt like I was slipping. Two issues with below-40% open rates is a warning sign. The topics weren't wrong, but the subject lines were lazy — I wrote them in a rush. In a newsletter business, subject lines are the product as much as the content. Sloppy subject lines = sloppy product. Need to get disciplined about this again.
|
||||
|
||||
The Laputa graph view at 9,000 nodes is essentially unusable — just a hairball of connections. I need to rethink the approach: maybe cluster by type, maybe progressive disclosure, maybe skip the full graph entirely and focus on local neighborhoods. Technical challenge, but also a product design challenge. Not everything that works at 100 nodes works at 10,000.
|
||||
@@ -1,32 +0,0 @@
|
||||
---
|
||||
aliases: ["June 2025"]
|
||||
Is A: Month
|
||||
Created at: "2025-06-28"
|
||||
Belongs to: "[[25q2]]"
|
||||
Rating: "🤩"
|
||||
---
|
||||
# June 2025
|
||||
|
||||
## Focus
|
||||
|
||||
- Hit [[25q2-reach-70k]] subscribers
|
||||
- Ship [[25q2-laputa-v2]] with refined graph view and improved frontmatter editing
|
||||
- Start planning [[25q2-dolomites-trip]] logistics for late June
|
||||
|
||||
## Highlights
|
||||
|
||||
- Hit [[25q2-reach-70k]] subscribers on June 12th — two weeks ahead of schedule, driven by two viral LinkedIn posts and the referral program
|
||||
- Shipped [[25q2-laputa-v2]]: bidirectional linking, local graph view (neighborhood-based, not full graph), improved frontmatter editing — feels like a real product now
|
||||
- Completed [[25q2-dolomites-trip]]: 4 days, 5 passes, 380 km with three friends — Passo Fedaia, Pordoi, Sella, Gardena, Campolongo
|
||||
- [[25q2-podcast-season-3]] "Tactical Tuesday" format growing fast — 14k total downloads/month
|
||||
- [[measure-sponsorship-mrr]] hit EUR 17.5k — two new enterprise sponsors signed
|
||||
- Premium tier reached 680 paying subscribers — AMA calls now have 40+ attendees
|
||||
- Two pillar articles syndicated by InfoQ and The New Stack — drove ~4k new subscribers
|
||||
|
||||
## Reflections
|
||||
|
||||
June was the peak of [[25q2]] — everything came together. Hitting 70k subscribers two weeks early felt effortless, which is a sign that the flywheel is genuinely spinning now. The LinkedIn posts that went viral weren't planned; they were honest reflections on engineering leadership challenges that resonated. Authenticity scales, apparently.
|
||||
|
||||
The Dolomites trip was the cycling highlight of the year so far. Four days of pure mountain riding with friends — the kind of trip where you forget about metrics and just pedal. Passo Fedaia at sunset, the Marmolada glacier turning orange overhead, was a moment I'll remember forever. My legs felt strong — the [[25q1-strength-program]] is paying dividends in climbing power and stability.
|
||||
|
||||
Shipping Laputa v2 with the local graph view was the right call. Instead of showing all 9,000 nodes (useless hairball), it shows the current note plus 2 degrees of connection. Clean, useful, navigable. Sometimes the best product decisions are about what you don't show. Heading into Q3 with serious momentum and slightly too much on my plate.
|
||||
@@ -1,31 +0,0 @@
|
||||
---
|
||||
aliases: ["July 2025"]
|
||||
Is A: Month
|
||||
Created at: "2025-07-28"
|
||||
Belongs to: "[[25q3]]"
|
||||
Rating: "😄"
|
||||
---
|
||||
# July 2025
|
||||
|
||||
## Focus
|
||||
|
||||
- Begin [[25q3-ebook]] production — compile, edit, and design "The Engineering Leader's Playbook"
|
||||
- Prepare for [[25q3-community-launch]] on Circle — set up platform, invite founding members
|
||||
- Enter [[25q3-peak-training]] block for Stelvio in August
|
||||
|
||||
## Highlights
|
||||
|
||||
- [[25q3-ebook]] first draft completed — 12 chapters compiled from the best newsletter content, heavily rewritten for book format, sent to editor
|
||||
- [[25q3-community-launch]] platform set up on Circle — designed channels (ask-anything, career-advice, tech-leadership, off-topic), invited first 200 premium subscribers as founding members
|
||||
- [[25q3-peak-training]] underway: structured intervals 4x/week, long ride every Saturday, altitude simulation sessions on the trainer
|
||||
- Newsletter at 72k subscribers — summer slowdown minimal thanks to the community buzz
|
||||
- Recorded first episodes of [[25q3-podcast-season-4]] — the 3-part "Building Engineering Culture from Scratch" series
|
||||
- [[person-sara-ricci]] took over community moderation and onboarding, freeing me to focus on content and training
|
||||
|
||||
## Reflections
|
||||
|
||||
July was the month where the ambition of Q3 became real. Three major launches (ebook, community, podcast season) plus peak training for Stelvio. On paper it looks insane. In practice... it felt intense but manageable, mostly because [[person-sara-ricci]] stepped up massively on the community side. She went from coordinating podcast guests to designing the entire community architecture. The founding members are engaged, the conversations are high-quality, and I barely had to touch it.
|
||||
|
||||
The ebook rewriting was harder than expected. Newsletter articles are written for a 5-minute read with specific context. A book chapter needs to stand alone, build on previous chapters, and go deeper. I spent more time rewriting than compiling. But the result feels substantial — not a lazy content recycling, but a genuine book that adds value beyond the newsletter.
|
||||
|
||||
Training is going well. The altitude simulation sessions are brutal — 90 minutes at reduced oxygen while doing threshold intervals — but I can feel my body adapting. FTP is climbing toward 280W. Stelvio from Bormio is 24 km at 7.1% average gradient. At my weight and power, that's roughly 1h50m. Tight, but possible. The mountain is calling.
|
||||
@@ -1,31 +0,0 @@
|
||||
---
|
||||
aliases: ["August 2025"]
|
||||
Is A: Month
|
||||
Created at: "2025-08-28"
|
||||
Belongs to: "[[25q3]]"
|
||||
Rating: "🤩"
|
||||
---
|
||||
# August 2025
|
||||
|
||||
## Focus
|
||||
|
||||
- Publish [[25q3-ebook]] — final edits, cover design, launch campaign
|
||||
- Ride Stelvio — the culmination of [[25q3-peak-training]] and [[2025-ride-stelvio]]
|
||||
- Open [[25q3-community-launch]] to all premium subscribers
|
||||
|
||||
## Highlights
|
||||
|
||||
- Published [[25q3-ebook]] "The Engineering Leader's Playbook" — 1,400 copies sold in the first month at EUR 29, far exceeding the 500-copy target
|
||||
- Rode Stelvio from Bormio in 1h52m — [[2025-ride-stelvio]] achieved — the hardest and most beautiful thing I've done on a bike
|
||||
- [[25q3-community-launch]] opened to all premium members — 850 founding members, 65% weekly active rate from day one
|
||||
- [[25q3-podcast-season-4]] debut: "Building Engineering Culture from Scratch" Part 1 got 22k downloads in the first week — biggest episode ever
|
||||
- FTP peaked at 285W during [[25q3-peak-training]] — best numbers of my life
|
||||
- Newsletter at 75k subscribers
|
||||
|
||||
## Reflections
|
||||
|
||||
Stelvio. I don't know how to write about it without sounding dramatic, so I'll just be dramatic. It was the hardest thing I've ever done on a bike. The Bormio side starts deceptively gentle — wide roads, gradual grade, you think "this isn't so bad." Then the switchbacks begin. 48 of them. Each one steeper than the last, each one revealing another section of road winding above you. At km 18, with 6 km still to go, I almost stopped. My legs were screaming, my heart rate was at 175, and a voice in my head was saying "you've proved enough." But I kept going because the summit was up there, and I'd been thinking about it since December. 1h52m. Not fast. But done. I cried at the top. No shame.
|
||||
|
||||
The ebook launch going well was almost anticlimactic after Stelvio. But 1,400 copies in the first month is genuinely impressive for a self-published niche book. The "real book" effect is interesting — people treat you differently when you have a book, even if the content is largely derived from free newsletter articles. Perception is reality in media.
|
||||
|
||||
I need to be honest: by the end of August, I was running on empty. Three big things shipped (ebook, community, Stelvio), podcast recording ongoing, newsletter still weekly. Sleep has been under 6 hours most nights. [[person-paco-furiani]] told me I looked tired on our weekly call. He's right. September needs to include real rest, or I'll pay for it later.
|
||||
@@ -1,31 +0,0 @@
|
||||
---
|
||||
aliases: ["September 2025"]
|
||||
Is A: Month
|
||||
Created at: "2025-09-28"
|
||||
Belongs to: "[[25q3]]"
|
||||
Rating: "😄"
|
||||
---
|
||||
# September 2025
|
||||
|
||||
## Focus
|
||||
|
||||
- Deliver keynote at [[25q3-leaddev-london]]
|
||||
- Recover from August intensity — schedule white space, prioritize sleep
|
||||
- Close Q3 metrics and plan Q4
|
||||
|
||||
## Highlights
|
||||
|
||||
- Delivered keynote at [[25q3-leaddev-london]] on "The Newsletter-to-Business Pipeline" to 1,200 attendees — biggest stage ever, standing ovation, 5 enterprise sponsor inquiries
|
||||
- Took 10 days fully off in early September — first real break since January, no email, no Slack, just cycling and reading
|
||||
- Newsletter hit 78k subscribers by month end — the LeadDev exposure and ebook cross-promotion driving a late-Q3 surge
|
||||
- [[measure-sponsorship-mrr]] reached EUR 20.1k — [[2025-reach-22k-mrr]] within reach for Q4
|
||||
- [[25q3-podcast-season-4]] wrapped up with 8 episodes released — consistently above 15k downloads per episode
|
||||
- Community stabilizing at 850 members with strong engagement — [[person-sara-ricci]] running it almost independently
|
||||
|
||||
## Reflections
|
||||
|
||||
The 10 days off in early September saved me. I came back from August brittle — snapping at [[person-paco-furiani]] over minor things, dreading the newsletter instead of enjoying it, skipping workouts. The break was non-negotiable: I told the team I'd be unreachable, set up auto-responders, and disappeared into the Dolomites with my bike and a stack of books. By day 4, the fog lifted. By day 7, I was excited about work again. By day 10, I had a notebook full of ideas for Q4.
|
||||
|
||||
LeadDev London was the professional peak of the year. The keynote was different from Codemotion last year — bigger stage, broader audience, higher stakes. But I was more prepared and, honestly, more confident. The talk traced the arc from "person with a newsletter" to "media business with a team, a podcast, a book, and a community." The vulnerability of sharing revenue numbers on stage felt risky but landed well. Authenticity again.
|
||||
|
||||
Q3 was the most productive quarter of my career. Ebook published, community launched, Stelvio climbed, LeadDev delivered, podcast season completed. But it was also the most draining. The lesson is crystal clear: I can sustain this intensity for 13 weeks, but not 26. Q4 needs to be calmer. Fewer launches. More depth. More sleep.
|
||||
@@ -1,31 +0,0 @@
|
||||
---
|
||||
aliases: ["October 2025"]
|
||||
Is A: Month
|
||||
Created at: "2025-10-28"
|
||||
Belongs to: "[[25q4]]"
|
||||
Rating: "😐"
|
||||
---
|
||||
# October 2025
|
||||
|
||||
## Focus
|
||||
|
||||
- Begin [[25q4-laputa-v3]] development — AI chat panel, type creation, graph improvements
|
||||
- Start [[25q4-2026-sponsors]] pipeline with [[person-matteo-cellini]]
|
||||
- Settle into a sustainable Q4 rhythm after the intensity of Q3
|
||||
|
||||
## Highlights
|
||||
|
||||
- [[25q4-laputa-v3]] development started: AI chat panel prototype working — can ask questions about vault content and get contextual answers with linked references
|
||||
- [[person-matteo-cellini]] began [[25q4-2026-sponsors]] outreach — 4 sponsors already in early conversations for Q1-Q2 2026
|
||||
- Newsletter at 80k subscribers — growth continuing but I'm less obsessed with the number than I used to be
|
||||
- Community reaching a self-sustaining rhythm — members helping each other without prompting, which is the whole point
|
||||
- Cycling winding down for the season — 280 km this month, mostly easy rides, letting the body recover from Stelvio
|
||||
- Read "Working in Public" by Nadia Eghbal and "The Mom Test" by Rob Fitzpatrick — both relevant to the community and Laputa product thinking
|
||||
|
||||
## Reflections
|
||||
|
||||
October was deliberately quieter than the previous three months, and I'm grateful for it. After Q3's intensity, I needed a month of sustained work at 70% capacity rather than another sprint at 110%. The Laputa v3 work has been the right kind of creative — exploratory, no deadlines, just building something cool. The AI chat panel started as a "what if" experiment and quickly became the most interesting feature I've built. Asking your vault "what did I write about technical debt last year?" and getting a contextual, linked answer feels like magic.
|
||||
|
||||
But I'm also feeling a low-grade anxiety about hitting [[2025-reach-85k-subscribers]] by year end. We're at 80k with two months to go. The math works — 2.5k/month is within our normal range — but it doesn't leave room for a bad month. The Black Friday campaign will be critical again.
|
||||
|
||||
The sponsor pipeline work is less exciting but equally important. [[person-matteo-cellini]] is already mapping out 2026, which means I need to make decisions about pricing, formats, and what we offer. The business is maturing in ways that require more strategic thinking and less reactive selling. That's a good problem to have, but it requires a different kind of energy.
|
||||
@@ -1,32 +0,0 @@
|
||||
---
|
||||
aliases: ["November 2025"]
|
||||
Is A: Month
|
||||
Created at: "2025-11-28"
|
||||
Belongs to: "[[25q4]]"
|
||||
Rating: "😄"
|
||||
---
|
||||
# November 2025
|
||||
|
||||
## Focus
|
||||
|
||||
- Push for [[25q4-reach-85k]] with Black Friday campaign
|
||||
- Ship [[25q4-laputa-v3]] features — type creation UI, improved AI chat
|
||||
- Begin [[25q4-financial-review]] — compile 2025 revenue and expense data
|
||||
|
||||
## Highlights
|
||||
|
||||
- Black Friday campaign launched: bundled ebook + premium annual subscription at 30% off — early results strong, 1,800 new subscribers in the first week
|
||||
- Newsletter at 83.5k subscribers — on pace for [[25q4-reach-85k]] by mid-December
|
||||
- [[25q4-laputa-v3]] type creation shipped — users (well, me) can now define custom types with properties directly in the app, no more editing YAML manually
|
||||
- [[25q4-financial-review]] started: 2025 tracking to ~EUR 260k total revenue (sponsorships EUR 210k, premium EUR 35k, ebook EUR 15k)
|
||||
- [[person-matteo-cellini]] closed 3 more [[25q4-2026-sponsors]] deals — Q1 2026 pipeline at EUR 24k+ MRR
|
||||
- [[measure-podcast-downloads]] averaging 18k/month — consistent and growing
|
||||
- AI chat panel refined: better context retrieval, source linking, faster responses — starting to share with a few trusted friends for feedback
|
||||
|
||||
## Reflections
|
||||
|
||||
The Black Friday campaign this year was more strategic than last year's. Instead of a pure discount play, we bundled the ebook with premium — giving people a reason to upgrade that goes beyond price. Early numbers suggest it's working better than last year's approach. [[person-paco-furiani]] set up the automation so cleanly that I barely had to touch it — sign-ups flowing in, welcome emails going out, access provisioning automatic. The team is genuinely running the machine now.
|
||||
|
||||
The financial review numbers are staggering to me. EUR 260k from what was, not that long ago, a side project newsletter. The breakdown is healthy too — sponsorships dominate but premium and the ebook are diversifying the revenue. If the 2026 sponsor pipeline closes as projected, we'll be looking at EUR 300k+ next year. That's "hire a full-time person" territory.
|
||||
|
||||
Laputa v3 is getting close to something I could show other people without apologizing for it. The type creation UI was the missing piece — non-technical users (hypothetical ones, since I'm still the only user) can now define their own taxonomy without touching YAML. [[2025-ship-laputa]] feels achievable. I've started sharing it with 3 friends who maintain large vaults. Their feedback has been encouraging: "this is what I wanted Obsidian to be." Music to my ears.
|
||||
@@ -1,34 +0,0 @@
|
||||
---
|
||||
aliases: ["December 2025"]
|
||||
Is A: Month
|
||||
Created at: "2025-12-28"
|
||||
Belongs to: "[[25q4]]"
|
||||
Rating: "😄"
|
||||
---
|
||||
# December 2025
|
||||
|
||||
## Focus
|
||||
|
||||
- Close the year: hit [[25q4-reach-85k]], finalize [[25q4-financial-review]], run [[25q4-year-review-2025]]
|
||||
- Complete [[25q4-laputa-v3]] — prepare for closed beta in January
|
||||
- Reflect, rest, and plan [[2025]] wrap-up
|
||||
|
||||
## Highlights
|
||||
|
||||
- Newsletter crossed 85k subscribers on December 11th — [[2025-reach-85k-subscribers]] achieved, exactly one year after hitting 50k
|
||||
- [[25q4-financial-review]] finalized: 2025 total revenue EUR 262k, up 69% from 2024 — [[2025-reach-22k-mrr]] hit in November with MRR at EUR 22.3k
|
||||
- [[25q4-year-review-2025]] completed: every 2025 goal either hit or exceeded — [[2025-reach-85k-subscribers]], [[2025-reach-22k-mrr]], [[2025-ride-stelvio]], [[2025-ship-laputa]] (beta), [[2025-read-20-books]] (21 books)
|
||||
- [[25q4-laputa-v3]] beta-ready: 12 closed beta testers lined up for January, onboarding docs written, feedback channels set up
|
||||
- [[person-matteo-cellini]] locked in 8 sponsors for [[25q4-2026-sponsors]] Q1-Q2 pipeline — projected MRR EUR 24.5k
|
||||
- End-of-year podcast episode recorded: honest, vulnerable, grateful — the kind of episode that's hard to record and easy to listen to
|
||||
- Read 3 books in December: finished at 21 for the year
|
||||
|
||||
## Reflections
|
||||
|
||||
Crossing 85k subscribers felt different from crossing 50k last year. Last year was euphoria — "I can't believe this is happening." This year was quieter — "of course it happened, look at the system we built." That shift from surprise to expectation is both a sign of maturity and a subtle loss. I miss the wonder a little. But I'll take sustainable confidence over periodic amazement.
|
||||
|
||||
The financial review was the most sobering exercise of the month. EUR 262k in revenue. A real team of four people. A product in beta. A published book. A keynote at LeadDev. Two years ago, I was a solo newsletter writer hoping to make rent from sponsorships. The speed of change is disorienting when you zoom out. In the day-to-day, it feels gradual. In the annual review, it feels dramatic.
|
||||
|
||||
Laputa going to beta testers in January is the thing I'm most excited about heading into 2026. Building software for yourself is satisfying, but it's also a trap — you optimize for your own quirks and blind spots. Having 12 other people use it will surface assumptions I didn't know I was making. Some of those assumptions will be wrong. That's the point.
|
||||
|
||||
[[2025]] was the year I stopped being a content creator with a side hustle and became a founder running a media company with a software product. Both descriptions are true, but the second one is the more accurate framing now. Looking ahead: more team, more product, more depth, less heroics. And maybe Ventoux next summer. We'll see.
|
||||
@@ -1,33 +0,0 @@
|
||||
---
|
||||
aliases: ["Reach €22k MRR"]
|
||||
Is A: Goal
|
||||
Belongs to: "[[2025]]"
|
||||
Status: Open
|
||||
---
|
||||
# Reach €22k MRR
|
||||
|
||||
Growing sponsorship revenue to 22,000 EUR/month is the key financial milestone for 2025. Hitting this target would make Refactoring a comfortably profitable business capable of supporting a team of 3-4, funding product development like [[25q1-laputa-v1]], and providing the financial runway to experiment with new revenue lines such as community or premium offerings.
|
||||
|
||||
## Success criteria
|
||||
|
||||
- Reach 22,000 EUR MRR by December 2025
|
||||
- Maintain average deal size above 1,400 EUR per placement
|
||||
- Grow sponsor roster to 15+ active sponsors (up from ~10)
|
||||
- Achieve 85%+ renewal rate on existing contracts
|
||||
- Keep [[measure-close-rate]] above 35% on inbound leads
|
||||
|
||||
## Key milestones
|
||||
|
||||
- Start the year at 15k MRR baseline (carry-over from 2024 contracts)
|
||||
- Launch updated sponsorship packages with audience segmentation data in [[25q1]]
|
||||
- Cross 18k MRR by mid-year through new sponsor acquisition
|
||||
- Introduce premium placement options tied to [[25q2-reach-70k]] subscriber milestone
|
||||
- Close Q4 at 22k MRR, with strong pipeline for 2026
|
||||
|
||||
## Notes
|
||||
|
||||
- Currently tracking at approximately 19k MRR as of Q3. The gap to 22k is closable but requires 2-3 new sponsors or upsells in Q4.
|
||||
- Subscriber growth ([[2025-reach-85k-subscribers]]) is the enabler. Every 10k subscriber increase unlocks ~1,500 EUR in pricing power.
|
||||
- The [[25q1-referral-program]] is driving higher-quality subscribers, which improves engagement metrics that sponsors care about (open rate, click rate).
|
||||
- Risk: if [[measure-subscribers]] growth stalls, pricing increases become harder to justify. Revenue and audience growth are tightly coupled.
|
||||
- The community experiment ([[25q3-discord-community-soft]]) could become a supplemental revenue line, but is not included in the 22k MRR target to keep it focused on sponsorships.
|
||||
@@ -1,33 +0,0 @@
|
||||
---
|
||||
aliases: ["Reach 85k Subscribers"]
|
||||
Is A: Goal
|
||||
Belongs to: "[[2025]]"
|
||||
Status: Open
|
||||
---
|
||||
# Reach 85k Subscribers
|
||||
|
||||
Reaching 85,000 subscribers by end of 2025 is the primary growth target for the year. This represents roughly 60% growth over the 53k base from end of 2024 and would position Refactoring as one of the largest independent engineering newsletters globally. Audience scale is the engine that drives [[measure-sponsorship-mrr]], enables premium pricing for [[2025-reach-22k-mrr]], and provides the distribution base for any future product launches.
|
||||
|
||||
## Success criteria
|
||||
|
||||
- End 2025 with 85,000+ total newsletter subscribers
|
||||
- Maintain net monthly growth of 2,500+ subscribers
|
||||
- Keep unsubscribe rate below 1.3% per issue
|
||||
- Maintain [[measure-open-rate]] above 43% at scale
|
||||
- Diversify acquisition channels: no single channel should account for more than 40% of new subscribers
|
||||
|
||||
## Key milestones
|
||||
|
||||
- Launch [[25q1-newsletter-seo-sprint]] to build a long-term organic acquisition engine
|
||||
- Launch [[25q1-referral-program]] to drive word-of-mouth growth
|
||||
- Cross 65,000 by end of Q1 (strong start to the year)
|
||||
- Reach 70,000 by mid-year through [[25q2-reach-70k]] push
|
||||
- Hit 85,000 by December with sustained multi-channel growth through [[25q4-reach-85k]]
|
||||
|
||||
## Notes
|
||||
|
||||
- Currently at approximately 75,000 subscribers as of Q3. On track but the pace needs to hold through Q4.
|
||||
- The [[25q1-referral-program]] has been the strongest new channel in 2025, accounting for ~20% of new subscribers. Referral subscribers also have higher engagement.
|
||||
- SEO from the [[25q1-newsletter-seo-sprint]] is a slow burn. Articles are ranking but take 3-6 months to reach full traffic potential. Expect the biggest impact in Q4.
|
||||
- Twitter/X remains the largest single channel at ~35%, close to the 40% cap. Need to continue diversifying.
|
||||
- Key risk: list quality at scale. As the audience grows, maintaining [[measure-open-rate]] above 43% requires active list hygiene and consistently high content quality from [[responsibility-content-production]].
|
||||
@@ -1,33 +0,0 @@
|
||||
---
|
||||
aliases: ["Read 20 Books in 2025"]
|
||||
Is A: Goal
|
||||
Belongs to: "[[2025]]"
|
||||
Status: Open
|
||||
---
|
||||
# Read 20 Books in 2025
|
||||
|
||||
Reading remains the highest-leverage input for content quality and original thinking. The 2025 target of 20 books is calibrated to be ambitious but realistic based on the [[2024-read-24-books]] experience, where the 24-book target was missed at 18. The adjusted goal accounts for the increasing demands of a growing business while preserving [[responsibility-learning]] as a non-negotiable priority.
|
||||
|
||||
## Success criteria
|
||||
|
||||
- Read 20 books by December 31, 2025
|
||||
- Maintain at least 1 book per month, even in the busiest months
|
||||
- Track monthly progress via [[measure-books-per-month]]
|
||||
- At least 60% should be non-fiction relevant to content, engineering, or business
|
||||
- Create at least 3 newsletter essay ideas per quarter sourced from reading
|
||||
|
||||
## Key milestones
|
||||
|
||||
- Read 5 books by end of Q1 to build early momentum
|
||||
- Integrate audiobooks into cycling training to boost volume (learned from 2024)
|
||||
- Reach 10 books by mid-year
|
||||
- Maintain pace through the busy Q3 podcast and community launch season
|
||||
- Close the year at 20 by protecting reading time in Q4
|
||||
|
||||
## Notes
|
||||
|
||||
- Currently at 14 books as of Q3. On pace to hit 20, but Q4 needs to deliver 6 books which matches the best quarterly performance in 2024.
|
||||
- Audiobooks during cycling continue to be the most reliable reading channel. 8 of the 14 books so far were consumed via audio during rides.
|
||||
- The shift to 60% non-fiction is working well. Recent reads on platform economics and community building directly informed the [[25q3-community-launch]] strategy.
|
||||
- The biggest risk to this goal is the Q4 crunch: [[25q4-reach-85k]] and year-end sponsor renewals tend to consume all available bandwidth.
|
||||
- Linking reading notes to [[measure-evergreen-notes-created]] helps ensure books translate into lasting knowledge rather than forgotten highlights.
|
||||
@@ -1,34 +0,0 @@
|
||||
---
|
||||
aliases: ["Ride the Stelvio Pass"]
|
||||
Is A: Goal
|
||||
Belongs to: "[[2025]]"
|
||||
Status: Done
|
||||
---
|
||||
# Ride the Stelvio Pass
|
||||
|
||||
The Stelvio Pass is one of the most iconic climbs in professional cycling: 24.3 km at 7.4% average gradient, reaching 2,758 meters of elevation. Completing a full ascent has been a bucket-list goal for years and represents the intersection of [[responsibility-health-fitness]] and personal ambition. After successfully completing two gran fondos in 2024 ([[2024-complete-two-gran-fondos]]), the Stelvio was the natural next challenge.
|
||||
|
||||
## Success criteria
|
||||
|
||||
- Complete a full ascent of the Stelvio Pass from Prato allo Stelvio (the classic east side)
|
||||
- Finish under 2 hours 15 minutes
|
||||
- No mechanical issues or health incidents during the climb
|
||||
- Maintain [[measure-cycling-km-per-month]] above 500 km during the 3-month build phase
|
||||
- Sustain [[measure-resting-hr]] below 52 bpm during peak training
|
||||
|
||||
## Key milestones
|
||||
|
||||
- Begin structured climbing-specific training block in March 2025
|
||||
- Complete at least 3 alpine climbs of 1,500+ meters elevation gain as preparation rides by May
|
||||
- Ride the Stelvio in June or July, weather permitting
|
||||
- Document the ride for a potential newsletter essay on [[topic-cycling-training]]
|
||||
- Recover and return to baseline training volume within 2 weeks
|
||||
|
||||
## Notes
|
||||
|
||||
- Completed the Stelvio in late June 2025 in 2 hours 8 minutes. Under the 2:15 target by 7 minutes.
|
||||
- The climbing-specific training block made a significant difference. Focused on sustained threshold efforts and long climbs in the Dolomites during April-June.
|
||||
- [[measure-resting-hr]] averaged 50 bpm during peak training, indicating good aerobic adaptation.
|
||||
- Weather was perfect on the day. Started early (6:30 AM) to avoid afternoon traffic and wind.
|
||||
- Wrote a newsletter essay about the experience that became one of the most-engaged issues of the year. Personal stories consistently outperform tactical content.
|
||||
- This was the most personally meaningful goal completed in 2025. The physical challenge is a reminder that capacity is often self-limited.
|
||||
@@ -1,34 +0,0 @@
|
||||
---
|
||||
aliases: ["Ship Laputa App"]
|
||||
Is A: Goal
|
||||
Belongs to: "[[2025]]"
|
||||
Status: Done
|
||||
---
|
||||
# Ship Laputa App
|
||||
|
||||
Laputa is a personal knowledge and life management desktop app designed to replace the patchwork of tools currently used to manage the vault of ~9,200 markdown files that power content production, goal tracking, and personal operations. Shipping a usable v1 and adopting it as the daily driver is both a product goal and an infrastructure investment: a better tool for managing knowledge directly improves output quality for [[responsibility-content-production]] and operational clarity across all responsibilities.
|
||||
|
||||
## Success criteria
|
||||
|
||||
- Ship Laputa v1 as a functional Tauri desktop app with four-panel UI
|
||||
- Support reading and editing markdown files with YAML frontmatter
|
||||
- Implement vault navigation, search, and filtering by type
|
||||
- Use Laputa as the primary daily knowledge management tool (replace current workflow)
|
||||
- Achieve stable performance with the full ~9,200 file vault
|
||||
|
||||
## Key milestones
|
||||
|
||||
- Build the core Tauri + React architecture and file I/O layer during [[25q1-laputa-v1]]
|
||||
- Implement the four-panel UI (sidebar, list, editor, properties) by end of Q1
|
||||
- Add frontmatter parsing, type filtering, and basic search by mid-Q2
|
||||
- Iterate on editor experience (CodeMirror 6 integration) during [[25q2-laputa-v2]]
|
||||
- Reach daily-driver status by end of Q2 and sustain through Q3-Q4
|
||||
|
||||
## Notes
|
||||
|
||||
- Laputa v1 shipped in March 2025 and has been the daily driver since April. The app handles the full vault without performance issues.
|
||||
- The Tauri v2 + React + TypeScript stack was the right choice. Desktop performance is excellent and the Rust backend handles file I/O efficiently even at scale.
|
||||
- The mock layer (`src/mock-tauri.ts`) proved invaluable for rapid UI iteration without needing the full backend running.
|
||||
- CodeMirror 6 integration was the most complex part of the frontend. Live preview with reveal-on-focus required significant customization.
|
||||
- [[25q2-laputa-v2]] added refinements: better search, improved frontmatter editing, and the AI chat panel.
|
||||
- Key lesson: building your own tools is high-leverage when you are the primary user. Every improvement to Laputa directly improves daily workflow efficiency across [[responsibility-content-production]], [[responsibility-personal-finance]], and [[responsibility-learning]].
|
||||
@@ -1,51 +0,0 @@
|
||||
---
|
||||
aliases: ["2025"]
|
||||
Is A: Year
|
||||
Created at: "2025-01-01"
|
||||
Has: ["[[25q1]]", "[[25q2]]", "[[25q3]]", "[[25q4]]"]
|
||||
---
|
||||
# 2025
|
||||
|
||||
## Theme
|
||||
|
||||
2025 is the scale year — the year the question shifted from "can this work?" to "how big can this get?" Coming off the momentum of [[2024]], every metric pointed upward: subscribers, revenue, podcast downloads, team capacity. The goal wasn't just to grow more, but to grow *better* — building systems and a team that could sustain 85k subscribers, EUR 22k MRR, and a product (Laputa) without everything depending on me.
|
||||
|
||||
But scaling surfaced a tension I hadn't anticipated: the more successful Refactoring becomes, the more it pulls me away from the craft that made it successful in the first place — writing. Managing sponsors, coordinating a team, shipping a product, speaking at conferences — these are all valuable, but they're not writing. The creative struggle of 2025 has been protecting the space to think deeply and write well, while everything else demands attention. Some months I won that battle. Others, I didn't.
|
||||
|
||||
## Highlights
|
||||
|
||||
- Newsletter grew from 50k to 82k+ subscribers (on track for [[2025-reach-85k-subscribers]] by year end)
|
||||
- [[measure-sponsorship-mrr]] grew from EUR 11.4k to EUR 20.1k by end of Q3, tracking toward [[2025-reach-22k-mrr]]
|
||||
- Shipped three versions of Laputa: [[25q1-laputa-v1]], [[25q2-laputa-v2]], [[25q4-laputa-v3]] — from internal prototype to closed beta candidate ([[2025-ship-laputa]])
|
||||
- Podcast grew to 18k downloads/month across Seasons 3 and 4 ([[25q2-podcast-season-3]], [[25q3-podcast-season-4]])
|
||||
- Published [[25q3-ebook]] "The Engineering Leader's Playbook" — 1,400+ copies sold in the first month
|
||||
- Launched [[25q3-community-launch]] with 850 founding members
|
||||
- Rode Stelvio from Bormio in 1h52m — [[2025-ride-stelvio]] achieved
|
||||
- Keynote at [[25q3-leaddev-london]] to 1,200 attendees — biggest speaking engagement to date
|
||||
- [[25q2-team-retreat]] in Tuscany aligned the team for H2 and catalyzed the community idea
|
||||
- Executed [[25q1-rate-increase]] — sponsor rates up 25%, retention at 90%
|
||||
|
||||
## By the numbers
|
||||
|
||||
- **Subscribers**: 50.2k to ~82k (projected 85k by Dec) — +64% YoY
|
||||
- **Sponsorship MRR**: EUR 11.4k to EUR 20.1k (Q3 close) — targeting EUR 22k by year end
|
||||
- **Podcast downloads**: 8.2k/month to 18k/month — +120% YoY
|
||||
- **Premium subscribers**: 320 to 680+ — +112% YoY
|
||||
- **Ebook sales**: 1,400+ copies (launched Q3)
|
||||
- **Community members**: 850 (launched Q3)
|
||||
- **Books read**: on track for [[2025-read-20-books]] (16 through Q3)
|
||||
- **Cycling km**: ~5,800 km through Q3, projected 7,500+ for year — [[measure-cycling-km-per-month]]
|
||||
- **Stelvio**: 1h52m from Bormio — personal milestone
|
||||
- **Conference talks**: 2 (LeadDev London keynote, plus a workshop)
|
||||
- **Total revenue**: projected ~EUR 260k (+68% vs 2024)
|
||||
- **Resting HR**: [[measure-resting-hr]] averaged 48 bpm, down from 52 bpm in 2024
|
||||
|
||||
## Reflections
|
||||
|
||||
The most important thing I did in 2025 was hire well and delegate aggressively. [[person-matteo-cellini]] now runs the entire sponsor relationship lifecycle — from prospecting to renewal — and does it better than I did. [[person-paco-furiani]] owns operations, invoicing, and now coordinates the community logistics. [[person-sara-ricci]] handles podcast production and guest coordination. For the first time, I can take a week off and nothing breaks. That's the real milestone, even if it doesn't show up in the metrics.
|
||||
|
||||
The ebook and community launches in Q3 represented a philosophical shift. For three years, Refactoring was a one-to-many broadcast channel: I write, you read. The community made it many-to-many. Engineering leaders helping each other, sharing war stories, giving feedback on each other's challenges. I'm a facilitator now, not just a broadcaster. It's a better model, but it requires a different kind of energy — more listening, less performing.
|
||||
|
||||
Laputa has been the most surprising journey of the year. What started as a weekend hack in Q4 2024 has become a serious product with three major versions shipped. Using it daily to manage my 9,000+ file vault has been the best kind of dogfooding — every friction point I hit becomes a feature. The AI chat panel in v3 was a breakthrough moment: asking your own vault questions and getting contextual, linked answers feels like the future of personal knowledge management. Whether this becomes a real product with real users or stays a personal tool, building it has made me a better engineer and a better thinker.
|
||||
|
||||
If I'm being honest, Q3 nearly broke me. Three major launches, a keynote, and peak cycling training in the same 13 weeks was too much. I hit a wall in late August — low energy, poor sleep, short temper. [[person-paco-furiani]] noticed before I did. I took 10 days fully off in early September and came back feeling human again. The lesson is one I keep having to relearn: ambition without recovery is just a burnout timeline. Going into Q4 and planning for 2026, I'm trying to build more white space into the calendar. Fewer launches per quarter. More weeks with nothing scheduled. The business can handle it — the question is whether I can resist the urge to fill every gap with a new project. History suggests I can't, but I'm working on it.
|
||||
@@ -1,36 +0,0 @@
|
||||
---
|
||||
aliases: ["Launch Sponsorship Packages"]
|
||||
Is A: Project
|
||||
Belongs to: "[[24q1]]"
|
||||
Advances: "[[responsibility-sponsorships]]"
|
||||
Status: Done
|
||||
Owner: "[[person-luca-rossi]]"
|
||||
---
|
||||
# Launch Sponsorship Packages
|
||||
|
||||
## Overview
|
||||
|
||||
This project formalized Refactoring's monetization through sponsorships by creating structured packages that sponsors could evaluate and purchase. Before this, sponsorship deals were ad-hoc and inconsistent — pricing varied per deal, deliverables were loosely defined, and there was no media kit to send prospects.
|
||||
|
||||
The goal was to create three clear tiers (Logo, Spotlight, and Deep Dive) with fixed pricing, well-defined deliverables, and a professional media kit PDF. This would make outreach scalable and give [[person-luca-rossi]] a repeatable process instead of negotiating every deal from scratch. The project directly supported [[2024-double-revenue]] by establishing the revenue engine for the year.
|
||||
|
||||
## Goals
|
||||
|
||||
- Define three sponsorship tiers with clear deliverables and pricing (Logo: $500, Spotlight: $1,200, Deep Dive: $2,500)
|
||||
- Create a professional media kit PDF with audience demographics, open rates, and testimonials
|
||||
- Set up a Stripe billing workflow for recurring sponsors
|
||||
- Draft email templates for outreach, follow-up, and renewal
|
||||
- Close the first 3 paying sponsors before end of [[24q1]]
|
||||
|
||||
## Key decisions
|
||||
|
||||
- **Three tiers, not two or four.** Considered a simpler two-tier model, but having three gives sponsors a clear upgrade path. The Logo tier serves as a low-friction entry point.
|
||||
- **Monthly pricing, not per-issue.** Charging monthly (4 issues) simplifies invoicing and gives sponsors more predictable budgets. This also aligns with how [[measure-sponsorship-mrr]] is tracked.
|
||||
- **No exclusivity clauses.** Decided against offering category exclusivity because the newsletter audience is too broad to guarantee meaningful exclusivity, and it would limit revenue potential.
|
||||
|
||||
## Notes
|
||||
|
||||
- The media kit took longer than expected — gathering accurate audience data required exporting from Substack and ConvertKit, which have inconsistent analytics. Settled on a conservative subscriber count to maintain credibility.
|
||||
- [[person-matteo-cellini]] provided useful feedback on the pricing deck layout. His experience with B2B sales helped shape the value proposition framing.
|
||||
- First three sponsors came from warm outreach to developer tool companies who had previously engaged with newsletter content. Cold outreach conversion was near zero at this stage.
|
||||
- This project established the foundation for [[procedure-sponsor-onboarding]] and [[procedure-quarterly-sponsor-outreach]], which were formalized later in [[24q2]].
|
||||
@@ -1,36 +0,0 @@
|
||||
---
|
||||
aliases: ["Plan 2024 Cycling Season"]
|
||||
Is A: Project
|
||||
Belongs to: "[[24q1]]"
|
||||
Advances: "[[responsibility-health-fitness]]"
|
||||
Status: Done
|
||||
Owner: "[[person-luca-rossi]]"
|
||||
---
|
||||
# Plan 2024 Cycling Season
|
||||
|
||||
## Overview
|
||||
|
||||
This project laid out the full 2024 cycling calendar — target events, training blocks, rest weeks, and equipment needs. The main objective was to complete two gran fondos in 2024, supporting [[2024-complete-two-gran-fondos]], while maintaining a sustainable training load that would not interfere with work commitments.
|
||||
|
||||
Planning happened in January and February so that structured training could begin in March. This included selecting target events, mapping out a periodized training plan (base, build, peak, recovery), and identifying any gear upgrades needed before the season. [[person-paco-furiani]] helped review the training plan and suggested adjustments based on his own racing experience.
|
||||
|
||||
## Goals
|
||||
|
||||
- Select two target gran fondos for 2024 (spring and autumn)
|
||||
- Design a 24-week periodized training plan with base, build, and peak phases
|
||||
- Establish weekly volume targets: 8-12 hours per week during build phase
|
||||
- Budget and plan equipment upgrades (new wheelset, tire strategy for events)
|
||||
- Set up tracking in Strava and TrainingPeaks for [[measure-cycling-km-per-month]]
|
||||
|
||||
## Key decisions
|
||||
|
||||
- **Granfondo di Varese (May) and Granfondo dell'Appennino (September)** selected as the two target events. Varese was chosen for its accessibility and moderate difficulty; Appennino for its challenging profile that would test peak fitness.
|
||||
- **Polarized training model** rather than threshold-heavy. After reading several studies on amateur endurance performance, decided that 80/20 (easy/hard) distribution would be more sustainable alongside a demanding work schedule.
|
||||
- **No coach for now.** Considered hiring a cycling coach but decided to self-coach for 2024 using structured plans from TrainingPeaks. If results are good, revisit for 2025.
|
||||
|
||||
## Notes
|
||||
|
||||
- The biggest risk to the plan was always going to be consistency — weeks with heavy newsletter deadlines or sponsor calls tend to eat into training time. Built in buffer weeks to account for this.
|
||||
- Realized during planning that the winter base phase was already partially missed. Adjusted by extending the base phase by two weeks and compressing the first build block slightly.
|
||||
- Equipment decision: went with a new set of carbon wheels (Campagnolo Bora WTO 45) rather than upgrading the frame. Better bang for the buck in terms of performance gain per euro.
|
||||
- This plan feeds directly into [[24q2-spring-gran-fondo]] for the first event execution and into [[topic-cycling-training]] for ongoing training notes.
|
||||
@@ -1,36 +0,0 @@
|
||||
---
|
||||
aliases: ["Podcast Season 1 Launch"]
|
||||
Is A: Project
|
||||
Belongs to: "[[24q1]]"
|
||||
Advances: "[[responsibility-podcast]]"
|
||||
Status: Done
|
||||
Owner: "[[person-luca-rossi]]"
|
||||
---
|
||||
# Podcast Season 1 Launch
|
||||
|
||||
## Overview
|
||||
|
||||
This project launched the Refactoring podcast — a long-form interview show focused on engineering culture, technical leadership, and the human side of building software. The idea had been brewing for months, but the decision to actually ship was driven by [[2024-launch-podcast]] as a key goal for the year.
|
||||
|
||||
The strategy was to batch-record 6 episodes before publishing anything, so that the launch would have a backlog and a consistent weekly cadence from day one. This meant spending most of January and February on guest outreach, recording, and editing, with the public launch in mid-March. [[person-sara-ricci]] joined as editor toward the end of the project to handle post-production, which proved essential for maintaining quality while keeping up with the newsletter schedule.
|
||||
|
||||
## Goals
|
||||
|
||||
- Record 6 episodes before public launch (buffer for consistent weekly releases)
|
||||
- Secure guests with strong engineering leadership backgrounds (CTOs, VPEs, Staff+ engineers)
|
||||
- Set up podcast hosting, RSS feed, and distribution to Apple Podcasts, Spotify, and YouTube
|
||||
- Design cover art and episode template graphics
|
||||
- Establish [[procedure-podcast-recording]] and [[procedure-podcast-editing]] workflows
|
||||
|
||||
## Key decisions
|
||||
|
||||
- **Interview format, not solo.** Considered doing solo episodes or co-hosted commentary, but interviews leverage guest audiences for cross-promotion and are more engaging for a new show with no existing listener base.
|
||||
- **Season model, not continuous.** Adopted a seasonal structure (8-10 episodes per season) with breaks between seasons. This prevents burnout and allows time to plan each season's theme. Season 1 theme: "Engineering Culture."
|
||||
- **Audio-first, video as bonus.** Recorded video but optimized for audio quality. Video clips are used for social promotion, but the primary distribution is audio podcasts. This reduced production overhead significantly.
|
||||
|
||||
## Notes
|
||||
|
||||
- Guest booking was the hardest part. Sent about 40 outreach emails to get 8 confirmed guests (6 recorded, 2 cancelled). Cold outreach to high-profile CTOs had about a 5% response rate. Warm intros from [[person-matteo-cellini]] and [[person-david-kim]] were far more effective.
|
||||
- The first two episodes had noticeable audio quality issues — learned the hard way that remote recordings via Zoom are not sufficient. Switched to Riverside.fm for episodes 3-6, which was a significant improvement.
|
||||
- Launch week saw about 1,200 downloads across the first 3 episodes, which was above the initial target of 500. The newsletter cross-promotion was the primary driver — [[measure-podcast-downloads]] tracked closely with newsletter send days.
|
||||
- This project created the foundation for [[24q3-podcast-season-2]] and all subsequent seasons. The workflows established here, particularly [[procedure-podcast-recording]], have remained largely unchanged.
|
||||
@@ -1,36 +0,0 @@
|
||||
---
|
||||
aliases: ["Redesign Newsletter Template"]
|
||||
Is A: Project
|
||||
Belongs to: "[[24q1]]"
|
||||
Advances: "[[responsibility-content-production]]"
|
||||
Status: Done
|
||||
Owner: "[[person-luca-rossi]]"
|
||||
---
|
||||
# Redesign Newsletter Template
|
||||
|
||||
## Overview
|
||||
|
||||
The Refactoring newsletter template had been largely unchanged since launch — a plain text-heavy layout that worked at small scale but felt increasingly dated as the subscriber base grew. This project redesigned the email template for better readability, visual hierarchy, and sponsor placement. The redesign needed to improve the reading experience while also making sponsorship placements more visually distinct and valuable, supporting [[2024-double-revenue]].
|
||||
|
||||
The new template introduced a cleaner header, better typography spacing, a dedicated sponsor block with clear visual boundaries, and a structured footer with social links and podcast promotion. It was tested across major email clients (Gmail, Apple Mail, Outlook) and went through three iterations based on feedback from a small group of beta readers.
|
||||
|
||||
## Goals
|
||||
|
||||
- Redesign the email template with improved visual hierarchy and readability
|
||||
- Create a dedicated, visually distinct sponsor placement section
|
||||
- Ensure compatibility across Gmail, Apple Mail, Outlook, and mobile clients
|
||||
- Improve click-through rate on inline links by at least 15%
|
||||
- Establish the new template as the standard for [[procedure-weekly-newsletter]]
|
||||
|
||||
## Key decisions
|
||||
|
||||
- **Single-column layout.** Evaluated two-column layouts with sidebar content, but testing showed that single-column performs significantly better on mobile (where 65%+ of opens happen). Simplicity won.
|
||||
- **Sponsor block above the fold.** Moved the sponsor placement from mid-article to just below the header intro. This increased sponsor visibility without feeling intrusive, since the block is clearly labeled and visually separated. Sponsors noticed and appreciated the change.
|
||||
- **No images in the main body.** Kept the template text-focused with minimal imagery. Images in email are unreliable (blocked by many clients), increase load time, and distract from the content. The only image is the sponsor logo.
|
||||
|
||||
## Notes
|
||||
|
||||
- The biggest surprise was how much email client rendering varies. A layout that looked perfect in Gmail was broken in Outlook. Ended up using a very conservative CSS approach with inline styles and table-based layout for maximum compatibility.
|
||||
- [[person-sara-ricci]] helped with copy feedback on the new footer and CTA sections. Her editorial eye caught several awkward phrasings in the template boilerplate.
|
||||
- A/B tested the new template against the old one over two issues. The new template showed a 22% increase in click-through rate, primarily driven by better link placement and the structured "further reading" section at the bottom.
|
||||
- [[measure-open-rate]] remained stable through the transition, confirming that the redesign did not trigger spam filters or cause deliverability issues. This was a real concern given how sensitive email infrastructure is to template changes.
|
||||
@@ -1,36 +0,0 @@
|
||||
---
|
||||
aliases: ["Set Up Investing Framework"]
|
||||
Is A: Project
|
||||
Belongs to: "[[24q1]]"
|
||||
Advances: "[[responsibility-personal-finance]]"
|
||||
Status: Done
|
||||
Owner: "[[person-luca-rossi]]"
|
||||
---
|
||||
# Set Up Investing Framework
|
||||
|
||||
## Overview
|
||||
|
||||
As Refactoring's revenue grew, it became clear that personal finance needed a more structured approach. This project defined a personal investment policy statement (IPS) and set up automated monthly contributions to a diversified portfolio of index funds. The goal was to remove emotion and ad-hoc decision-making from investing and replace it with a simple, repeatable system.
|
||||
|
||||
Before this project, savings were sitting in a low-yield savings account with no clear allocation strategy. The framework needed to be simple enough to maintain alongside a busy content business, while being sophisticated enough to actually build long-term wealth. Research phase took about three weeks, followed by account setup and automation.
|
||||
|
||||
## Goals
|
||||
|
||||
- Draft a personal Investment Policy Statement defining asset allocation, risk tolerance, and rebalancing rules
|
||||
- Open a brokerage account with a low-cost provider (selected Degiro for European access and low fees)
|
||||
- Set up automated monthly contributions (20% of net monthly revenue)
|
||||
- Define a target allocation: 70% global equities (VWCE), 20% bonds (VAGF), 10% cash reserve
|
||||
- Create a quarterly review checklist to assess allocation drift and rebalance if needed
|
||||
|
||||
## Key decisions
|
||||
|
||||
- **Passive index funds, not individual stocks.** After extensive reading (Bogle, Bernstein, Housel), decided that passive investing is the only approach that makes sense for someone whose primary income comes from a content business. Time spent stock-picking is time not spent on the newsletter.
|
||||
- **Single accumulating ETF for equities (VWCE).** Rather than splitting across US, European, and emerging market ETFs, chose a single all-world accumulating ETF. Simplicity reduces friction and the urge to tinker.
|
||||
- **20% of revenue, not a fixed amount.** Tying contributions to revenue percentage means the system scales with business growth and naturally reduces in leaner months without requiring manual adjustment.
|
||||
|
||||
## Notes
|
||||
|
||||
- The hardest part was actually committing to a framework and stopping the research phase. There is an infinite amount of personal finance content online, and it is easy to fall into analysis paralysis. Set a hard deadline of February 15 to finalize the IPS and stop reading.
|
||||
- [[person-marco-bianchi]] shared his own investment framework, which was helpful for validating the approach. His suggestion to keep a separate 6-month emergency fund in a high-yield savings account was incorporated.
|
||||
- The [[24q2-stock-screener]] experiment later in the year was a brief detour into individual stock analysis, but it reinforced the decision to stick with passive investing. The time spent did not justify the marginal (and uncertain) returns.
|
||||
- Quarterly reviews are tracked as part of [[responsibility-personal-finance]]. The first two reviews in 2024 showed minimal drift, confirming that the set-and-forget approach works as intended.
|
||||
@@ -1,31 +0,0 @@
|
||||
---
|
||||
aliases: ["Q1 2024"]
|
||||
Is A: Quarter
|
||||
Created at: "2024-01-01"
|
||||
Belongs to: "[[2024]]"
|
||||
Has: ["[[24q1-launch-sponsorship-packages]]", "[[24q1-redesign-newsletter-template]]", "[[24q1-plan-cycling-season]]", "[[24q1-set-investing-framework]]", "[[24q1-podcast-season-1]]"]
|
||||
Status: Done
|
||||
---
|
||||
# Q1 2024
|
||||
|
||||
## Focus
|
||||
|
||||
The first quarter of 2024 was about laying foundations. After a strong 2023, the goal was to professionalize the business side of Refactoring — move from ad-hoc sponsor deals to structured packages, refresh the newsletter design, and finally launch the podcast that had been in planning for months. On the personal side, I wanted to get the cycling season mapped out early and put a real investing framework in place.
|
||||
|
||||
## Highlights
|
||||
|
||||
- Launched [[24q1-launch-sponsorship-packages]] with three tiers (Gold, Silver, Bronze) — immediately booked two Gold sponsors for Q2
|
||||
- Shipped [[24q1-redesign-newsletter-template]] with a cleaner layout, better mobile rendering, and improved CTA placement — [[measure-open-rate]] ticked up ~1.5pp
|
||||
- Recorded and released the first four episodes of [[24q1-podcast-season-1]], crossing 2k downloads in the first month
|
||||
- Mapped out the full [[24q1-plan-cycling-season]] including target events: Nove Colli in May and Maratona dles Dolomites in July (goal: [[2024-complete-two-gran-fondos]])
|
||||
- Built out [[24q1-set-investing-framework]] — monthly DCA into a global ETF portfolio, emergency fund topped up
|
||||
- Newsletter crossed 37k subscribers by end of March, up from ~35k at year start
|
||||
- [[person-matteo-cellini]] closed the first two sponsor deals under the new packages within weeks of launch
|
||||
|
||||
## Reflections
|
||||
|
||||
Q1 felt like the quarter where things started clicking into place on the business side. The sponsorship packages were long overdue — I'd been doing custom deals for every sponsor, which was eating time and leaving money on the table. Having a clear menu made everything faster: [[person-matteo-cellini]] could sell without looping me in on every detail, and sponsors appreciated the transparency.
|
||||
|
||||
The podcast launch was nerve-wracking. I'd been procrastinating on it for almost a year, finding reasons to delay. But once the first episode went live and the feedback started coming in, I wondered why I'd waited so long. The format — 20-minute deep dives on one engineering leadership topic — resonated immediately. Production quality was rough in the first couple of episodes, but it improved fast.
|
||||
|
||||
On the personal front, planning the cycling season early was a game changer. Having Nove Colli and Maratona on the calendar gave structure to my training weeks. The investing framework was less exciting but equally important — automating contributions meant I could stop thinking about it and focus on building the business. All in all, a solid start to [[2024]].
|
||||
@@ -1,36 +0,0 @@
|
||||
---
|
||||
aliases: ["Write 10 Pillar Articles"]
|
||||
Is A: Project
|
||||
Belongs to: "[[24q2]]"
|
||||
Advances: "[[responsibility-content-production]]"
|
||||
Status: Done
|
||||
Owner: "[[person-luca-rossi]]"
|
||||
---
|
||||
# Write 10 Pillar Articles
|
||||
|
||||
## Overview
|
||||
|
||||
This project focused on creating 10 long-form "pillar" articles — comprehensive, SEO-optimized pieces targeting high-traffic search terms in the engineering leadership and developer productivity space. These articles serve a dual purpose: they drive organic search traffic to the Refactoring website (feeding [[measure-subscribers]] growth) and establish topical authority that supports [[2024-reach-50k-subscribers]].
|
||||
|
||||
Each article was 2,500-4,000 words, thoroughly researched, and structured for both readability and search engine performance. Topics were selected based on keyword research using Ahrefs, focusing on terms with decent search volume (1K-10K monthly) and moderate competition. The articles were published on the Refactoring blog and cross-promoted through the newsletter over [[24q2]].
|
||||
|
||||
## Goals
|
||||
|
||||
- Research and select 10 high-value keyword targets using Ahrefs
|
||||
- Write 10 articles of 2,500-4,000 words each, with clear structure and internal linking
|
||||
- Optimize each article for on-page SEO (meta descriptions, headers, image alt text)
|
||||
- Publish on a bi-weekly cadence throughout Q2
|
||||
- Achieve first-page Google ranking for at least 3 target keywords within 6 months
|
||||
|
||||
## Key decisions
|
||||
|
||||
- **Evergreen topics only.** All 10 articles target topics that will remain relevant for 2+ years (e.g., "how to run effective 1:1s," "staff engineer vs. engineering manager"). Avoided trending or news-driven topics that would decay quickly.
|
||||
- **Newsletter-first drafts, then expanded for SEO.** Several articles started as popular newsletter issues that were expanded with additional depth, examples, and structure. This reduced research time and ensured the topics had proven audience interest.
|
||||
- **No freelance writers.** Considered outsourcing some articles but decided that the Refactoring voice is too distinctive to delegate at this stage. [[person-sara-ricci]] edited all 10 pieces, which maintained quality while freeing time for drafting.
|
||||
|
||||
## Notes
|
||||
|
||||
- Writing 10 pillar articles in one quarter while maintaining the weekly newsletter was extremely demanding. Weeks 5-8 were particularly brutal — the output quality dipped on newsletter issues during that period. In hindsight, 7-8 articles would have been more sustainable.
|
||||
- The article on "Engineering Manager vs. Tech Lead" became the highest-performing piece, ranking on page 1 within 8 weeks and driving ~3,000 monthly organic visits. This single article accounted for about 30% of the total organic traffic from all 10 pieces.
|
||||
- Internal linking between pillar articles and existing newsletter content created a noticeable boost in site-wide SEO metrics. Average session duration increased by 40% during Q2.
|
||||
- This project directly informed [[topic-content-strategy]] and established the content pillar framework that was later extended in [[25q1-newsletter-seo-sprint]].
|
||||
@@ -1,36 +0,0 @@
|
||||
---
|
||||
aliases: ["Build Podcast Landing Page"]
|
||||
Is A: Project
|
||||
Belongs to: "[[24q2]]"
|
||||
Advances: "[[responsibility-podcast]]"
|
||||
Status: Done
|
||||
Owner: "[[person-luca-rossi]]"
|
||||
---
|
||||
# Build Podcast Landing Page
|
||||
|
||||
## Overview
|
||||
|
||||
After launching the podcast in [[24q1-podcast-season-1]], it became clear that there was no proper home for the show on the web. Episodes were scattered across Apple Podcasts, Spotify, and YouTube, but there was no single page where listeners could browse the archive, read show notes, and subscribe. This project created a dedicated landing page on the Refactoring website to serve as the podcast's canonical home.
|
||||
|
||||
The page needed to work both as a discovery tool for new listeners and as a reference for existing ones. It also needed to support sponsor visibility, since podcast sponsorships were part of the package tiers defined in [[24q1-launch-sponsorship-packages]]. The page was built using the existing site's tech stack (Next.js) and went live in early May.
|
||||
|
||||
## Goals
|
||||
|
||||
- Design and build a podcast landing page with episode archive, show notes, and embedded audio player
|
||||
- Include subscribe CTAs for Apple Podcasts, Spotify, YouTube, and RSS
|
||||
- Add a "Featured Guest" section to highlight notable episodes and drive social proof
|
||||
- Integrate sponsor logos and links for current podcast sponsors
|
||||
- Ensure the page is mobile-responsive and loads quickly (target: <2s LCP)
|
||||
|
||||
## Key decisions
|
||||
|
||||
- **Static generation, not dynamic.** Each episode page is statically generated at build time from a markdown file with frontmatter metadata. This keeps hosting costs zero (Vercel free tier) and ensures fast page loads. New episodes trigger a rebuild via a GitHub webhook.
|
||||
- **Embedded player, not custom.** Used the native Spotify/Apple embed widgets rather than building a custom audio player. The maintenance cost of a custom player was not justified given that most listeners use their preferred podcast app anyway.
|
||||
- **Show notes as full articles.** Rather than just listing bullet points, each episode's show notes page is a full article with key takeaways, timestamps, and links. This gives the pages SEO value and provides genuine utility for listeners who prefer reading to listening.
|
||||
|
||||
## Notes
|
||||
|
||||
- The page design went through two iterations. The first version was too sparse — just a list of episodes with play buttons. [[person-giulia-conti]] suggested adding guest photos and pull quotes, which made the page significantly more engaging.
|
||||
- Getting the embedded players to render correctly across browsers was more annoying than expected. Spotify's embed widget has inconsistent height behavior on Safari. Ended up using a fixed-height iframe with a fallback link.
|
||||
- The landing page became the default link shared in the newsletter for podcast promotion, replacing direct Spotify/Apple links. This centralized analytics tracking through [[measure-podcast-downloads]] and gave better visibility into listener behavior.
|
||||
- Page traffic is modest (~500 unique visitors/month) but steady, and it ranks for several long-tail podcast-related keywords. The SEO investment in show notes is paying off gradually.
|
||||
@@ -1,36 +0,0 @@
|
||||
---
|
||||
aliases: ["Hire Editor (Sara)"]
|
||||
Is A: Project
|
||||
Belongs to: "[[24q2]]"
|
||||
Advances: "[[responsibility-team-management]]"
|
||||
Status: Done
|
||||
Owner: "[[person-luca-rossi]]"
|
||||
---
|
||||
# Hire Editor (Sara)
|
||||
|
||||
## Overview
|
||||
|
||||
By mid-2024, writing the weekly newsletter, managing sponsorships, recording podcast episodes, and handling business operations was becoming unsustainable as a solo operation. The most impactful hire at this stage was an editor — someone who could elevate the writing quality, reduce revision cycles, and free up time for higher-leverage activities like strategy and guest relationships.
|
||||
|
||||
After a lightweight hiring process, [[person-sara-ricci]] joined as a part-time editor in May 2024. Sara had previous experience editing technical content for developer publications, which was critical — engineering leadership content requires an editor who understands the domain, not just grammar. This hire directly supported scaling [[responsibility-content-production]] and was a prerequisite for hitting [[2024-reach-50k-subscribers]].
|
||||
|
||||
## Goals
|
||||
|
||||
- Define the editor role: scope, hours, compensation, and working relationship
|
||||
- Source and evaluate 5-10 candidates through network referrals and job boards
|
||||
- Run a paid trial edit with top 3 candidates using a real newsletter draft
|
||||
- Onboard the selected editor with style guide, editorial calendar, and tooling access
|
||||
- Achieve a steady-state workflow where drafts go through one editorial pass before publish
|
||||
|
||||
## Key decisions
|
||||
|
||||
- **Part-time contractor, not full-time employee.** The workload did not justify a full-time hire yet, and contractor status keeps the arrangement flexible as the business scales. Sara works approximately 15 hours/week.
|
||||
- **Paid trial before commitment.** Rather than hiring based on interviews alone, asked the top 3 candidates to edit the same newsletter draft. This revealed huge differences in editorial judgment that interviews could not surface. Sara's edits were the most substantive — she restructured sections and challenged weak arguments, not just fixed typos.
|
||||
- **Editor has veto power on publish readiness.** Gave Sara explicit authority to push back on drafts that are not ready. This was uncomfortable at first but has significantly improved quality. If Sara says it needs another pass, it gets another pass.
|
||||
|
||||
## Notes
|
||||
|
||||
- The hiring process took about 4 weeks from posting to signed contract. Received ~30 applications, shortlisted 8, did calls with 5, trial edits with 3. The trial edit step was by far the most valuable signal.
|
||||
- Biggest adjustment was learning to write "editor-friendly" first drafts — meaning rough structure and arguments are solid, but prose is intentionally unpolished. Trying to write perfectly on the first pass and then having an editor change it creates friction. Better to write fast and let Sara shape the prose.
|
||||
- Sara also took on podcast show notes editing, which was not in the original scope but made sense as her familiarity with the content grew. This expanded into the workflow documented in [[procedure-podcast-editing]].
|
||||
- [[person-matteo-cellini]] recommended Sara — they had worked together at a previous company. Network referrals continue to be the best hiring channel for roles that require domain expertise.
|
||||
@@ -1,36 +0,0 @@
|
||||
---
|
||||
aliases: ["Set Up Sponsor CRM"]
|
||||
Is A: Project
|
||||
Belongs to: "[[24q2]]"
|
||||
Advances: "[[responsibility-sponsorships]]"
|
||||
Status: Done
|
||||
Owner: "[[person-luca-rossi]]"
|
||||
---
|
||||
# Set Up Sponsor CRM
|
||||
|
||||
## Overview
|
||||
|
||||
With sponsorship deals increasing after [[24q1-launch-sponsorship-packages]], tracking outreach, deal status, invoicing, and renewals in a spreadsheet was no longer viable. This project built a proper CRM system in Airtable to manage the full sponsor lifecycle — from initial outreach through booking, fulfillment, reporting, and renewal.
|
||||
|
||||
The CRM needed to support [[procedure-quarterly-sponsor-outreach]] and [[procedure-sponsor-onboarding]] while being lightweight enough for a one-person sales operation (with occasional help from [[person-matteo-cellini]] on outreach). The system went live in April 2024 and has been the backbone of sponsor management since, directly supporting [[2024-double-revenue]].
|
||||
|
||||
## Goals
|
||||
|
||||
- Design an Airtable base with tables for Companies, Contacts, Deals, Invoices, and Placements
|
||||
- Build views for pipeline management (kanban), upcoming renewals, and revenue tracking
|
||||
- Set up automations for deal stage notifications and renewal reminders (30 days before expiry)
|
||||
- Import existing sponsor data from the old spreadsheet (~15 companies, ~25 deals)
|
||||
- Document the CRM workflow and train [[person-matteo-cellini]] on outreach tracking
|
||||
|
||||
## Key decisions
|
||||
|
||||
- **Airtable, not a dedicated CRM tool.** Evaluated HubSpot, Pipedrive, and Notion. HubSpot was overkill (and expensive) for ~50 deals/year. Pipedrive was close but lacked the flexibility for tracking placement fulfillment. Airtable's combination of structured data, views, and automations hit the sweet spot for this scale.
|
||||
- **Deal-centric model, not contact-centric.** The primary entity is the Deal (a specific sponsorship booking), not the Contact or Company. This reflects the actual workflow — most actions are about advancing a specific deal, not managing a relationship in the abstract.
|
||||
- **Automated renewal reminders at 30 and 7 days.** Renewal is the highest-leverage moment in the sponsor lifecycle. Automating reminders ensures no renewal slips through the cracks, which was happening with the spreadsheet system.
|
||||
|
||||
## Notes
|
||||
|
||||
- Migrating from the spreadsheet exposed several data quality issues — duplicate companies, inconsistent naming, missing invoice dates. Cleaning this up took a full day but was worth it for a clean foundation.
|
||||
- The kanban view for pipeline management became the default "daily check" for sponsor operations. Being able to see all deals by stage (Prospect, Outreach, Negotiation, Booked, Fulfilled, Renewed) at a glance changed how proactive the outreach process feels.
|
||||
- Airtable's automation limits on the free plan were a constraint. Upgraded to the Team plan ($20/month) to get enough automation runs. The cost is trivial relative to the revenue it manages.
|
||||
- [[measure-sponsorship-mrr]] is now calculated directly from the CRM data, which eliminated the manual reconciliation that was happening before. Revenue reporting went from a quarterly headache to an always-current dashboard.
|
||||
@@ -1,37 +0,0 @@
|
||||
---
|
||||
aliases: ["Spring Gran Fondo 2024"]
|
||||
Is A: Project
|
||||
Belongs to: "[[24q2]]"
|
||||
Advances: "[[responsibility-health-fitness]]"
|
||||
Status: Done
|
||||
Owner: "[[person-luca-rossi]]"
|
||||
---
|
||||
# Spring Gran Fondo 2024
|
||||
|
||||
## Overview
|
||||
|
||||
This was the first of two target events for 2024, as planned in [[24q1-plan-cycling-season]]. The Granfondo di Varese — 130km with 2,200m of elevation gain — served as both a fitness benchmark and a motivational milestone for the spring training block. The event was scheduled for late May, giving a solid 12-week build phase from March through mid-May.
|
||||
|
||||
Preparation included structured interval training (3 sessions/week), long weekend rides (4-5 hours), and two recon rides on sections of the actual course. The goal was not to race competitively but to complete the full route in under 5 hours and 30 minutes, maintaining good form throughout. This project directly advanced [[2024-complete-two-gran-fondos]].
|
||||
|
||||
## Goals
|
||||
|
||||
- Complete the Granfondo di Varese full route (130km, 2,200m elevation) in under 5:30
|
||||
- Execute the 12-week build training plan with at least 80% adherence
|
||||
- Complete two course recon rides to familiarize with key climbs and descents
|
||||
- Maintain race weight (72kg) through the build phase
|
||||
- Log all training data for post-event analysis via [[measure-cycling-km-per-month]]
|
||||
|
||||
## Key decisions
|
||||
|
||||
- **Full route, not the medio.** The event offered a 90km "medio" option. Chose the full 130km because the training plan was designed for it, and the shorter route would not provide a meaningful fitness test. If the legs failed on race day, it would be better to know that at full distance.
|
||||
- **Conservative pacing strategy.** Rather than going hard on the first climb (which is where most amateurs blow up), planned to ride the first 60km at 75% of threshold power and increase effort in the second half. This negative-split approach is less exciting but far more reliable.
|
||||
- **No travel the day before.** Drove to Varese two days before the event to avoid travel fatigue. Stayed at a simple hotel near the start line. The extra cost was worth the better sleep and relaxed morning.
|
||||
|
||||
## Notes
|
||||
|
||||
- Finished in 5:17, which was well within the target. The conservative pacing strategy worked perfectly — felt strong on the final climb while many riders around were walking. Negative split confirmed: second half was 3 minutes faster than the first.
|
||||
- Training adherence was about 85%, which was above target. The two weeks lost to a mild cold in April were compensated by extending the build phase slightly and cutting one recovery week short.
|
||||
- [[person-paco-furiani]] rode the event as well and finished about 20 minutes ahead. His feedback on nutrition strategy (eating every 30 minutes from the start, not waiting until hungry) was a game-changer. Applied the same approach going forward.
|
||||
- Biggest lesson: the mental game matters more than fitness above a certain threshold. The climbs at km 90-110 were physically manageable but mentally draining. Having a clear pacing target on the bike computer helped maintain focus.
|
||||
- Post-event recovery took about 10 days before training resumed. This feeds into preparation for the autumn event, which was later scoped as a separate project.
|
||||
@@ -1,35 +0,0 @@
|
||||
---
|
||||
aliases: ["Vibe-coding a Stock Screener"]
|
||||
Is A: Experiment
|
||||
Belongs to: "[[24q2]]"
|
||||
Status: Done
|
||||
Owner: "[[person-luca-rossi]]"
|
||||
---
|
||||
# Vibe-coding a Stock Screener
|
||||
|
||||
Built a stock screener in Python over a weekend to test whether a simple EMA bounce strategy could surface actionable trade setups on US equities. The broader motivation was to explore whether personal tooling for investment analysis could complement the [[procedure-monthly-portfolio-review]] and eventually improve [[measure-net-worth]] trajectory.
|
||||
|
||||
## Hypothesis
|
||||
|
||||
A lightweight, self-built screener using exponential moving average crossovers would surface 2-3 viable swing trade candidates per week, outperforming manual chart scanning in both speed and consistency.
|
||||
|
||||
## Setup
|
||||
|
||||
- Wrote a Python script pulling daily OHLCV data from Yahoo Finance for the S&P 500 universe.
|
||||
- Implemented a 21/50 EMA bounce filter: flag stocks where price touches the 21 EMA from above, with the 50 EMA still trending up.
|
||||
- Ran the screener nightly via cron for 4 weeks, logging all flagged tickers and tracking outcomes over 10-day windows.
|
||||
- Compared results against manual chart review done for the same period.
|
||||
|
||||
## Results
|
||||
|
||||
- The screener flagged an average of 5 candidates per day (higher than expected).
|
||||
- Roughly 40% of flagged setups produced a positive 10-day return above 2%.
|
||||
- Manual chart review caught about 60% of the same setups, but took 3x longer.
|
||||
- False positives were mostly in low-volume mid-caps where the bounce pattern was noise.
|
||||
|
||||
## Takeaways
|
||||
|
||||
- Vibe-coding a quick tool like this is a high-leverage weekend project. Total effort was about 8 hours.
|
||||
- The EMA bounce strategy has signal, but needs volume and volatility filters to reduce false positives.
|
||||
- Automating the screener freed up time previously spent on manual scanning for the [[procedure-monthly-portfolio-review]].
|
||||
- Worth maintaining as a personal tool. Not worth productizing, but could become a newsletter content piece on [[topic-personal-finance]].
|
||||
@@ -1,36 +0,0 @@
|
||||
---
|
||||
aliases: ["Video Format Experiment"]
|
||||
Is A: Experiment
|
||||
Belongs to: "[[24q2]]"
|
||||
Status: Abandoned
|
||||
Owner: "[[person-luca-rossi]]"
|
||||
---
|
||||
# Video Format Experiment
|
||||
|
||||
Tested whether short-form video (3-5 minute explainers) could work as a distribution channel alongside the newsletter. The idea was to repurpose existing [[responsibility-content-production]] into video format to reach audiences who prefer visual content, potentially feeding [[measure-subscribers]] growth from a new channel.
|
||||
|
||||
## Hypothesis
|
||||
|
||||
Publishing 2 short technical explainer videos per week on YouTube and Twitter would generate at least 500 new newsletter subscribers over 6 weeks, with a production cost under 3 hours per video once a workflow was established.
|
||||
|
||||
## Setup
|
||||
|
||||
- Produced 3 videos covering topics already published as newsletter essays: system design patterns, engineering management tips, and a career growth framework.
|
||||
- Used screen recording with voiceover (no face camera) to minimize production overhead.
|
||||
- Published on YouTube Shorts and Twitter/X with a newsletter CTA in the description and pinned comment.
|
||||
- Tracked view counts, click-through to newsletter signup, and time spent per video.
|
||||
|
||||
## Results
|
||||
|
||||
- Total views across 3 videos: ~4,200 (mostly Twitter, YouTube was negligible).
|
||||
- Newsletter signups attributed to video: 23 total.
|
||||
- Average production time: 5-6 hours per video (scripting, recording, editing, captioning).
|
||||
- The time-to-subscriber ratio was roughly 15x worse than writing a newsletter essay.
|
||||
|
||||
## Takeaways
|
||||
|
||||
- Video production cost was significantly underestimated. Editing and captioning alone took 2+ hours per video.
|
||||
- The audience overlap between short-form video viewers and newsletter subscribers appears small.
|
||||
- Abandoned after 3 videos. The ROI does not justify the effort given current team size and [[responsibility-content-production]] bandwidth.
|
||||
- If revisited, would need a dedicated video editor or a fundamentally simpler format (e.g., talking head with no editing).
|
||||
- Better to double down on written content and [[topic-newsletter-growth]] strategies that already work.
|
||||
@@ -1,32 +0,0 @@
|
||||
---
|
||||
aliases: ["Q2 2024"]
|
||||
Is A: Quarter
|
||||
Created at: "2024-04-01"
|
||||
Belongs to: "[[2024]]"
|
||||
Has: ["[[24q2-hire-editor]]", "[[24q2-build-podcast-landing-page]]", "[[24q2-10-pillar-articles]]", "[[24q2-spring-gran-fondo]]", "[[24q2-sponsor-crm]]"]
|
||||
Status: Done
|
||||
---
|
||||
# Q2 2024
|
||||
|
||||
## Focus
|
||||
|
||||
Q2 was the execution quarter. With the foundations from Q1 in place, the focus shifted to scaling content production and building systems that could run without me doing everything. Hiring an editor was the biggest unlock. On the cycling side, Nove Colli was the target — months of base training coming to a head.
|
||||
|
||||
## Highlights
|
||||
|
||||
- [[24q2-hire-editor]] completed — brought on a freelance editor who now handles first-pass editing on every newsletter issue, saving me 4-5 hours per week
|
||||
- Published [[24q2-10-pillar-articles]]: long-form pieces on topics like "Staff Engineer vs. Manager" and "Technical Debt Prioritization" — two of them hit 50k+ views
|
||||
- Built and launched [[24q2-build-podcast-landing-page]] with episode archive, show notes, and Apple/Spotify links — podcast downloads crossed 5k/month
|
||||
- Completed Nove Colli ([[24q2-spring-gran-fondo]]) in 7h42m — not my best time, but finished strong on the climbs
|
||||
- Shipped [[24q2-sponsor-crm]] in Notion — tracking pipeline, renewal dates, and revenue per sponsor in one place
|
||||
- Newsletter hit 42k subscribers by end of June, right on track for the [[2024-reach-50k-subscribers]] goal
|
||||
- [[person-paco-furiani]] started handling more operational tasks — invoicing, sponsor logistics, calendar management
|
||||
- [[measure-sponsorship-mrr]] reached EUR 8.2k, up from EUR 5.5k at start of year
|
||||
|
||||
## Reflections
|
||||
|
||||
This was the quarter where I started to feel the leverage of having a small team. With [[person-matteo-cellini]] handling sponsor relationships and [[person-paco-furiani]] taking on operations, I could actually focus on writing and recording. The editor hire was transformative — I went from spending Sunday evenings frantically editing to having a polished draft waiting for me on Monday morning.
|
||||
|
||||
The pillar articles strategy paid off more than expected. Instead of chasing weekly topics, I invested in fewer, deeper pieces that could rank on search and get shared. "Staff Engineer vs. Manager" alone brought in 800+ new subscribers organically. This shifted my thinking about content strategy: depth over frequency.
|
||||
|
||||
Nove Colli was harder than I expected. The heat in Romagna in late May was brutal, and I bonked on the fourth climb. But crossing the finish line in Cesenatico — exhausted, sunburned, grinning — reminded me why I do this. Already looking forward to Maratona in July. The cycling and the business run on the same fuel: consistent effort over months, then showing up on race day. [[2024-complete-two-gran-fondos]] is halfway done.
|
||||
@@ -1,36 +0,0 @@
|
||||
---
|
||||
aliases: ["Speak at Codemotion Milan"]
|
||||
Is A: Project
|
||||
Belongs to: "[[24q3]]"
|
||||
Advances: "[[responsibility-content-production]]"
|
||||
Status: Done
|
||||
Owner: "[[person-luca-rossi]]"
|
||||
---
|
||||
# Speak at Codemotion Milan
|
||||
|
||||
## Overview
|
||||
|
||||
Codemotion Milan 2024 invited [[person-luca-rossi]] to deliver a 30-minute talk on growing a newsletter business as a technical founder. The talk, titled "From Side Project to 50K Subscribers: Building a Content Business in Public," covered the Refactoring growth story, key inflection points, and practical lessons for engineers considering content as a career path or side business.
|
||||
|
||||
This was the first major conference speaking engagement and represented an important step in building personal brand credibility beyond the newsletter audience. Conference talks drive a different kind of trust than written content — they put a face and voice to the brand, which strengthens both subscriber acquisition and sponsor confidence. The talk was well-received and led to several valuable connections, including two future podcast guests.
|
||||
|
||||
## Goals
|
||||
|
||||
- Write and rehearse a 30-minute talk with slides
|
||||
- Target key themes: newsletter economics, audience building, and the engineering-to-content pipeline
|
||||
- Deliver the talk at Codemotion Milan in October 2024
|
||||
- Record the talk for repurposing as a YouTube video and newsletter content
|
||||
- Generate at least 200 new newsletter subscribers from the event
|
||||
|
||||
## Key decisions
|
||||
|
||||
- **Story-driven structure, not a how-to.** Rather than a generic "10 tips for growing a newsletter" format, structured the talk as a chronological narrative of the Refactoring journey. Stories are more memorable and more authentic than advice lists.
|
||||
- **Include real numbers.** Shared actual subscriber counts, revenue milestones, and open rates. Transparency is a core Refactoring value and it differentiates the talk from vague "I grew my audience" presentations. This decision was slightly uncomfortable but generated the most positive feedback.
|
||||
- **No live demo.** Considered showing the newsletter creation process live but decided the risk of technical issues on stage was not worth it. Used screenshots and short video clips instead.
|
||||
|
||||
## Notes
|
||||
|
||||
- Preparation took about 3 weeks of part-time work — mostly slide design and rehearsal. Rehearsed the full talk 5 times, including twice in front of [[person-sara-ricci]] and [[person-matteo-cellini]] who gave tough but useful feedback on pacing and clarity.
|
||||
- The audience was about 300 people, skewing toward mid-career developers and engineering managers. Q&A was lively — the most common question was about time management (how to write a weekly newsletter while also doing other work). This became a topic for a future newsletter issue.
|
||||
- Post-talk networking led to meeting [[person-emma-wilson]] and [[person-david-kim]], both of whom later appeared as podcast guests in [[24q3-podcast-season-2]] and [[25q2-podcast-season-3]] respectively.
|
||||
- The talk recording was published on YouTube and performed modestly (~2,500 views in 3 months), but the subscriber spike from the event itself was about 180 — slightly below the 200 target but still a strong result for a single event. The real value was in credibility and relationship building, not direct conversion.
|
||||
@@ -1,36 +0,0 @@
|
||||
---
|
||||
aliases: ["Morning Journaling Habit"]
|
||||
Is A: Experiment
|
||||
Belongs to: "[[24q3]]"
|
||||
Status: Done
|
||||
Owner: "[[person-luca-rossi]]"
|
||||
---
|
||||
# Morning Journaling Habit
|
||||
|
||||
Ran an 8-week experiment with daily morning journaling to evaluate its impact on focus, decision-making clarity, and overall stress management. This ties into the broader theme of personal operating systems: if running a content business and training for cycling events simultaneously, mental bandwidth becomes the bottleneck, and journaling is often cited as a way to decompress and prioritize.
|
||||
|
||||
## Hypothesis
|
||||
|
||||
Writing 10-15 minutes each morning (freeform, no template) would reduce decision fatigue and improve weekly [[measure-task-completion-rate]] by at least 10%, while also providing a subjective sense of reduced stress.
|
||||
|
||||
## Setup
|
||||
|
||||
- Journaled every morning for 8 weeks using a plain markdown file in the vault.
|
||||
- No rigid template: some days were gratitude lists, some were brain dumps, some were planning sessions.
|
||||
- Tracked adherence (did I journal today? yes/no), subjective stress rating (1-5), and weekly task completion rate.
|
||||
- Weeks 1-4 were during a high-stress period (podcast launch prep for [[24q3-podcast-season-2]] and newsletter deadlines). Weeks 5-8 were calmer.
|
||||
|
||||
## Results
|
||||
|
||||
- Adherence: 85% in weeks 1-4 (high stress), dropped to 55% in weeks 5-8 (low stress).
|
||||
- Subjective stress rating improved by ~0.8 points on average during weeks 1-4.
|
||||
- Task completion rate showed no statistically meaningful change (too many confounding variables).
|
||||
- The most useful entries were "decision journals" where a specific choice was weighed explicitly.
|
||||
|
||||
## Takeaways
|
||||
|
||||
- Morning journaling is most valuable during high-stress, high-decision-load periods. It acts as a pressure valve.
|
||||
- During calm stretches, the habit felt forced and adherence dropped naturally. This is fine; it does not need to be daily forever.
|
||||
- Decision journals (structured: "What am I deciding? What are the options? What do I fear?") were the highest-signal format.
|
||||
- Will keep journaling as a situational tool rather than a rigid daily habit. Likely to resurface during intense project sprints like [[25q1-laputa-v1]].
|
||||
- No direct impact on [[measure-task-completion-rate]], but the qualitative clarity benefit is real.
|
||||
@@ -1,37 +0,0 @@
|
||||
---
|
||||
aliases: ["Expand Sponsor Verticals"]
|
||||
Is A: Project
|
||||
Belongs to: "[[24q3]]"
|
||||
Advances: "[[responsibility-sponsorships]]"
|
||||
Status: Done
|
||||
Owner: "[[person-luca-rossi]]"
|
||||
---
|
||||
# Expand Sponsor Verticals
|
||||
|
||||
## Overview
|
||||
|
||||
The first two quarters of sponsorship revenue came primarily from developer tools (CI/CD, monitoring, testing platforms). While this vertical was reliable, concentration risk was high — losing one or two devtools sponsors could significantly impact [[measure-sponsorship-mrr]]. This project expanded the sponsor base into three new verticals: cloud infrastructure, AI/ML platforms, and developer education.
|
||||
|
||||
Expanding verticals required adapting the outreach messaging and media kit to resonate with different buyer personas. Cloud infra companies care about enterprise decision-makers in the audience; AI/ML companies want to reach early adopters; education companies want ambitious individual developers. [[person-matteo-cellini]] led much of the outreach effort, using the CRM built in [[24q2-sponsor-crm]] to manage the pipeline. This project was critical for [[2024-double-revenue]].
|
||||
|
||||
## Goals
|
||||
|
||||
- Identify 30+ target companies across cloud infrastructure, AI/ML, and developer education verticals
|
||||
- Adapt the media kit with vertical-specific audience data and case studies
|
||||
- Run targeted outreach campaigns for each vertical (10+ companies each)
|
||||
- Close at least 5 new sponsors from the new verticals by end of [[24q3]]
|
||||
- Achieve vertical diversification: no single vertical should represent more than 40% of sponsorship revenue
|
||||
|
||||
## Key decisions
|
||||
|
||||
- **Three verticals, not more.** Considered also targeting recruitment platforms and SaaS tools, but decided to focus on three verticals where the audience fit was strongest. Spreading too thin would dilute outreach quality.
|
||||
- **Vertical-specific case studies.** Rather than using a generic media kit, created tailored one-pagers for each vertical showing relevant audience segments. For example, the cloud infra pitch highlighted the percentage of readers who are engineering managers or directors (the buying decision-makers for infrastructure).
|
||||
- **Higher pricing for AI/ML vertical.** AI/ML companies had larger marketing budgets and higher urgency to reach developers. Tested a 20% price premium on the Deep Dive tier for this vertical, and it held without pushback.
|
||||
|
||||
## Notes
|
||||
|
||||
- Cloud infrastructure was the easiest vertical to crack — many companies were already familiar with newsletter sponsorships and had established budgets for developer marketing. Closed 3 deals in this vertical within 6 weeks.
|
||||
- AI/ML was more volatile — companies had budget but were less predictable. Two deals fell through after verbal commitment because of budget freezes. The companies that did close were excellent sponsors with high renewal rates.
|
||||
- Developer education was harder than expected. Many education companies preferred affiliate/commission models over flat-rate sponsorships. Only closed 1 deal in this vertical during Q3, though it renewed for 3 months.
|
||||
- By end of Q3, vertical distribution was: devtools 35%, cloud infra 30%, AI/ML 25%, education 10%. This was a significant improvement from the previous 85% devtools concentration.
|
||||
- The outreach templates developed during this project became part of [[procedure-quarterly-sponsor-outreach]] and continue to be used with minor updates.
|
||||
@@ -1,37 +0,0 @@
|
||||
---
|
||||
aliases: ["Podcast Season 2"]
|
||||
Is A: Project
|
||||
Belongs to: "[[24q3]]"
|
||||
Advances: "[[responsibility-podcast]]"
|
||||
Status: Done
|
||||
Owner: "[[person-luca-rossi]]"
|
||||
---
|
||||
# Podcast Season 2
|
||||
|
||||
## Overview
|
||||
|
||||
Building on the momentum from [[24q1-podcast-season-1]], Season 2 expanded the podcast to 8 episodes with a focused theme on engineering leadership and organizational design. The season explored how high-performing engineering teams are structured, how technical leaders make decisions, and the human dynamics that make or break engineering organizations.
|
||||
|
||||
The production workflow was significantly smoother this time — [[person-sara-ricci]] handled all post-production editing using the [[procedure-podcast-editing]] workflow established during Season 1, and [[person-paco-furiani]] helped with guest research and outreach. The landing page built in [[24q2-build-podcast-landing-page]] served as the canonical home for all episodes. Season 2 launched in July and ran through September.
|
||||
|
||||
## Goals
|
||||
|
||||
- Record and publish 8 episodes on engineering leadership and org design
|
||||
- Secure higher-profile guests (target: at least 2 guests with 10K+ Twitter following)
|
||||
- Grow average episode downloads to 2,000 (up from Season 1's ~1,500 average)
|
||||
- Introduce a podcast sponsorship slot (mid-roll read) aligned with newsletter sponsor packages
|
||||
- Improve audio quality: all episodes recorded on Riverside.fm with backup local recordings
|
||||
|
||||
## Key decisions
|
||||
|
||||
- **Themed season with a narrative arc.** Rather than random topics, structured Season 2 around a progression: from individual contributor leadership (episodes 1-2) to team dynamics (3-4) to org design (5-6) to executive leadership (7-8). This gave the season a cohesive feel and encouraged binge listening.
|
||||
- **Mid-roll sponsor read, not pre-roll.** Tested sponsor placements and found that mid-roll reads (inserted at a natural break around the 15-minute mark) had significantly better recall and click-through than pre-roll. This aligned with the sponsorship packages from [[24q1-launch-sponsorship-packages]].
|
||||
- **No video this season.** Dropped the video component entirely to reduce production overhead. The video clips from Season 1 generated minimal YouTube traction, and the effort-to-impact ratio was poor. Refocused entirely on audio quality.
|
||||
|
||||
## Notes
|
||||
|
||||
- Guest booking was easier in Season 2 — having a published Season 1 with real download numbers made outreach emails far more credible. Response rate improved from ~15% to ~30%.
|
||||
- [[person-emma-wilson]] (VP Engineering at a Series C startup) was the standout guest — her episode on "Building Engineering Culture from Scratch" became the most downloaded episode of the season (~3,200 downloads) and was widely shared on LinkedIn.
|
||||
- Average downloads per episode reached 2,100, exceeding the 2,000 target. The newsletter cross-promotion and improved show notes SEO from [[24q2-build-podcast-landing-page]] were the primary growth drivers.
|
||||
- The podcast sponsorship slot generated about $1,500 in additional monthly revenue during the season, which validated the channel as a revenue stream alongside newsletter sponsorships.
|
||||
- [[measure-podcast-downloads]] showed a clear pattern: downloads spike on newsletter send days and taper gradually. This confirms that the newsletter audience is the primary distribution channel for the podcast.
|
||||
@@ -1,37 +0,0 @@
|
||||
---
|
||||
aliases: ["Launch Premium Newsletter Tier"]
|
||||
Is A: Project
|
||||
Belongs to: "[[24q3]]"
|
||||
Advances: "[[responsibility-grow-newsletter]]"
|
||||
Status: Abandoned
|
||||
Owner: "[[person-luca-rossi]]"
|
||||
---
|
||||
# Launch Premium Newsletter Tier
|
||||
|
||||
## Overview
|
||||
|
||||
This project explored adding a paid subscription tier to the Refactoring newsletter. The hypothesis was that a segment of the audience would pay $10/month for premium content — deeper case studies, exclusive interviews, and a private community. If successful, this would create a second revenue stream alongside sponsorships and reduce dependency on a single monetization model.
|
||||
|
||||
The premium tier launched in mid-July with a 2-week free trial. After 6 weeks of operation, conversion rates were significantly below projections (0.3% vs. the 2% target), and the incremental content burden was unsustainable alongside the free newsletter and podcast. The project was abandoned in early September. While the outcome was a clear negative, the experiment provided valuable data about the audience's willingness to pay and the true cost of premium content production.
|
||||
|
||||
## Goals
|
||||
|
||||
- Design the premium tier: pricing, content cadence, and exclusive features
|
||||
- Set up Substack paid subscriptions with a 2-week free trial
|
||||
- Create 4 premium-only deep dives to serve as the initial content library
|
||||
- Acquire 200 paying subscribers within the first 6 weeks
|
||||
- Evaluate viability: target a sustainable $2,000/month from paid subscriptions
|
||||
|
||||
## Key decisions
|
||||
|
||||
- **$10/month, not $5 or $15.** Benchmarked against similar engineering newsletters. $5 felt too cheap to justify the effort; $15 was above what comparable newsletters charged. $10 was the market rate, but it turned out the audience was not price-sensitive — they simply did not want to pay for content that was perceived as similar to the free tier.
|
||||
- **Deep dives as the core premium offering.** Rather than gating regular newsletter issues, created separate premium-only deep dives (3,000-5,000 words). This preserved the free newsletter's value while offering something genuinely different. The problem was that the deep dives took 8-10 hours each to write, making the unit economics terrible at low subscriber counts.
|
||||
- **Abandoned rather than pivoted.** Considered pivoting to a community-only premium tier (no premium content, just Discord access), but decided the timing was wrong. The audience was not large enough to sustain a vibrant paid community. Revisiting community as a concept in [[25q3-community-launch]] with a different approach.
|
||||
|
||||
## Notes
|
||||
|
||||
- The 0.3% conversion rate was the clearest signal. Industry benchmarks for engineering newsletters suggest 1-3% is typical. Being at 0.3% meant the value proposition was fundamentally off, not just undermarketed.
|
||||
- The 60 subscribers who did convert were highly engaged and gave positive feedback. Several said they subscribed to "support the newsletter" rather than for the premium content itself. This suggests a patronage model (like Patreon) might work better than a content-gated model.
|
||||
- The biggest cost was not financial but attentional. Writing premium deep dives while maintaining the free newsletter, podcast, and sponsor obligations created unsustainable pressure during July-August. [[person-sara-ricci]] flagged this early and was right.
|
||||
- Key lesson: for a sponsorship-funded newsletter, adding paid subscriptions creates a conflict. Free reach drives sponsor value; gating content behind a paywall reduces reach. The two models work against each other unless the audience is very large. Better to focus on growing [[measure-subscribers]] and increasing sponsor rates.
|
||||
- This experiment informed the decision to try [[25q1-paid-newsletter-trial]] with a completely different approach (annual pricing, bundled with podcast content).
|
||||
@@ -1,37 +0,0 @@
|
||||
---
|
||||
aliases: ["Summer Reading Sprint"]
|
||||
Is A: Project
|
||||
Belongs to: "[[24q3]]"
|
||||
Advances: "[[responsibility-learning]]"
|
||||
Status: Done
|
||||
Owner: "[[person-luca-rossi]]"
|
||||
---
|
||||
# Summer Reading Sprint
|
||||
|
||||
## Overview
|
||||
|
||||
July and August are traditionally lighter months for the newsletter (sponsors pause, audience engagement dips during European summer holidays), which creates space for deeper reading. This project set an intentional reading goal of 6 books in two months, focused on history, business, and philosophy of science — topics that inform the newsletter's intellectual depth without being directly "content research."
|
||||
|
||||
The sprint contributed to [[2024-read-24-books]] and was structured around dedicated morning reading blocks (7:00-8:30 AM, before work) and longer sessions during weekends. The mix of topics was deliberate: history for perspective, business for practical frameworks, and philosophy of science for clearer thinking about complex systems.
|
||||
|
||||
## Goals
|
||||
|
||||
- Read 6 books in July and August (3 per month)
|
||||
- Topic mix: 2 history, 2 business, 2 philosophy of science
|
||||
- Write brief reading notes for each book in the vault
|
||||
- Identify at least 3 ideas from the reading that can be developed into newsletter issues
|
||||
- Maintain the reading habit: minimum 45 minutes per day, 6 days per week
|
||||
|
||||
## Key decisions
|
||||
|
||||
- **Curated list, not discovery-driven.** Selected all 6 books before the sprint began rather than picking the next book after finishing one. This avoided the decision fatigue that usually slows down reading between books. The list was finalized in late June.
|
||||
- **Physical books only.** Committed to reading physical copies rather than Kindle during the sprint. The tactile experience and absence of screen distractions improved focus and retention. Kindle is fine for casual reading, but for a concentrated sprint, physical books won.
|
||||
- **Notes in the vault, not Goodreads.** All reading notes written as markdown files in the personal vault rather than on Goodreads or a public platform. The notes are for personal reference and newsletter idea generation, not social signaling.
|
||||
|
||||
## Notes
|
||||
|
||||
- Completed 5 of 6 books (83% completion). The sixth book (a dense history of the Scientific Revolution) was about 60% finished by end of August and completed in early September. Close enough to call it a success.
|
||||
- The standout read was "The Innovator's Dilemma" (re-read) — it directly influenced thinking about the newsletter's competitive dynamics and informed two Q4 newsletter issues on disruption in developer tools.
|
||||
- Morning reading blocks worked exceptionally well. The consistency of a fixed time slot made the habit nearly automatic by the third week. This approach has been maintained beyond the sprint.
|
||||
- [[person-marco-bianchi]] recommended "The Structure of Scientific Revolutions" (Kuhn), which turned out to be the most intellectually challenging and rewarding book of the sprint. The concept of paradigm shifts maps surprisingly well onto how engineering organizations evolve.
|
||||
- Three newsletter issues in Q4 drew directly from the reading: one on Christensen's disruption theory applied to DevOps tools, one on historical patterns in technology adoption, and one on mental models from philosophy of science. The reading-to-content pipeline works, but with a 2-3 month delay.
|
||||
@@ -1,32 +0,0 @@
|
||||
---
|
||||
aliases: ["Q3 2024"]
|
||||
Is A: Quarter
|
||||
Created at: "2024-07-01"
|
||||
Belongs to: "[[2024]]"
|
||||
Has: ["[[24q3-premium-tier]]", "[[24q3-codemotion-talk]]", "[[24q3-summer-reading-sprint]]", "[[24q3-podcast-season-2]]", "[[24q3-new-sponsor-verticals]]"]
|
||||
Status: Done
|
||||
---
|
||||
# Q3 2024
|
||||
|
||||
## Focus
|
||||
|
||||
Q3 was about diversifying revenue and expanding reach. The premium tier launch was the biggest bet — adding a paid subscription layer on top of the free newsletter. I also committed to a speaking slot at Codemotion and kicked off Season 2 of the podcast. On the personal side, summer meant a reading sprint and Maratona dles Dolomites.
|
||||
|
||||
## Highlights
|
||||
|
||||
- Launched [[24q3-premium-tier]] with deep-dive case studies, templates, and a monthly AMA — 320 paid subscribers in the first 6 weeks
|
||||
- Delivered [[24q3-codemotion-talk]] on "Scaling Engineering Culture" to ~400 attendees — led to 3 inbound sponsor inquiries and a burst of new subscribers
|
||||
- Completed [[24q3-summer-reading-sprint]]: read 8 books in July-August including "An Elegant Puzzle", "Team Topologies", and "The Ride of a Lifetime"
|
||||
- Recorded and shipped [[24q3-podcast-season-2]] (episodes 13-24) — introduced guest interviews, with [[person-sara-ricci]] helping coordinate guests
|
||||
- Opened [[24q3-new-sponsor-verticals]] targeting DevOps tooling and cloud platforms, expanding beyond the original developer tools niche
|
||||
- Newsletter hit 46k subscribers — [[2024-reach-50k-subscribers]] within reach
|
||||
- Completed Maratona dles Dolomites in 9h15m — [[2024-complete-two-gran-fondos]] achieved
|
||||
- [[measure-podcast-downloads]] reached 8k/month, nearly 4x since launch
|
||||
|
||||
## Reflections
|
||||
|
||||
The premium tier launch taught me a lot about pricing psychology. I initially set it at EUR 8/month, then bumped to EUR 12/month after the first week when conversion was higher than expected. The people who pay aren't price-sensitive — they want signal, not savings. The AMA calls turned out to be the most valued feature, which I didn't expect. Hearing directly from engineering leaders about their challenges gives me better content ideas than any amount of keyword research.
|
||||
|
||||
Codemotion was a turning point for visibility. Speaking on stage to 400 people, then having a line of folks wanting to chat afterward — it hit me that Refactoring had become a real brand, not just "Luca's newsletter." Three sponsors reached out within a week of the talk. [[person-matteo-cellini]] converted two of them.
|
||||
|
||||
Finishing Maratona was the personal highlight of the year. The Dolomites are otherworldly — Passo Giau at dawn, with the peaks turning pink, was one of those moments where you forget about pace and just exist. Both gran fondos done, [[2024-complete-two-gran-fondos]] checked off. The [[24q3-summer-reading-sprint]] kept me intellectually fueled during the quieter August weeks. Heading into Q4 feeling strong on all fronts.
|
||||
@@ -1,36 +0,0 @@
|
||||
---
|
||||
aliases: ["Team Annual Review"]
|
||||
Is A: Project
|
||||
Belongs to: "[[24q4]]"
|
||||
Advances: "[[responsibility-team-management]]"
|
||||
Status: Done
|
||||
Owner: "[[person-luca-rossi]]"
|
||||
---
|
||||
# Team Annual Review
|
||||
|
||||
## Overview
|
||||
|
||||
As the Refactoring team grew from a solo operation to a small but functional team, it became clear that informal check-ins were not sufficient for aligning on expectations, giving structured feedback, and planning development. This project designed and executed the first formal annual review process for the three team members: [[person-matteo-cellini]] (sponsorship outreach), [[person-paco-furiani]] (operations and podcast support), and [[person-sara-ricci]] (editing).
|
||||
|
||||
The goal was not to create a heavyweight corporate HR process but to establish a lightweight, honest, and useful review cycle that fits a small indie team. Reviews happened in December 2024, covering the full calendar year, and set the stage for [[25q1]] planning. The process was designed to be reusable and will run annually going forward.
|
||||
|
||||
## Goals
|
||||
|
||||
- Design a review template covering: role scope, key accomplishments, areas for growth, and goals for next year
|
||||
- Write a self-assessment prompt for each team member to complete before the review meeting
|
||||
- Conduct 1-hour review conversations with each team member
|
||||
- Document agreed-upon goals and any compensation adjustments for 2025
|
||||
- Establish this as a repeatable annual process with a calendar reminder and documented procedure
|
||||
|
||||
## Key decisions
|
||||
|
||||
- **Conversation-first, not form-first.** The review is structured around a 1-hour conversation, not a lengthy written form. The self-assessment is a short (half-page) prompt to help team members reflect before the meeting. The conversation is where the real feedback happens.
|
||||
- **Bidirectional feedback.** Each team member was explicitly asked to give feedback on working with [[person-luca-rossi]] — what works, what does not, what they need more of. This was uncomfortable but produced the most actionable insights of the entire process.
|
||||
- **Compensation adjustments tied to review, not negotiation.** Rather than waiting for team members to ask for raises, proactively included compensation review as part of the process. All three team members received adjustments based on expanded scope and demonstrated value.
|
||||
|
||||
## Notes
|
||||
|
||||
- [[person-sara-ricci]]'s review was the most impactful. She articulated clearly that the editing workflow needed more lead time — receiving drafts 24 hours before publish was creating unnecessary stress and reducing edit quality. This led to adjusting the [[procedure-weekly-newsletter]] timeline to give her 48 hours minimum.
|
||||
- [[person-matteo-cellini]] expressed interest in taking on more strategic work beyond outreach execution. This was a growth signal — his Q4 performance on [[24q3-new-sponsor-verticals]] showed he could handle strategic thinking, not just tactical execution. Agreed to expand his role in 2025.
|
||||
- [[person-paco-furiani]] was the most straightforward review — he is happy with his current role scope and compensated fairly. His main feedback was wanting more visibility into the business roadmap, which is a fair ask. Started sharing quarterly plans with the full team afterward.
|
||||
- The process took about 6 hours total (prep + 3 conversations + documentation). This is very manageable for a small team. The key learning: do this every year, not just when it feels necessary. Scheduled the 2025 review for December in advance.
|
||||
@@ -1,37 +0,0 @@
|
||||
---
|
||||
aliases: ["Black Friday Newsletter Campaign"]
|
||||
Is A: Project
|
||||
Belongs to: "[[24q4]]"
|
||||
Advances: "[[responsibility-grow-newsletter]]"
|
||||
Status: Done
|
||||
Owner: "[[person-luca-rossi]]"
|
||||
---
|
||||
# Black Friday Newsletter Campaign
|
||||
|
||||
## Overview
|
||||
|
||||
Black Friday represents a unique opportunity for a technical newsletter: readers are actively looking for tool and resource recommendations, and companies are offering significant discounts. This project designed and executed a curated Black Friday campaign — a special edition newsletter with hand-picked tool recommendations for engineering leaders and developers, paired with a subscriber acquisition push through social media and cross-promotions.
|
||||
|
||||
The campaign ran from November 25-29, 2024, and generated +1,200 new subscribers in 3 days — the single largest acquisition spike of the year. The key insight was that curation is the value: readers do not want to sift through hundreds of generic deals. They want a trusted voice telling them which 10-15 tools are actually worth buying. This directly supported [[2024-reach-50k-subscribers]] and pushed the subscriber count past the 48K mark.
|
||||
|
||||
## Goals
|
||||
|
||||
- Curate a list of 15-20 Black Friday deals on developer tools, SaaS products, and learning resources
|
||||
- Negotiate exclusive or enhanced discount codes with at least 5 companies
|
||||
- Publish a special-edition newsletter issue on Black Friday week
|
||||
- Run a social media campaign (Twitter, LinkedIn) driving traffic to the newsletter signup with the deal list as the lead magnet
|
||||
- Acquire 1,000+ new subscribers during the campaign window
|
||||
|
||||
## Key decisions
|
||||
|
||||
- **Curated recommendations, not affiliate links.** While affiliate revenue was tempting, prioritized trust over short-term monetization. Every recommendation was a tool that [[person-luca-rossi]] personally uses or has thoroughly evaluated. Included honest caveats alongside recommendations. This authenticity drove shares and word-of-mouth signups.
|
||||
- **Gated the full list behind email signup.** Shared 5 deals publicly on social media with a CTA to "get the full list of 20 deals" via newsletter signup. This was the primary acquisition mechanism and converted at approximately 35% of landing page visitors.
|
||||
- **Separate from regular sponsor placements.** Black Friday sponsors paid a premium rate for a dedicated section, clearly labeled as "sponsored picks." This maintained editorial integrity while monetizing the high-attention window. Three sponsors participated at $3,000 each.
|
||||
|
||||
## Notes
|
||||
|
||||
- The +1,200 subscribers in 3 days was remarkable, but retention was the real test. At the 30-day mark, 78% of Black Friday subscribers were still active (industry benchmark is ~60% for acquisition spikes). This suggests the deal-seekers were genuinely interested in the newsletter's regular content, not just the deals.
|
||||
- Negotiating exclusive discount codes with tool companies was easier than expected. Most developer tool companies are eager for newsletter placement during Black Friday and will create custom codes quickly. Started these conversations in early November — next year, will start in October.
|
||||
- [[person-matteo-cellini]] handled sponsor coordination for the campaign, managing the three premium placements. His work on the CRM ([[24q2-sponsor-crm]]) made tracking these deals seamless.
|
||||
- The social media push was amplified significantly by retweets/reshares from the tools featured in the list. This organic amplification was not planned but accounted for an estimated 30-40% of the total reach. Will be more intentional about asking for shares in future campaigns.
|
||||
- [[measure-subscribers]] jumped from ~46,800 to ~48,000 during the campaign. Combined with organic growth, this put the year-end total within striking distance of the 50K target set in [[2024-reach-50k-subscribers]].
|
||||
@@ -1,38 +0,0 @@
|
||||
---
|
||||
aliases: ["Cycling Year in Review 2024"]
|
||||
Is A: Project
|
||||
Belongs to: "[[24q4]]"
|
||||
Advances: "[[responsibility-health-fitness]]"
|
||||
Status: Done
|
||||
Owner: "[[person-luca-rossi]]"
|
||||
---
|
||||
# Cycling Year in Review 2024
|
||||
|
||||
## Overview
|
||||
|
||||
This project reviewed the full 2024 cycling season — training volumes, event results, fitness progression, and gear decisions — and used the analysis to plan the 2025 season. The review covered everything from the spring training plan established in [[24q1-plan-cycling-season]] through the two gran fondos and into the autumn detraining period.
|
||||
|
||||
The review was conducted in November-December, using data from Strava, TrainingPeaks, and personal notes. It combined quantitative analysis (volume, intensity distribution, power data) with qualitative reflection (what felt good, what caused fatigue, where motivation dipped). The output was both a retrospective document and a set of concrete recommendations for [[25q1-strength-program]] and the 2025 race calendar.
|
||||
|
||||
## Goals
|
||||
|
||||
- Compile and analyze full-year training data: total distance, climbing, hours, and TSS
|
||||
- Review performance at both gran fondos (Varese in May, Appennino in September)
|
||||
- Assess the effectiveness of the polarized training model adopted in Q1
|
||||
- Identify 3-5 specific areas for improvement in 2025
|
||||
- Draft a preliminary 2025 season plan including target events and training philosophy adjustments
|
||||
|
||||
## Key decisions
|
||||
|
||||
- **Add structured strength training for 2025.** The year review revealed that upper body and core fatigue was a limiter on long climbs. Pure cycling volume is not enough — need complementary strength work. This directly led to [[25q1-strength-program]].
|
||||
- **Target the Stelvio in 2025.** The two 2024 gran fondos were rewarding but relatively conservative choices. For 2025, the stretch goal is the Stelvio — one of cycling's most iconic climbs. This requires a step up in training specificity and altitude preparation. Supports [[2025-ride-stelvio]].
|
||||
- **Hire a coach for 2025.** Self-coaching worked reasonably well in 2024 (both events completed within targets), but the Stelvio goal requires more sophisticated periodization and real-time plan adjustments. Will look for a coach in Q1.
|
||||
|
||||
## Notes
|
||||
|
||||
- Full-year numbers: 8,200 km ridden, 98,000m elevation gain, approximately 320 hours on the bike. This was above target (7,500 km) and sustainable — no overtraining symptoms or significant injuries.
|
||||
- The polarized model (80% easy / 20% hard) was broadly successful, though the actual distribution ended up closer to 75/25 because "easy" rides often crept into moderate territory. Discipline on easy days is something to improve.
|
||||
- [[person-paco-furiani]] completed the same two events and his season review is a useful comparison point. His higher-volume approach (10,500 km) yielded better absolute results but also led to a knee issue in October. Reinforces the value of sustainability over pure volume.
|
||||
- The Campagnolo Bora WTO 45 wheels purchased pre-season were the best equipment decision of the year. Noticeable improvement on climbs and crosswind stability. No regrets on prioritizing wheels over frame upgrade.
|
||||
- [[measure-cycling-km-per-month]] data shows clear seasonal pattern: low in Jan-Feb (800-900km/month), building through Mar-May (1,000-1,200km), peaking in Jun-Aug (1,200-1,400km), and tapering in Sep-Nov. December was deliberately rest-focused (~400km).
|
||||
- The retrospective feeds directly into [[topic-cycling-training]] notes and sets the direction for 2025 season planning.
|
||||
@@ -1,37 +1,17 @@
|
||||
---
|
||||
aliases: ["Start Laputa App Project"]
|
||||
Is A: Project
|
||||
Belongs to: "[[24q4]]"
|
||||
Advances: "[[responsibility-learning]]"
|
||||
Status: Done
|
||||
Owner: "[[person-luca-rossi]]"
|
||||
type: Project
|
||||
aliases:
|
||||
- "[[Start Laputa App Project]]"
|
||||
belongs_to: "[[24q4]]"
|
||||
owner: "[[person-luca-rossi]]"
|
||||
status: Done
|
||||
---
|
||||
|
||||
# Start Laputa App Project
|
||||
|
||||
## Overview
|
||||
The original spike that proved Tolaria could read a markdown vault, render note metadata, and support keyboard-first navigation.
|
||||
|
||||
After years of managing a personal knowledge vault in Obsidian and finding it increasingly insufficient for the structured, frontmatter-heavy workflow that had evolved, this project kicked off the development of Laputa — a custom desktop application built specifically for the vault's unique requirements. The app is built with Tauri v2 (Rust backend) and React (TypeScript frontend), designed to read and write the same markdown files with YAML frontmatter that the vault already uses.
|
||||
- Set the initial four-panel layout.
|
||||
- Proved the note list, editor, and inspector could coexist in one flow.
|
||||
- Led directly into [[25q1-laputa-v1]].
|
||||
|
||||
The motivation was not to replace Obsidian for general note-taking, but to build a purpose-built tool for the structured data layer — types, relationships, properties, and views that Obsidian handles poorly. Laputa treats the vault as a lightweight database of markdown files, offering a four-panel UI with type-aware navigation, property editing, and relational browsing. This project covers the initial spike: architecture decisions, proof of concept, and first working prototype. It advances [[2025-ship-laputa]] as the longer-term goal.
|
||||
|
||||
## Goals
|
||||
|
||||
- Define the technical architecture: Tauri v2, React 18, CodeMirror 6, Vite
|
||||
- Build a proof of concept that reads markdown files from a directory and displays them in a panel UI
|
||||
- Implement frontmatter parsing in Rust (using serde and gray_matter)
|
||||
- Create the basic four-panel layout: type sidebar, note list, editor, and property panel
|
||||
- Validate that the app can handle the full vault (~9,200 markdown files) without performance issues
|
||||
|
||||
## Key decisions
|
||||
|
||||
- **Tauri v2, not Electron.** Tauri produces much smaller binaries, uses less memory, and leverages native webview rather than shipping a full Chromium instance. For a single-user desktop app, Tauri's trade-offs (no cross-platform webview consistency, newer ecosystem) are acceptable. The Rust backend is also more natural for file I/O heavy workloads.
|
||||
- **Files as the source of truth, not a database.** Laputa reads and writes markdown files directly. No SQLite, no JSON store, no separate database. This means the vault remains portable, version-controllable with git, and readable in any text editor. The cost is that some queries are slower than they would be with a database, but for ~10K files, filesystem operations are fast enough.
|
||||
- **CodeMirror 6, not ProseMirror or TipTap.** CodeMirror 6 provides the best foundation for a reveal-on-focus markdown editor — showing rendered markdown by default but revealing raw syntax when the cursor enters a block. This is the editing model that feels most natural for power users.
|
||||
|
||||
## Notes
|
||||
|
||||
- The initial proof of concept took about 3 weeks of focused evening/weekend work. Getting Tauri v2 set up and communicating between the Rust backend and React frontend required working through several documentation gaps — Tauri v2 was still relatively new and the ecosystem was evolving.
|
||||
- Performance testing with the full vault (~9,200 files) showed that initial directory scanning takes about 2 seconds, and frontmatter parsing adds another 1.5 seconds. This is acceptable for app startup, though indexing will need optimization for real-time search.
|
||||
- [[person-david-kim]] (who works on developer tools) provided useful early feedback on the UI concept during a podcast conversation. His perspective on tool-building for personal use versus building products helped frame the project's scope.
|
||||
- The mock data layer (`src/mock-tauri.ts`) was built early and proved invaluable for browser-based testing. Being able to develop and test the React frontend without the Rust backend running dramatically accelerated UI iteration.
|
||||
- This project transitions directly into [[25q1-laputa-v1]] for the first real milestone of the app.
|
||||
|
||||
@@ -1,37 +0,0 @@
|
||||
---
|
||||
aliases: ["LinkedIn Cross-posting Experiment"]
|
||||
Is A: Experiment
|
||||
Belongs to: "[[24q4]]"
|
||||
Status: Done
|
||||
Owner: "[[person-luca-rossi]]"
|
||||
---
|
||||
# LinkedIn Cross-posting Experiment
|
||||
|
||||
Tested whether repurposing newsletter essays as LinkedIn posts could drive meaningful follower growth and referral traffic back to the newsletter. LinkedIn's algorithm favors native content, so the hypothesis was that adapted versions of existing essays would perform well without requiring net-new writing effort, supporting both [[responsibility-grow-newsletter]] and [[topic-newsletter-growth]].
|
||||
|
||||
## Hypothesis
|
||||
|
||||
Posting 3 adapted newsletter essays per week on LinkedIn for 6 weeks would generate at least 500 new LinkedIn followers and drive 200+ clicks to the newsletter signup page, at under 30 minutes of incremental work per post.
|
||||
|
||||
## Setup
|
||||
|
||||
- Selected the top-performing newsletter essays from [[24q3]] and [[24q4]] based on [[measure-open-rate]].
|
||||
- Adapted each essay into LinkedIn-native format: shorter paragraphs, hook-first structure, no external links in the body (link in first comment instead).
|
||||
- Posted 3x per week (Monday, Wednesday, Friday) for 6 weeks.
|
||||
- Tracked LinkedIn follower growth, post impressions, engagement rate, and UTM-tagged clicks to the newsletter signup page.
|
||||
|
||||
## Results
|
||||
|
||||
- LinkedIn followers: +812 over 6 weeks (from ~2,400 to ~3,200).
|
||||
- Average post impressions: ~6,500 (range: 1,200 to 28,000).
|
||||
- Newsletter signups via LinkedIn referral: 147 (below the 200 target but still meaningful).
|
||||
- Time per post: approximately 20 minutes (mostly reformatting, not rewriting).
|
||||
- Top-performing posts were contrarian takes and personal stories; tactical how-to posts underperformed.
|
||||
|
||||
## Takeaways
|
||||
|
||||
- LinkedIn cross-posting is a high-ROI channel for newsletter growth. The effort-to-output ratio is excellent since the content already exists.
|
||||
- Follower growth exceeded the target. The platform rewards consistency and native formatting.
|
||||
- Referral traffic was modest. LinkedIn users tend to engage on-platform rather than clicking out. The real value is brand awareness and top-of-funnel reach.
|
||||
- Will continue cross-posting as a permanent part of the [[procedure-weekly-newsletter]] workflow.
|
||||
- Consider having [[person-matteo-cellini]] handle the reformatting to free up time for [[responsibility-content-production]].
|
||||
@@ -1,37 +0,0 @@
|
||||
---
|
||||
aliases: ["Build Sponsor Dashboard"]
|
||||
Is A: Project
|
||||
Belongs to: "[[24q4]]"
|
||||
Advances: "[[responsibility-sponsorships]]"
|
||||
Status: Done
|
||||
Owner: "[[person-luca-rossi]]"
|
||||
---
|
||||
# Build Sponsor Dashboard
|
||||
|
||||
## Overview
|
||||
|
||||
As the sponsor base grew through 2024, reporting became a bottleneck. After each newsletter issue, sponsors would ask for performance data — click counts, open rates, and placement impressions. This data was being compiled manually from ConvertKit analytics and sent via email, which was time-consuming and error-prone. This project built an Airtable-based dashboard that gives sponsors self-serve access to real-time performance data for their placements.
|
||||
|
||||
The dashboard connects to the CRM built in [[24q2-sponsor-crm]] and pulls performance data from ConvertKit's API via a Zapier integration. Each sponsor gets a unique link to a filtered Airtable view showing only their placements, metrics, and invoice history. This project was a significant step toward professionalizing the sponsor experience and supporting [[2024-double-revenue]] through improved retention and upsell conversations.
|
||||
|
||||
## Goals
|
||||
|
||||
- Build an Airtable dashboard with per-sponsor performance data (clicks, CTR, impressions)
|
||||
- Set up a Zapier integration to pull click and open data from ConvertKit after each newsletter send
|
||||
- Create unique, filtered dashboard links for each sponsor (no login required, link-based access)
|
||||
- Include historical performance trends (week-over-week, month-over-month)
|
||||
- Reduce sponsor reporting time from ~2 hours/week to near-zero
|
||||
|
||||
## Key decisions
|
||||
|
||||
- **Airtable interface, not a custom web app.** Considered building a custom dashboard with Next.js and a database, but the engineering effort was not justified for ~20 active sponsors. Airtable's Interface Designer provides 80% of the functionality at 5% of the development cost. Can always migrate to a custom solution if the sponsor base grows significantly.
|
||||
- **Self-serve access via unique links.** Rather than requiring sponsors to log into a portal, each sponsor gets a unique Airtable shared view URL. This is simpler for both parties — no password management, no onboarding friction. The trade-off is that anyone with the link can see the data, but the risk is low for non-sensitive marketing metrics.
|
||||
- **Automated data ingestion, manual quality checks.** The Zapier integration pulls data automatically after each send, but [[person-matteo-cellini]] reviews the numbers weekly before they appear on sponsor dashboards. This catches any data anomalies (e.g., bot clicks, tracking issues) before sponsors see them.
|
||||
|
||||
## Notes
|
||||
|
||||
- The dashboard launch received very positive feedback from sponsors. Several mentioned it was the most transparent reporting they had seen from any newsletter sponsorship. This transparency became a competitive advantage in renewal conversations.
|
||||
- The Zapier integration was the most fragile part of the system. ConvertKit's API rate limits and occasional data delays caused the integration to fail about once a month. Building retry logic and error notifications helped, but this remains a maintenance burden.
|
||||
- Sponsor self-serve reporting reduced [[person-matteo-cellini]]'s weekly reporting workload from approximately 2 hours to about 15 minutes (just the quality review). This freed significant time for outreach and relationship management.
|
||||
- The dashboard data also proved valuable for internal analysis. Being able to see click-through rates across all sponsors and placements revealed which types of copy and positioning perform best, informing both editorial decisions and sponsor pitch strategy.
|
||||
- One unexpected benefit: sponsors who could see their own performance data were more likely to experiment with different ad copy and CTAs. This improved their results and, by extension, their satisfaction and renewal rates. The dashboard supported [[measure-sponsorship-mrr]] growth indirectly through improved retention.
|
||||
@@ -1,32 +1,16 @@
|
||||
---
|
||||
aliases: ["Q4 2024"]
|
||||
Is A: Quarter
|
||||
Created at: "2024-10-01"
|
||||
Belongs to: "[[2024]]"
|
||||
Has: ["[[24q4-annual-review-process]]", "[[24q4-sponsor-dashboard]]", "[[24q4-laputa-start]]", "[[24q4-black-friday-campaign]]", "[[24q4-cycling-year-review]]"]
|
||||
Status: Done
|
||||
type: Quarter
|
||||
aliases:
|
||||
- "[[Q4 2024]]"
|
||||
status: Done
|
||||
has:
|
||||
- "[[24q4-laputa-start]]"
|
||||
---
|
||||
|
||||
# Q4 2024
|
||||
|
||||
## Focus
|
||||
The quarter where the Laputa prototype became real enough to replace sketches and notes.
|
||||
|
||||
Q4 was about closing the year strong and planting seeds for 2025. The Black Friday campaign was the commercial centerpiece. But the most exciting thing was starting work on [[24q4-laputa-start]] — the personal knowledge management tool I'd been sketching in notebooks for years. The quarter also meant wrapping up the annual review process and reflecting on a transformative cycling season.
|
||||
- Started [[24q4-laputa-start]] as the first working app spike.
|
||||
- Captured the initial panel layout and editor decisions.
|
||||
|
||||
## Highlights
|
||||
|
||||
- [[24q4-black-friday-campaign]] drove 2,800 new subscribers in 10 days and converted 180 free readers to premium — best single campaign ever
|
||||
- Built [[24q4-sponsor-dashboard]] giving sponsors real-time access to their campaign metrics — reduced back-and-forth emails by ~70%
|
||||
- Started [[24q4-laputa-start]]: initial Tauri + React prototype, basic markdown reading, YAML frontmatter parsing — the MVP of the MVP
|
||||
- Designed and documented [[24q4-annual-review-process]] — a structured template for reviewing goals, projects, and metrics across the year
|
||||
- Completed [[24q4-cycling-year-review]]: 6,200 km ridden, two gran fondos, FTP up 12% year-over-year
|
||||
- Newsletter crossed 50k subscribers in November — [[2024-reach-50k-subscribers]] achieved
|
||||
- [[measure-sponsorship-mrr]] closed the year at EUR 11.4k, more than doubling from January — [[2024-double-revenue]] hit
|
||||
- [[2024-read-24-books]] finished with 26 books total — exceeded target
|
||||
|
||||
## Reflections
|
||||
|
||||
Hitting 50k subscribers felt like a milestone that validated the whole year's strategy. When I started 2024 at 35k, doubling the growth rate felt ambitious. But the combination of pillar content, podcast reach, the Codemotion talk, and the Black Friday push all compounded. Each channel fed the others in ways I couldn't have predicted at the start of the year.
|
||||
|
||||
Starting Laputa was the sleeper highlight of Q4. I'd been frustrated with Obsidian's limitations for months — specifically around structured data, relationships between notes, and the inability to build custom views. So I started building my own tool. The early prototype was rough, but the moment I saw my own vault rendered in a four-panel layout I'd designed, something clicked. This could be more than a side project.
|
||||
|
||||
The sponsor dashboard was [[person-matteo-cellini]]'s idea, and it was brilliant. Sponsors love transparency, and giving them self-serve access to metrics reduced our support burden dramatically. [[person-paco-furiani]] set up the automated reporting pipeline. Looking back at [[2024]] as a whole, every major goal was hit or exceeded. That doesn't happen often, and I don't take it for granted. Now the question is: how do we scale this in [[2025]] without burning out?
|
||||
|
||||
@@ -1,37 +1,17 @@
|
||||
---
|
||||
aliases: ["Laputa App V1"]
|
||||
Is A: Project
|
||||
Belongs to: "[[25q1]]"
|
||||
Advances: "[[responsibility-learning]]"
|
||||
Status: Done
|
||||
Owner: "[[person-luca-rossi]]"
|
||||
type: Project
|
||||
aliases:
|
||||
- "[[Laputa App V1]]"
|
||||
belongs_to: "[[25q1]]"
|
||||
owner: "[[person-luca-rossi]]"
|
||||
status: Done
|
||||
---
|
||||
|
||||
# Laputa App V1
|
||||
|
||||
## Overview
|
||||
The first usable release for daily browsing, quick open, and note-property editing.
|
||||
|
||||
Following the proof of concept built in [[24q4-laputa-start]], this project delivered the first working version of Laputa with a complete four-panel layout, a property inspector, and a quick-open command palette. V1 represents the transition from "can this work?" to "I can actually use this daily" — the app became the primary interface for browsing and editing the vault, replacing Obsidian for structured data workflows.
|
||||
- Shipped the working command palette.
|
||||
- Made the inspector practical for real frontmatter editing.
|
||||
- Captured enough confidence to continue with [[25q2-laputa-v2]].
|
||||
|
||||
The four-panel layout includes a type sidebar (showing all note types like Project, Person, Quarter), a filterable note list, the main editor panel with CodeMirror 6, and a property inspector for viewing and editing YAML frontmatter fields. Quick Open (Cmd+K) enables instant navigation across all ~9,200 vault files. This milestone is a key step toward [[2025-ship-laputa]] and was completed during [[25q1]].
|
||||
|
||||
## Goals
|
||||
|
||||
- Implement the four-panel layout with resizable panels and responsive behavior
|
||||
- Build the property inspector panel for viewing and editing frontmatter properties
|
||||
- Implement Quick Open (Cmd+K) with fuzzy search across all vault files
|
||||
- Add type-aware sidebar navigation with collapsible sections and file counts
|
||||
- Achieve sub-200ms navigation between notes (read file, parse frontmatter, render editor)
|
||||
|
||||
## Key decisions
|
||||
|
||||
- **CodeMirror 6 with reveal-on-focus editing.** The editor shows rendered markdown by default but reveals raw syntax when the cursor enters a block. This hybrid approach was chosen over a pure WYSIWYG editor (TipTap/ProseMirror) because power users need direct access to markdown syntax, especially for frontmatter and wiki-links. The reveal-on-focus model provides the best of both worlds.
|
||||
- **Frontmatter parsing in Rust, rendering in React.** The Rust backend parses YAML frontmatter and provides structured data to the frontend via Tauri commands. This keeps the parsing fast and consistent, while React handles all presentation logic. The alternative — parsing in JavaScript — would be slower for bulk operations and harder to keep consistent with the Rust-side file operations.
|
||||
- **Mock layer for browser development.** Built `src/mock-tauri.ts` to return realistic test data when running in a browser without the Tauri backend. This allows UI development and testing in Chrome without starting the full Rust backend, which dramatically accelerated the development loop.
|
||||
|
||||
## Notes
|
||||
|
||||
- The biggest engineering challenge was making the editor performant with large files. Some vault files are 5,000+ lines, and CodeMirror 6's default configuration struggled with these. Enabling viewport-based rendering and lazy decoration computation solved the performance issues.
|
||||
- Quick Open search across 9,200 files needed careful optimization. The initial naive approach (filter on every keystroke) was too slow. Implemented a pre-built search index that loads at startup and supports fuzzy matching with sub-50ms response times.
|
||||
- [[person-david-kim]] tested an early build and identified several UX issues with the property inspector — particularly around editing array-type frontmatter fields (like tags and aliases). His feedback led to a redesigned array editor with inline add/remove controls.
|
||||
- The mock data layer proved its value immediately. It allowed running Playwright E2E tests and visual verification in Chrome without any Rust toolchain setup. This made CI testing feasible and significantly reduced the feedback loop for UI changes.
|
||||
- V1 was usable but rough around the edges. The editor lacked many creature comforts (undo/redo state persistence, find-and-replace, syntax highlighting for YAML). These were deferred to [[25q2-laputa-v2]] to keep V1 scope manageable.
|
||||
|
||||
@@ -1,37 +0,0 @@
|
||||
---
|
||||
aliases: ["Newsletter SEO Sprint"]
|
||||
Is A: Project
|
||||
Belongs to: "[[25q1]]"
|
||||
Advances: "[[responsibility-grow-newsletter]]"
|
||||
Status: Done
|
||||
Owner: "[[person-luca-rossi]]"
|
||||
---
|
||||
# Newsletter SEO Sprint
|
||||
|
||||
## Overview
|
||||
|
||||
Building on the pillar article strategy from [[24q2-10-pillar-articles]], this project was a concentrated 30-day sprint to boost organic search traffic and subscriber acquisition. The sprint had two components: writing 5 new high-traffic articles targeting keywords identified in an updated Ahrefs analysis, and overhauling the internal linking structure across all existing content to improve site-wide SEO authority.
|
||||
|
||||
The sprint ran through February 2025 and was timed to take advantage of the post-holiday traffic recovery in the engineering content space. By this point, the blog had about 40 published articles, but many lacked internal links and had suboptimal meta descriptions. The combination of new content and structural improvements was designed to create a compounding traffic effect that would support [[2025-reach-85k-subscribers]] through organic growth.
|
||||
|
||||
## Goals
|
||||
|
||||
- Write and publish 5 new SEO-optimized articles (2,500-4,000 words each)
|
||||
- Audit and update internal links across all existing blog posts (target: every article links to at least 3 other articles)
|
||||
- Update meta descriptions and title tags for the 20 highest-traffic existing articles
|
||||
- Improve site-wide Core Web Vitals scores (target: all pages in "Good" range)
|
||||
- Achieve a 30% increase in organic search traffic within 90 days of sprint completion
|
||||
|
||||
## Key decisions
|
||||
|
||||
- **Concentrated sprint, not ongoing drip.** Rather than publishing one SEO article per month, concentrated the effort into a 30-day window. This allowed full focus on SEO without the context-switching cost of alternating between newsletter writing and SEO writing. The trade-off was that the regular newsletter required extra preparation in advance to cover the sprint period.
|
||||
- **Internal linking overhaul as a force multiplier.** The internal linking audit was actually higher-impact than the new articles. Many existing articles had zero internal links, meaning Google could not discover or weight them properly. Adding contextual links between related articles created a proper content topology that improved rankings site-wide.
|
||||
- **Content refresh over new content for some keywords.** For 3 keywords where existing articles were already ranking on page 2-3, chose to refresh and expand those articles rather than write new ones. Refreshing existing URLs preserves accumulated link equity and is faster than starting from scratch.
|
||||
|
||||
## Notes
|
||||
|
||||
- The 30% organic traffic increase was achieved in about 75 days (ahead of the 90-day target). The internal linking overhaul was the primary driver — several existing articles jumped from page 2 to page 1 within weeks of gaining internal links.
|
||||
- [[person-sara-ricci]] edited all 5 new articles and the refreshed content. The editing workload during the sprint was significant, and she flagged that future sprints should give her more lead time. Noted for next time.
|
||||
- The article on "How to Run Effective Engineering All-Hands" became the strongest performer, reaching #3 on Google for its target keyword and driving ~1,500 monthly organic visits. This single article generated an estimated 200 new subscribers per month through its email capture CTA.
|
||||
- Core Web Vitals optimization required some technical work — lazy loading images, reducing JavaScript bundle size, and optimizing font loading. These changes improved site speed across the board and had a modest positive effect on rankings.
|
||||
- This sprint validated the content pillar strategy and demonstrated that concentrated SEO investment compounds over time. The articles written in [[24q2-10-pillar-articles]] continued to grow in traffic alongside the new content, suggesting that the internal linking network creates a rising-tide effect. Feeds into [[topic-newsletter-growth]] and [[topic-content-strategy]].
|
||||
@@ -1,36 +0,0 @@
|
||||
---
|
||||
aliases: ["Paid Newsletter Trial"]
|
||||
Is A: Experiment
|
||||
Belongs to: "[[25q1]]"
|
||||
Status: Abandoned
|
||||
Owner: "[[person-luca-rossi]]"
|
||||
---
|
||||
# Paid Newsletter Trial
|
||||
|
||||
Tested a Substack-style paid tier to evaluate whether the newsletter audience would convert to a direct subscription model. The motivation was to explore revenue diversification beyond [[measure-sponsorship-mrr]], which currently accounts for the majority of income. A paid tier could provide more predictable, audience-aligned revenue while reducing dependence on the sponsorship sales cycle.
|
||||
|
||||
## Hypothesis
|
||||
|
||||
Offering a paid tier at $10/month (or $100/year) with one exclusive deep-dive essay per month and early access to all posts would convert at least 2% of the free subscriber base within 8 weeks, generating $2,000+ MRR.
|
||||
|
||||
## Setup
|
||||
|
||||
- Built a paid tier on the existing newsletter platform with two perks: one exclusive monthly deep-dive and 48-hour early access to all free posts.
|
||||
- Announced the paid tier via a dedicated newsletter issue, a pinned tweet, and a LinkedIn post.
|
||||
- Ran a 2-week launch promotion at 50% off the annual plan.
|
||||
- Tracked conversion rate, churn after first month, and qualitative feedback from subscribers who did and did not convert.
|
||||
|
||||
## Results
|
||||
|
||||
- Total paid subscribers after 8 weeks: 89 (approximately 0.2% of the free base, well below the 2% target).
|
||||
- MRR generated: ~$620 (mix of monthly and discounted annual).
|
||||
- Churn after first month: 18% of monthly subscribers cancelled.
|
||||
- Qualitative feedback: most free subscribers said the free content was already "more than enough." Those who paid cited "supporting the creator" as the primary reason, not exclusive content.
|
||||
|
||||
## Takeaways
|
||||
|
||||
- The audience does not perceive enough differentiation between free and paid content to justify a subscription. The newsletter's value proposition is built on free, high-quality essays.
|
||||
- Conversion was 10x below target. The paid tier would need a fundamentally different value prop (e.g., community, tools, templates) to work.
|
||||
- Abandoned the paid tier to avoid splitting focus. The sponsorship model ([[responsibility-sponsorships]], [[24q1-launch-sponsorship-packages]]) delivers significantly higher revenue per hour of effort.
|
||||
- If revisited, would explore a premium community model (see [[25q3-discord-community-soft]]) rather than paywalled content.
|
||||
- Key lesson: do not assume that "more content" is a compelling paid offering. Access, community, and tools tend to convert better for this audience segment.
|
||||
@@ -1,37 +0,0 @@
|
||||
---
|
||||
aliases: ["Increase Sponsorship Rates Q2"]
|
||||
Is A: Project
|
||||
Belongs to: "[[25q1]]"
|
||||
Advances: "[[responsibility-sponsorships]]"
|
||||
Status: Done
|
||||
Owner: "[[person-luca-rossi]]"
|
||||
---
|
||||
# Increase Sponsorship Rates Q2
|
||||
|
||||
## Overview
|
||||
|
||||
With the subscriber base approaching 60K and click-through performance data showing consistent improvement through 2024, the sponsorship rates set during [[24q1-launch-sponsorship-packages]] were underpriced relative to the audience value delivered. This project prepared and executed a 25% rate increase across all sponsorship tiers, effective for Q2 2025 bookings.
|
||||
|
||||
The rate increase required careful preparation: assembling performance data, updating the media kit, communicating the changes to existing sponsors, and adjusting outreach messaging for new prospects. The goal was to increase [[measure-sponsorship-mrr]] without losing existing sponsors. [[person-matteo-cellini]] led the communication with current sponsors while [[person-luca-rossi]] handled the strategic positioning and media kit updates.
|
||||
|
||||
## Goals
|
||||
|
||||
- Analyze per-sponsor performance data to build the justification case (CTR, impressions, conversions)
|
||||
- Update the media kit with current audience size, demographics, and performance benchmarks
|
||||
- Communicate the rate increase to all existing sponsors with 60 days advance notice
|
||||
- Update pricing across all three tiers: Logo ($625), Spotlight ($1,500), Deep Dive ($3,125)
|
||||
- Retain at least 80% of existing sponsors through the transition
|
||||
|
||||
## Key decisions
|
||||
|
||||
- **25% increase, not 15% or 30%.** Benchmarked against similar-sized engineering newsletters and found the current rates were 20-35% below market. A 25% increase brings pricing to market rate while leaving room for another adjustment in 12-18 months as the audience continues to grow.
|
||||
- **Grandfathered pricing for 90 days.** Offered existing sponsors who renewed before April 1 the option to lock in the old rate for one additional quarter. This rewarded loyalty and created urgency for renewals, which improved cash flow predictability.
|
||||
- **Performance data as the lead message.** Rather than leading with "rates are going up," led with "here is how your sponsorship performed." Sharing concrete performance data first made the rate increase feel like a natural consequence of value delivered, not an arbitrary price hike.
|
||||
|
||||
## Notes
|
||||
|
||||
- Sponsor retention through the increase was 88% — above the 80% target. The two sponsors who did not renew cited budget constraints (not value concerns), which is a healthy signal. They indicated interest in returning at a future date.
|
||||
- [[person-matteo-cellini]] handled the sponsor communication calls and reported that the response was overwhelmingly positive. Several sponsors said the previous rates felt "too cheap" and appreciated the transparency about pricing methodology.
|
||||
- The rate increase translated to approximately $3,200/month in additional MRR, which was a significant contribution to [[2025-reach-22k-mrr]]. This was the highest-leverage revenue action of Q1 — no new sponsors needed, just better pricing for existing value.
|
||||
- The updated media kit now includes a "Performance Highlights" section with anonymized case studies showing sponsor CTR and conversion data. This has become one of the most effective tools in the outreach process.
|
||||
- Key lesson: rate increases should be proactive and regular (annually), not reactive and rare. Waiting too long to adjust pricing means leaving significant revenue on the table. The discomfort of raising prices is always greater in anticipation than in reality.
|
||||
@@ -1,38 +0,0 @@
|
||||
---
|
||||
aliases: ["Newsletter Referral Program"]
|
||||
Is A: Project
|
||||
Belongs to: "[[25q1]]"
|
||||
Advances: "[[responsibility-grow-newsletter]]"
|
||||
Status: Done
|
||||
Owner: "[[person-luca-rossi]]"
|
||||
---
|
||||
# Newsletter Referral Program
|
||||
|
||||
## Overview
|
||||
|
||||
Word-of-mouth had always been the newsletter's strongest organic growth channel, but it was entirely passive — readers shared when they felt like it, with no structured incentive. This project launched a formal referral program that rewards subscribers who bring in new readers, turning the most engaged audience members into active growth ambassadors. The program contributes to [[2025-reach-85k-subscribers]] and [[measure-subscribers]] growth.
|
||||
|
||||
The program was built using SparkLoop (a referral platform designed for newsletters) and integrated with the existing ConvertKit setup. Rewards are tiered: 3 referrals earns a digital resource pack, 10 referrals earns a private Q&A session, and 25 referrals earns a 1-year premium membership. The program launched in February 2025 and immediately became the second-largest subscriber acquisition channel after organic search.
|
||||
|
||||
## Goals
|
||||
|
||||
- Design a tiered reward structure that motivates sharing without creating perverse incentives (spam, fake signups)
|
||||
- Set up SparkLoop integration with ConvertKit for automated tracking and reward fulfillment
|
||||
- Create shareable assets: personalized referral links, social cards, and email forward templates
|
||||
- Acquire 2,000+ new subscribers through the referral program in Q1
|
||||
- Achieve a referral conversion rate of at least 20% (referred visitors who subscribe)
|
||||
|
||||
## Key decisions
|
||||
|
||||
- **SparkLoop, not a custom solution.** Evaluated building a custom referral system with unique links and a tracking database, but SparkLoop handles fraud detection, analytics, and reward automation out of the box. The $99/month cost is trivially justified by the subscriber value it generates.
|
||||
- **Digital rewards, not physical.** Physical rewards (stickers, merchandise) create fulfillment complexity and shipping costs. Digital rewards (resource packs, access, sessions) scale infinitely and are more aligned with what the audience values. The private Q&A session at 10 referrals proved to be the most motivating tier.
|
||||
- **Anti-fraud measures from day one.** Required referred subscribers to confirm via double opt-in and excluded signups from disposable email domains. This reduced fraudulent referrals to less than 2% of total, which is well below SparkLoop's industry average of 5-8%.
|
||||
|
||||
## Notes
|
||||
|
||||
- The referral program generated approximately 2,400 new subscribers in the first 8 weeks — above the 2,000 target. About 15% of the existing subscriber base activated their referral link, which is strong engagement for a referral program.
|
||||
- The conversion rate on referred visitors was 28%, significantly above the 20% target. This makes sense: referred visitors arrive with a personal recommendation from someone they trust, which dramatically reduces the "should I subscribe?" friction.
|
||||
- The top referrer brought in 47 new subscribers in the first month. Sent a personal thank-you email and offered an hour-long call. This kind of super-referrer engagement is worth cultivating.
|
||||
- [[person-elena-rossi]] suggested adding a leaderboard showing top referrers (anonymized). Implemented this as a monthly "Referral Champions" section in the newsletter footer. It created a subtle competitive dynamic that boosted engagement.
|
||||
- One unexpected finding: referred subscribers have higher 30-day retention (85%) compared to subscribers from other channels (~72% average). This suggests that referral-acquired subscribers are genuinely interested in the content, not just casually signing up.
|
||||
- The program runs continuously and feeds into [[topic-newsletter-growth]]. The per-subscriber acquisition cost through referrals is approximately $0.40 (cost of SparkLoop + digital rewards amortized), compared to $2-3 for paid acquisition channels.
|
||||
@@ -1,38 +0,0 @@
|
||||
---
|
||||
aliases: ["New Strength Program"]
|
||||
Is A: Project
|
||||
Belongs to: "[[25q1]]"
|
||||
Advances: "[[responsibility-health-fitness]]"
|
||||
Status: Done
|
||||
Owner: "[[person-luca-rossi]]"
|
||||
---
|
||||
# New Strength Program
|
||||
|
||||
## Overview
|
||||
|
||||
The [[24q4-cycling-year-review]] identified a clear gap: upper body and core strength were limiters on long climbs, and pure cycling volume was not addressing them. This project introduced a structured 12-week strength training program to complement the cycling training plan, with the primary goal of improving climbing performance and reducing injury risk for the 2025 season — specifically for [[2025-ride-stelvio]].
|
||||
|
||||
The program followed a progressive overload model with three sessions per week (Monday, Wednesday, Friday mornings), focusing on core stability, hip strength, and upper body endurance. The sessions were designed to be cycling-compatible — meaning they build functional strength without adding excessive muscle mass that would penalize climbing performance. The program ran from January through March 2025.
|
||||
|
||||
## Goals
|
||||
|
||||
- Complete a 12-week strength program with at least 85% session adherence
|
||||
- Focus areas: core stability (planks, dead bugs, pallof press), hip strength (single-leg squats, hip thrusts), upper body endurance (rows, push-ups, shoulder press)
|
||||
- Sessions capped at 45 minutes to fit before the work day
|
||||
- Improve core endurance test (plank hold) from 2:00 to 3:00+ minutes
|
||||
- No negative impact on cycling performance metrics (FTP, weekly TSS)
|
||||
|
||||
## Key decisions
|
||||
|
||||
- **Gym-based, not home-based.** Considered a bodyweight-only home program for convenience, but decided that gym access (barbells, cable machines) allows for more precise progressive overload. Signed up at a gym 10 minutes from home with early morning hours (opens 6:30 AM).
|
||||
- **Three sessions per week, not two or four.** Two sessions would not provide sufficient stimulus for meaningful adaptation. Four would risk interfering with cycling recovery, especially during build-phase weeks. Three sessions, alternating between upper/lower focus, hit the sweet spot.
|
||||
- **No heavy squats or deadlifts.** Traditional powerlifting movements build too much leg mass for a cyclist targeting climbing performance. Instead, focused on single-leg movements and moderate-weight, higher-rep schemes that build strength endurance rather than maximal strength.
|
||||
|
||||
## Notes
|
||||
|
||||
- Session adherence was 89% (32 of 36 planned sessions). The four missed sessions were all during a particularly heavy newsletter week in February. Built in makeup sessions the following week when possible.
|
||||
- The plank hold test improved from 2:00 to 3:25 — well above the 3:00 target. More importantly, subjective feedback from early-season rides confirmed that upper body fatigue during long climbs was significantly reduced.
|
||||
- [[person-paco-furiani]] joined for several sessions and followed a modified version of the same program. Training together added accountability and made the early morning sessions more tolerable.
|
||||
- The biggest adaptation was not physical but scheduling. Finding 45 minutes three mornings per week while also cycling 8-10 hours per week and running a content business required ruthless calendar management. The key was treating strength sessions as non-negotiable appointments.
|
||||
- No measurable negative impact on cycling FTP (remained at ~275W throughout the program). This confirmed that the moderate-weight, endurance-focused approach was compatible with cycling performance.
|
||||
- The program transitions into a maintenance phase (2 sessions/week) during the peak cycling season. Detailed in [[topic-cycling-training]] notes.
|
||||
@@ -1,32 +1,13 @@
|
||||
---
|
||||
aliases: ["Q1 2025"]
|
||||
Is A: Quarter
|
||||
Created at: "2025-01-01"
|
||||
Belongs to: "[[2025]]"
|
||||
Has: ["[[25q1-laputa-v1]]", "[[25q1-newsletter-seo-sprint]]", "[[25q1-strength-program]]", "[[25q1-rate-increase]]", "[[25q1-referral-program]]"]
|
||||
Status: Done
|
||||
type: Quarter
|
||||
aliases:
|
||||
- "[[Q1 2025]]"
|
||||
status: Done
|
||||
has:
|
||||
- "[[25q1-laputa-v1]]"
|
||||
---
|
||||
|
||||
# Q1 2025
|
||||
|
||||
## Focus
|
||||
The first period where Laputa was usable for daily navigation, quick open, and inspector flows.
|
||||
|
||||
Q1 2025 was about building momentum for the scale year. The two big bets were shipping Laputa v1 as a usable internal tool and pushing a sponsor rate increase now that we had the audience and metrics to justify it. On the content side, an SEO sprint to capture organic search traffic. On the fitness side, starting a strength training program to complement cycling.
|
||||
|
||||
## Highlights
|
||||
|
||||
- Shipped [[25q1-laputa-v1]] — four-panel layout, markdown rendering, frontmatter-driven navigation, and a working search. Using it daily for my own vault
|
||||
- Executed [[25q1-rate-increase]]: raised Gold sponsorship from EUR 2.8k to EUR 3.5k per issue — retained 90% of sponsors, [[measure-sponsorship-mrr]] jumped to EUR 14.2k
|
||||
- Launched [[25q1-referral-program]] with tiered rewards — drove 3,200 referral subscribers in Q1
|
||||
- [[25q1-newsletter-seo-sprint]]: optimized 15 pillar articles for search, added internal linking structure — organic traffic up 40% by March
|
||||
- Started [[25q1-strength-program]]: 3x/week gym sessions focusing on core and legs — noticeable improvement in climbing power
|
||||
- Newsletter reached 56k subscribers by end of March
|
||||
- [[person-matteo-cellini]] onboarded two new enterprise sponsors at the higher rate with no pushback
|
||||
- Podcast backlog strategy: released "best of Season 1 & 2" compilation episodes during recording break — maintained download numbers
|
||||
|
||||
## Reflections
|
||||
|
||||
The rate increase was the decision I was most nervous about. Raising prices always feels like you're testing whether the value you think you provide matches what the market believes. But [[person-matteo-cellini]] was right — we were underpriced. When sponsors didn't blink at the new rate, it was a clear signal we could have moved sooner. Lesson learned: if you're anxious about raising prices, you're probably already late.
|
||||
|
||||
Laputa v1 was a labor of love. Spending evenings and weekends building a tool that only I use might seem indulgent, but it's the most energized I've felt about a coding project in years. There's something deeply satisfying about building software for a problem you understand intimately. The vault has 9,000+ files now, and seeing them organized in my own four-panel UI — types, relationships, properties — feels like the PKM tool I've been waiting for.
|
||||
|
||||
The strength program was a humbling start. Years of cycling had given me good cardio but embarrassingly weak upper body strength. The first month was all DOMS and ego management. By March, though, I could feel the difference on the bike — more stable in the saddle, better power transfer on steep gradients. The [[2025-ride-stelvio]] goal feels more realistic with this foundation. Big quarter overall. [[2025]] is off to a strong start.
|
||||
|
||||
@@ -1,38 +0,0 @@
|
||||
---
|
||||
aliases: ["Cycling Trip: Dolomites"]
|
||||
Is A: Project
|
||||
Belongs to: "[[25q2]]"
|
||||
Advances: "[[responsibility-health-fitness]]"
|
||||
Status: Done
|
||||
Owner: "[[person-luca-rossi]]"
|
||||
---
|
||||
# Cycling Trip: Dolomites
|
||||
|
||||
## Overview
|
||||
|
||||
This was the centerpiece event of the 2025 cycling season — a 5-day cycling trip through the Dolomites with [[person-paco-furiani]], tackling two of cycling's most legendary climbs: the Stelvio and the Mortirolo. The trip totaled approximately 650km with over 12,000m of elevation gain, and it represented the culmination of the training work from [[25q1-strength-program]] and the 2025 season plan outlined in [[24q4-cycling-year-review]].
|
||||
|
||||
The trip was planned as both a personal challenge and a goal completion event for [[2025-ride-stelvio]]. Route planning, accommodation booking, and logistics were handled in April-May, with the ride itself taking place in the second half of June when the high-altitude passes are reliably open and the weather is most favorable. Each day featured one major climb plus connecting roads through the valleys.
|
||||
|
||||
## Goals
|
||||
|
||||
- Complete the Stelvio from Bormio (the hardest side: 21.5km, 1,533m elevation gain, avg 7.1% gradient)
|
||||
- Complete the Mortirolo from Mazzo (12.4km, 1,300m elevation gain, avg 10.5% gradient)
|
||||
- Ride 5 consecutive days with an average of 130km and 2,400m elevation per day
|
||||
- Maintain a sustainable pace throughout — no bonking, no mechanical failures, no injuries
|
||||
- Document the trip with photos and notes for the vault and a potential newsletter piece
|
||||
|
||||
## Key decisions
|
||||
|
||||
- **Stelvio from Bormio, not Prato.** The Bormio side is considered the hardest but also the most dramatic — 48 hairpin turns through alpine scenery. The Prato side is more famous (classic from the Giro d'Italia) but less challenging. Chose difficulty over fame.
|
||||
- **Self-supported, not organized tour.** Rather than joining a commercial cycling tour, planned the trip independently. This allowed full control over pacing, rest stops, and daily distance. Carried minimal gear (jersey pockets + a small saddle bag) and relied on valley-town hotels rather than mountain huts.
|
||||
- **Rest day strategy: active recovery on day 3.** Rather than a full rest day mid-trip, did a short flat recovery ride (50km, no significant climbing) on day 3. This kept the legs moving and prevented the stiffness that comes from a complete rest day mid-tour.
|
||||
|
||||
## Notes
|
||||
|
||||
- The Stelvio was everything expected and more. Completed the climb in 2:08, which was within the target window (sub 2:15). The final 5km above the tree line, with the glacier visible ahead, were genuinely transcendent. The strength training from [[25q1-strength-program]] made a noticeable difference — upper body and core held up far better than they would have a year ago.
|
||||
- The Mortirolo was harder than the Stelvio in terms of pure suffering. The sustained 10%+ gradients for 12km with minimal respite are relentless. Finished but it required every ounce of mental discipline. [[person-paco-furiani]] later said it was the hardest thing he has done on a bike.
|
||||
- Nutrition strategy (eating every 25 minutes, starting from the first pedal stroke) worked perfectly. No bonking episodes across all 5 days. This was a direct application of the lesson from [[24q2-spring-gran-fondo]].
|
||||
- Day 4 featured unexpected rain on the descent from Passo Gavia, which made the wet hairpins genuinely nerve-wracking. Descended very conservatively (brake check every turn) and lost about 40 minutes compared to the planned schedule. Safety over speed — always.
|
||||
- The trip generated a newsletter issue ("What Climbing the Stelvio Taught Me About Endurance") that became one of the most engaged pieces of the year. The personal story format resonated strongly with readers, even those who do not cycle.
|
||||
- [[measure-cycling-km-per-month]] for June peaked at 1,450km — the highest monthly total ever. The Dolomites trip alone contributed 650km of that.
|
||||
@@ -1,38 +1,19 @@
|
||||
---
|
||||
aliases: ["Laputa App V2"]
|
||||
Is A: Project
|
||||
Belongs to: "[[25q2]]"
|
||||
Advances: "[[responsibility-learning]]"
|
||||
Status: Done
|
||||
Owner: "[[person-luca-rossi]]"
|
||||
type: Project
|
||||
aliases:
|
||||
- "[[Laputa App V2]]"
|
||||
belongs_to: "[[25q2]]"
|
||||
owner: "[[person-luca-rossi]]"
|
||||
status: Active
|
||||
related_to:
|
||||
- "[[laputa-qa-reference]]"
|
||||
---
|
||||
|
||||
# Laputa App V2
|
||||
|
||||
## Overview
|
||||
The active polish project used for current QA, especially richer editing, keyboard navigation, and attachment rendering.
|
||||
|
||||
V2 of Laputa was a major iteration that addressed the rough edges from [[25q1-laputa-v1]] and introduced three headline features: a BlockNote-based rich text editor replacing the raw CodeMirror setup, wiki-link autocomplete for seamless navigation between notes, and a completely redesigned theme system with CSS custom properties. These changes transformed the app from a functional prototype into something that feels genuinely pleasant to use daily.
|
||||
- Tightened the editor interaction model.
|
||||
- Reduced friction in wikilink navigation.
|
||||
- Uses [[laputa-qa-reference]] as a lightweight visual reference note.
|
||||
|
||||
The BlockNote integration was the most significant architectural change — it provides a block-based editing experience similar to Notion while preserving the underlying markdown format. Wiki-link autocomplete (triggered by typing `[[`) searches across all vault files and inserts links with proper display names. The theme system uses design tokens defined as CSS custom properties, making it straightforward to adjust the visual style without touching component code. This milestone continues the path toward [[2025-ship-laputa]].
|
||||
|
||||
## Goals
|
||||
|
||||
- Replace the basic CodeMirror editor with BlockNote for a block-based editing experience
|
||||
- Implement wiki-link autocomplete with fuzzy search across all vault files
|
||||
- Redesign the theme system using CSS custom properties and a centralized token file
|
||||
- Improve the property inspector with inline editing for all frontmatter field types
|
||||
- Fix the top 10 UX issues reported during V1 daily use
|
||||
|
||||
## Key decisions
|
||||
|
||||
- **BlockNote over plain CodeMirror.** While CodeMirror 6 is excellent for code editing, the reveal-on-focus model proved too jarring for prose-heavy content. BlockNote provides a smoother editing experience where formatting is always visible and blocks can be manipulated as units. The trade-off is less control over the raw markdown, but for the primary use case (structured notes with frontmatter), this is acceptable.
|
||||
- **CSS custom properties, not a CSS-in-JS theme.** Evaluated styled-components and Tailwind CSS theme approaches but chose plain CSS custom properties. They are natively supported, have zero runtime cost, can be inspected in browser DevTools, and align with the project philosophy of preferring simple, standard approaches over framework-dependent abstractions.
|
||||
- **Wiki-link search index prebuilt at startup.** The autocomplete needs to search across all ~9,200 files in sub-50ms. Rather than querying the filesystem on each keystroke, the Rust backend builds a search index at startup (including file names, aliases, and titles) and the frontend queries this index. Incremental updates happen when files change.
|
||||
|
||||
## Notes
|
||||
|
||||
- The BlockNote integration took longer than expected — about 3 weeks of focused work. The main challenge was ensuring that markdown round-tripping (markdown to BlockNote blocks to markdown) preserved formatting perfectly. Several edge cases with nested lists and code blocks required custom serializers.
|
||||
- Wiki-link autocomplete was the most impactful feature from a daily use perspective. Being able to type `[[` and instantly find any note in the vault by name, alias, or title makes the linking experience far superior to Obsidian's implementation (which does not search aliases by default).
|
||||
- [[person-david-kim]] continued to provide UX feedback, particularly around keyboard navigation patterns. His suggestion to add Vim-style `j/k` navigation in the note list was implemented and feels natural for power users.
|
||||
- The theme redesign consolidated about 200 scattered color values into 45 semantic design tokens. This made it trivial to create a dark mode variant (completed in a single afternoon) and establishes a foundation for future theme customization.
|
||||
- Performance remained solid despite the architectural changes. Note switching is still sub-200ms, and the BlockNote editor handles files up to 5,000 lines without noticeable lag. Memory usage increased by about 15% compared to V1 due to BlockNote's richer DOM, which is acceptable.
|
||||
- The V2 release was the point where Laputa became genuinely preferable to Obsidian for daily vault management. Feeds into [[25q4-laputa-v3]] for the next major milestone.
|
||||
|
||||
@@ -1,38 +0,0 @@
|
||||
---
|
||||
aliases: ["Podcast Season 3"]
|
||||
Is A: Project
|
||||
Belongs to: "[[25q2]]"
|
||||
Advances: "[[responsibility-podcast]]"
|
||||
Status: Done
|
||||
Owner: "[[person-luca-rossi]]"
|
||||
---
|
||||
# Podcast Season 3
|
||||
|
||||
## Overview
|
||||
|
||||
Season 3 expanded the Refactoring podcast to 10 episodes with a thematic focus on building in public, founder journeys, and product-led growth. After two seasons focused on engineering leadership and org design, this season shifted toward the entrepreneurial side of the audience — developers who are building products, running side projects, or considering the leap to full-time founder. The theme resonated strongly, producing the highest average downloads per episode of any season so far.
|
||||
|
||||
Production was now a well-oiled operation: [[person-sara-ricci]] handled post-production, [[person-paco-furiani]] managed guest scheduling and logistics, and [[person-luca-rossi]] focused purely on guest selection, interview preparation, and hosting. The workflows from [[procedure-podcast-recording]] and [[procedure-podcast-editing]] continued to function with only minor updates.
|
||||
|
||||
## Goals
|
||||
|
||||
- Record and publish 10 episodes on building in public, founder journeys, and product-led growth
|
||||
- Secure at least 3 guests who are active indie hackers or solopreneurs with public revenue data
|
||||
- Grow average episode downloads to 3,000 (up from Season 2's ~2,100)
|
||||
- Introduce YouTube video for 3 select episodes as an experiment
|
||||
- Generate at least 5 newsletter issues from podcast conversations (repurposed content)
|
||||
|
||||
## Key decisions
|
||||
|
||||
- **10 episodes, up from 8.** The seasonal model allows for flexibility in episode count. 10 episodes fit cleanly into the Q2 window (April-June) with a weekly release schedule. The extra two episodes justified by the stronger guest pipeline and revenue from podcast sponsorships.
|
||||
- **Selective video for 3 episodes.** Rather than doing video for all episodes (too much production overhead) or none (missed YouTube opportunity), selected 3 episodes with the most visually engaging guests for video. This tests the YouTube channel without committing to full video production.
|
||||
- **Founder-focused theme, not pure engineering.** The shift toward entrepreneurial topics was a deliberate audience expansion move. Refactoring's readership increasingly includes developers who are also founders or aspire to be. This theme serves them while maintaining technical credibility through the "building" angle.
|
||||
|
||||
## Notes
|
||||
|
||||
- Average downloads per episode reached 3,400, significantly exceeding the 3,000 target. The standout episode was a conversation with an indie hacker who publicly shared their journey from $0 to $25K MRR, which was shared widely on Twitter/X and LinkedIn.
|
||||
- The 3 video episodes performed well on YouTube (~4,000-6,000 views each), which is a promising signal for a channel with no prior content. The production cost was manageable because Riverside.fm captures both audio and video in the same recording session.
|
||||
- [[person-david-kim]] appeared as a guest for an episode on developer tool startups. His insights on distribution in the developer tools market were among the most practical advice shared on the show.
|
||||
- Content repurposing worked exceptionally well this season. 6 newsletter issues drew directly from podcast conversations (exceeding the 5-issue target), and the cross-promotion between newsletter and podcast continued to be the primary growth engine for [[measure-podcast-downloads]].
|
||||
- Podcast sponsorship revenue reached $2,500/month during the season, representing meaningful incremental revenue on top of newsletter sponsorships. The combined sponsorship revenue contributes significantly to [[2025-reach-22k-mrr]].
|
||||
- Season 3 was the first season where the podcast felt like it was pulling its own weight as a growth channel, not just a newsletter supplement. This has implications for resource allocation in [[25q3-podcast-season-4]].
|
||||
@@ -1,38 +0,0 @@
|
||||
---
|
||||
aliases: ["Reach 70k Subscribers"]
|
||||
Is A: Project
|
||||
Belongs to: "[[25q2]]"
|
||||
Advances: "[[responsibility-grow-newsletter]]"
|
||||
Status: Done
|
||||
Owner: "[[person-luca-rossi]]"
|
||||
---
|
||||
# Reach 70k Subscribers
|
||||
|
||||
## Overview
|
||||
|
||||
This project was a focused growth sprint to push [[measure-subscribers]] from approximately 60K to 70K during [[25q2]]. The strategy combined two approaches: amplifying the referral program launched in [[25q1-referral-program]] with targeted campaigns, and establishing cross-promotion partnerships with 3 complementary newsletters in adjacent spaces (startup operations, product management, and design leadership).
|
||||
|
||||
The 70K milestone was significant both psychologically (a round number that signals scale) and commercially (larger audience commands higher sponsorship rates and opens doors to enterprise-tier sponsors). The sprint ran from April through June and involved weekly tracking against a linear growth trajectory to catch underperformance early.
|
||||
|
||||
## Goals
|
||||
|
||||
- Grow from ~60K to 70K subscribers by end of June 2025
|
||||
- Drive 4,000+ subscribers through the referral program (with targeted campaigns and incentive boosts)
|
||||
- Establish 3 newsletter cross-promotion partnerships with reciprocal recommendation swaps
|
||||
- Maintain subscriber quality: 30-day retention rate above 75% for new subscribers
|
||||
- Achieve the milestone while keeping cost-per-acquisition below $1.00
|
||||
|
||||
## Key decisions
|
||||
|
||||
- **Cross-promotion over paid acquisition.** Considered investing in paid newsletter ads (Sparkloop's partner network, Twitter ads), but the economics favored cross-promotion. A reciprocal recommendation swap with a complementary newsletter costs nothing and generates high-quality subscribers who are already newsletter readers. Paid acquisition was kept as a backup if organic channels underperformed.
|
||||
- **Three partners, carefully selected.** Rather than doing many low-quality cross-promotions, invested time in selecting three newsletters where the audience overlap is genuine but not complete. The partners were a startup operations newsletter (~40K subscribers), a product management newsletter (~55K subscribers), and a design leadership newsletter (~30K subscribers). Each introduces Refactoring to a slightly different but adjacent audience.
|
||||
- **Weekly tracking cadence.** Set up a weekly check against a linear growth trajectory (~2,500 new subscribers/month needed). This allowed early detection of underperformance and mid-course corrections. In week 3, growth was tracking below target, which led to an additional referral incentive push that course-corrected by week 5.
|
||||
|
||||
## Notes
|
||||
|
||||
- Final subscriber count at end of June: 71,200 — slightly above the 70K target. The growth distribution was approximately: cross-promotions (3,800), referral program (3,500), organic search (2,200), social media (800), other (900).
|
||||
- Cross-promotion partnerships were the highest-quality growth channel. Subscribers from cross-promotions had a 30-day retention rate of 82%, the highest of any acquisition channel. This makes sense — they were already engaged newsletter readers, just not yet aware of Refactoring.
|
||||
- The referral program saw a significant boost from a "Spring Referral Challenge" campaign run in May, where the rewards were temporarily enhanced (extra rewards at the 5-referral tier). This generated a spike of ~1,200 new subscribers in a single week.
|
||||
- [[person-elena-rossi]] helped design the cross-promotion creative (email copy and graphics) for the partner newsletters. Her marketing background was valuable for crafting the recommendation copy that appeared in partner newsletters.
|
||||
- Cost-per-acquisition across all channels was $0.62, well below the $1.00 target. The primary costs were SparkLoop subscription, referral rewards, and the time spent on partnership management.
|
||||
- The 70K milestone was used in the updated media kit and referenced in sponsorship conversations, directly supporting the rate increases and new sponsor acquisition feeding into [[2025-reach-22k-mrr]].
|
||||
@@ -1,37 +0,0 @@
|
||||
---
|
||||
aliases: ["Team Retreat Milan"]
|
||||
Is A: Project
|
||||
Belongs to: "[[25q2]]"
|
||||
Advances: "[[responsibility-team-management]]"
|
||||
Status: Done
|
||||
Owner: "[[person-luca-rossi]]"
|
||||
---
|
||||
# Team Retreat Milan
|
||||
|
||||
## Overview
|
||||
|
||||
After more than a year of working together remotely, the Refactoring team gathered for its first in-person retreat in Milan. The retreat brought together [[person-luca-rossi]], [[person-matteo-cellini]], [[person-paco-furiani]], and [[person-sara-ricci]] for two days of strategy sessions, workshops, and team bonding. The purpose was to align on the business direction for the second half of 2025, address operational friction points, and build the personal relationships that make remote collaboration smoother.
|
||||
|
||||
The retreat was planned for early May, timed to fall between Season 3 podcast production and the summer growth push. The agenda was deliberately balanced between structured work sessions (morning) and unstructured social time (afternoon/evening). Milan was chosen for its central location relative to all team members.
|
||||
|
||||
## Goals
|
||||
|
||||
- Conduct a half-day business strategy session covering H2 2025 priorities and goals
|
||||
- Run a workflow retrospective: identify and resolve the top 3 operational friction points
|
||||
- Host a creative workshop on content format experimentation for H2
|
||||
- Provide space for unstructured team bonding (dinner, activities)
|
||||
- Document decisions and action items for follow-up within 1 week
|
||||
|
||||
## Key decisions
|
||||
|
||||
- **Two days, not three.** Considered a 3-day retreat but decided that two full days (plus a team dinner the evening before) provided enough time for meaningful work without pulling everyone away from their schedules for too long. Especially for a small team where everyone wears multiple hats, minimizing away-from-work time matters.
|
||||
- **Strategy session first, social second.** Ran the strategy and retrospective sessions on the first day (when energy and focus are highest) and the creative workshop + social activities on the second day. This ensured the most important decisions got the best thinking.
|
||||
- **External facilitator for the retrospective.** Rather than running the retrospective internally, brought in [[person-giulia-conti]] (who has experience facilitating team sessions) to moderate. Having an external voice made it easier for team members to surface honest feedback without the founder-employee dynamic filtering the conversation.
|
||||
|
||||
## Notes
|
||||
|
||||
- The retrospective produced the most actionable outcomes. The top 3 friction points identified were: (1) newsletter draft handoff timing (Sara needs more lead time), (2) sponsor pipeline visibility (Matteo wants to see content calendar earlier for pitch alignment), and (3) podcast scheduling conflicts (Paco needs more advance notice for guest confirmations). All three had clear action items assigned.
|
||||
- The strategy session confirmed H2 priorities: community launch ([[25q3-community-launch]]), continued Laputa development ([[25q4-laputa-v3]]), and the push toward [[2025-reach-85k-subscribers]]. There was a genuine debate about whether to invest in a premium podcast (higher production value, video-first) versus the community. Decided on community as the higher-leverage bet.
|
||||
- The creative workshop generated several content format ideas for H2, including a "Refactoring Case Studies" series (deep dives into real company engineering decisions) and a "Founder Metrics" segment sharing real revenue/growth numbers. Both were added to the content roadmap.
|
||||
- Team dinner was at a classic Milanese trattoria near the Navigli. The informal conversation over dinner revealed a lot about individual motivations and career aspirations that would never surface in a video call. [[person-matteo-cellini]] mentioned wanting to eventually run his own content business, which was valuable context for thinking about his long-term role.
|
||||
- Total cost: approximately 2,800 EUR (hotel, meals, workspace rental, facilitator). Very reasonable for the alignment value produced. Planning to make this a biannual event (spring and autumn).
|
||||
@@ -1,32 +1,13 @@
|
||||
---
|
||||
aliases: ["Q2 2025"]
|
||||
Is A: Quarter
|
||||
Created at: "2025-04-01"
|
||||
Belongs to: "[[2025]]"
|
||||
Has: ["[[25q2-reach-70k]]", "[[25q2-podcast-season-3]]", "[[25q2-team-retreat]]", "[[25q2-laputa-v2]]", "[[25q2-dolomites-trip]]"]
|
||||
Status: Done
|
||||
type: Quarter
|
||||
aliases:
|
||||
- "[[Q2 2025]]"
|
||||
status: Active
|
||||
has:
|
||||
- "[[25q2-laputa-v2]]"
|
||||
---
|
||||
|
||||
# Q2 2025
|
||||
|
||||
## Focus
|
||||
The polish cycle focused on richer editing, faster linking, and better keyboard QA.
|
||||
|
||||
Q2 was the heart of the execution phase for 2025. The 70k subscriber milestone was the key growth target. Season 3 of the podcast brought a new format — shorter episodes, more actionable. The team retreat in Tuscany was about alignment and bonding. And the Dolomites cycling trip was both training for Stelvio and pure joy.
|
||||
|
||||
## Highlights
|
||||
|
||||
- Hit [[25q2-reach-70k]] subscribers in early June — two weeks ahead of schedule, driven by viral LinkedIn posts and the referral program compounding
|
||||
- Launched [[25q2-podcast-season-3]] with a new 15-minute "Tactical Tuesday" format alongside the regular deep dives — downloads surged to 14k/month
|
||||
- Held [[25q2-team-retreat]] in a farmhouse outside Siena with [[person-matteo-cellini]], [[person-paco-furiani]], and [[person-sara-ricci]] — aligned on H2 roadmap, built real trust
|
||||
- Shipped [[25q2-laputa-v2]] with bidirectional linking, graph view, and improved frontmatter editing — starting to feel like a real product
|
||||
- Completed [[25q2-dolomites-trip]]: 4 days, 5 passes, 380 km — Passo Fedaia, Pordoi, Sella, Gardena, Campolongo
|
||||
- [[measure-sponsorship-mrr]] hit EUR 17.5k — tracking well toward [[2025-reach-22k-mrr]]
|
||||
- Premium tier reached 680 paying subscribers, up from 420 at start of quarter
|
||||
- Two pillar articles syndicated by major tech publications, driving ~4k new subscribers
|
||||
|
||||
## Reflections
|
||||
|
||||
The team retreat was the highlight of the quarter, and maybe the most important thing we did all year. Working remotely with contractors is efficient, but you miss the informal conversations that build real relationships. Three days in Tuscany — cooking together, long walks, whiteboard sessions on the terrace — transformed how we collaborate. [[person-sara-ricci]] pitched the community idea over dinner on the second night, and it was so obvious in hindsight that we all wondered why we hadn't done it sooner. That became [[25q3-community-launch]].
|
||||
|
||||
Reaching 70k subscribers felt almost routine, which is itself remarkable. A year ago, every thousand felt hard-won. Now the flywheel is spinning: content drives subscribers, subscribers attract sponsors, sponsors fund better content. The referral program is the turbocharger — word-of-mouth from engineering leaders who trust their peers' recommendations.
|
||||
|
||||
The Dolomites trip was pure magic. Four days of riding through the most beautiful mountain landscape in Europe with three friends. Passo Fedaia at sunset, the Marmolada glacier glowing overhead — these are the moments that make the 5am training rides worth it. My legs are ready for [[2025-ride-stelvio]] in August. Bring it on.
|
||||
|
||||
@@ -1,38 +0,0 @@
|
||||
---
|
||||
aliases: ["Launch Refactoring Community"]
|
||||
Is A: Project
|
||||
Belongs to: "[[25q3]]"
|
||||
Advances: "[[responsibility-grow-newsletter]]"
|
||||
Status: Open
|
||||
Owner: "[[person-luca-rossi]]"
|
||||
---
|
||||
# Launch Refactoring Community
|
||||
|
||||
## Overview
|
||||
|
||||
This project is building a private Discord community for Refactoring's most engaged readers — a space where engineering leaders, indie hackers, and technical founders can connect, share challenges, and learn from each other beyond the one-way format of the newsletter and podcast. The community concept was validated during the [[25q2-team-retreat]] strategy session and represents the most significant new product surface since the podcast launch.
|
||||
|
||||
The approach is a soft launch with a small, curated initial cohort rather than opening the doors to all subscribers at once. The [[25q3-discord-community-soft]] experiment is testing engagement patterns, channel structure, and moderation needs with approximately 150 founding members before a broader rollout. The community is free for now, with potential for a paid tier once the value proposition is validated. This is a fundamentally different approach from the failed [[24q3-premium-tier]] — the focus is on community value, not content gating.
|
||||
|
||||
## Goals
|
||||
|
||||
- Set up the Discord server with initial channel structure (introductions, weekly discussions, resource sharing, career advice, project showcases)
|
||||
- Recruit 150 founding members from the most engaged newsletter subscribers (based on open rate, click-through, and referral data)
|
||||
- Establish community guidelines, moderation policies, and a code of conduct
|
||||
- Host 2 live events during the soft launch period (AMA sessions with podcast guests)
|
||||
- Achieve a weekly active member rate of at least 40% during the soft launch
|
||||
|
||||
## Key decisions
|
||||
|
||||
- **Discord, not Circle or Slack.** Evaluated all three platforms. Discord offers the best combination of real-time and async communication, has robust moderation tools, and most of the target audience already has accounts. Circle has a cleaner UX but lacks real-time chat. Slack is too work-associated — people do not want another Slack workspace.
|
||||
- **Soft launch with curated cohort, not open launch.** The lesson from [[24q3-premium-tier]] was that launching to everyone at once creates a spike-and-fade pattern. A soft launch with hand-selected engaged members builds genuine activity and social proof before scaling. The founding members become community champions.
|
||||
- **Free entry, no paywall yet.** The community needs to prove its value before asking members to pay. Charging too early kills participation momentum. Will evaluate a paid tier once weekly active member rate stabilizes above 40% and there is clear demand for premium features (private channels, exclusive events).
|
||||
|
||||
## Notes
|
||||
|
||||
- The soft launch began in late July with 150 invitations sent to subscribers identified through engagement scoring. Acceptance rate was 72% (108 active members), which is strong for a community invitation.
|
||||
- Early engagement is promising but uneven. The "weekly discussion" channel (a prompted discussion thread posted every Monday) generates the most activity. Organic conversation in open channels is still sparse — this is the typical early-community challenge.
|
||||
- The two AMA sessions (with guests from [[25q2-podcast-season-3]]) were well-attended (~60 members each) and generated follow-up discussions that lasted several days. Live events appear to be the strongest engagement driver.
|
||||
- [[person-paco-furiani]] is handling day-to-day community moderation. His operational experience and even temperament make him well-suited for this role. The time commitment is currently about 3 hours/week.
|
||||
- The biggest risk is that the community becomes another content channel (one-to-many) rather than a genuine peer space (many-to-many). Actively encouraging member-initiated discussions and resisting the urge to "fill the silence" with content is important.
|
||||
- This project feeds into the broader question of whether the community becomes a revenue stream or remains a free engagement tool. Will evaluate after the soft launch period ends in [[25q4]].
|
||||
@@ -1,38 +0,0 @@
|
||||
---
|
||||
aliases: ["Discord Community Soft Launch"]
|
||||
Is A: Experiment
|
||||
Belongs to: "[[25q3]]"
|
||||
Status: Open
|
||||
Owner: "[[person-luca-rossi]]"
|
||||
---
|
||||
# Discord Community Soft Launch
|
||||
|
||||
Soft-launched a Discord community for the Refactoring newsletter with an initial cohort of 50 beta subscribers. The goal is to validate whether a private community can drive meaningful engagement, reduce churn, and eventually become a monetizable product tied to [[25q3-community-launch]]. This experiment directly informs whether community is the right next revenue line beyond sponsorships.
|
||||
|
||||
## Hypothesis
|
||||
|
||||
A curated Discord community with 50 hand-picked beta members will achieve at least 40% weekly active participation (defined as posting or reacting at least once per week) over the first 8 weeks, and at least 70% of members will express willingness to pay $20/month for continued access.
|
||||
|
||||
## Setup
|
||||
|
||||
- Invited 50 beta members from the most engaged newsletter subscribers (high open rate, frequent replies, active on social).
|
||||
- Created a minimal channel structure: #introductions, #general, #content-ideas, #career-advice, #weekly-thread.
|
||||
- [[person-luca-rossi]] posts a weekly discussion prompt every Monday tied to that week's newsletter topic.
|
||||
- [[person-paco-furiani]] moderates and tracks engagement metrics.
|
||||
- Running a survey at week 4 and week 8 to gauge willingness to pay and feature requests.
|
||||
|
||||
## Results
|
||||
|
||||
- Currently in progress. Early signals after 3 weeks:
|
||||
- Weekly active participation: ~52% (above the 40% target so far).
|
||||
- Most active channels: #career-advice and #general. #content-ideas is quieter than expected.
|
||||
- Three members have organically started peer accountability threads (unplanned, positive signal).
|
||||
- No dropoffs yet, though engagement tends to decay after the novelty period.
|
||||
|
||||
## Takeaways
|
||||
|
||||
- Early results are encouraging but it is too soon to draw conclusions. The real test is whether engagement sustains past week 6.
|
||||
- The #career-advice channel suggests the community's core value may be peer networking rather than content discussion. Worth leaning into.
|
||||
- If the willingness-to-pay survey at week 8 confirms demand, this becomes the foundation for [[25q3-community-launch]] and a potential path to the [[2025-reach-22k-mrr]] goal.
|
||||
- Lessons from the [[25q1-paid-newsletter-trial]] apply here: the value proposition must be access and connection, not more content.
|
||||
- Key risk: community management overhead. Need to assess whether [[person-paco-furiani]] can handle moderation long-term or if additional support is needed from [[responsibility-team-management]].
|
||||
@@ -1,38 +0,0 @@
|
||||
---
|
||||
aliases: ["Write Newsletter Growth E-book"]
|
||||
Is A: Project
|
||||
Belongs to: "[[25q3]]"
|
||||
Advances: "[[responsibility-content-production]]"
|
||||
Status: Done
|
||||
Owner: "[[person-luca-rossi]]"
|
||||
---
|
||||
# Write Newsletter Growth E-book
|
||||
|
||||
## Overview
|
||||
|
||||
This project produced a 15,000-word e-book titled "The Newsletter Growth Playbook for Technical Founders" — a comprehensive guide distilling the Refactoring growth story, strategies, and lessons into a free downloadable resource. The e-book serves as a high-value lead magnet for subscriber acquisition and establishes authority in the growing "technical content creator" space.
|
||||
|
||||
The e-book drew heavily from existing content — the Codemotion talk from [[24q3-codemotion-talk]], pillar articles from [[24q2-10-pillar-articles]], and newsletter issues that covered growth topics throughout 2024. Rather than writing everything from scratch, the project assembled, updated, and expanded existing material into a cohesive narrative. [[person-sara-ricci]] edited the full manuscript over two rounds, and [[person-giulia-conti]] designed the cover and interior layout.
|
||||
|
||||
## Goals
|
||||
|
||||
- Write a 15,000-word e-book covering newsletter strategy, growth tactics, and monetization for technical audiences
|
||||
- Structure in 8 chapters with actionable takeaways and real data from the Refactoring journey
|
||||
- Design a professional PDF layout with custom cover art and clean typography
|
||||
- Set up a landing page with email capture for the download (new subscribers get the e-book as a welcome gift)
|
||||
- Generate 3,000+ new subscribers through the e-book in the first 3 months
|
||||
|
||||
## Key decisions
|
||||
|
||||
- **Free, not paid.** The primary goal is subscriber acquisition, not direct revenue. A free e-book with an email gate generates far more subscribers than a $9.99 paid e-book would generate in revenue. The subscriber lifetime value (through sponsorship revenue) far exceeds any reasonable e-book price.
|
||||
- **Existing content as foundation, not writing from scratch.** About 60% of the e-book content was adapted from existing newsletter issues, blog posts, and the Codemotion talk transcript. This reduced the writing time from an estimated 6 weeks to about 3 weeks while producing a more proven and polished result (the content had already been validated with the audience).
|
||||
- **PDF format, not a Substack post or blog series.** A designed PDF feels more substantial and "worth downloading" than a blog series. It also creates a sense of exclusivity and ownership that a public web page does not. The design investment was modest but made the resource feel premium.
|
||||
|
||||
## Notes
|
||||
|
||||
- The writing process was faster than expected. The structure emerged naturally from grouping existing content into themes: chapters on audience identification, content strategy, distribution, SEO, sponsorship, and community. New writing focused mainly on connecting threads and adding updated data.
|
||||
- [[person-sara-ricci]]'s editing was particularly valuable for the e-book format. Newsletter writing can be casual and conversational; a longer-form document requires more careful structure and flow. She restructured two chapters significantly and improved the opening section.
|
||||
- [[person-giulia-conti]]'s cover design was clean and professional — a minimalist style with the Refactoring brand colors. The interior layout used a two-column format with pull quotes and data callouts that made the dense content more scannable.
|
||||
- The landing page conversion rate is approximately 42% (visitors who enter email and download). This is above the benchmark for e-book lead magnets (~25-30%). The high conversion likely reflects the specificity of the topic and the trust already built through the newsletter brand.
|
||||
- At the 2-month mark, the e-book has generated approximately 2,300 new subscribers, tracking toward the 3,000 target. The e-book landing page is now promoted in the newsletter footer, on the podcast, and through social media. It has also been shared organically by readers, creating a compounding distribution effect.
|
||||
- The e-book content will likely need updating in 6-12 months as strategies evolve. Built the manuscript in markdown with a separate build script for PDF generation, making updates straightforward. Feeds into [[topic-content-strategy]].
|
||||
@@ -1,38 +0,0 @@
|
||||
---
|
||||
aliases: ["LeadDev London 2025"]
|
||||
Is A: Project
|
||||
Belongs to: "[[25q3]]"
|
||||
Advances: "[[responsibility-content-production]]"
|
||||
Status: Done
|
||||
Owner: "[[person-luca-rossi]]"
|
||||
---
|
||||
# LeadDev London 2025
|
||||
|
||||
## Overview
|
||||
|
||||
LeadDev London is one of the premier conferences for engineering leaders, and attending in 2025 served three purposes: participating in a panel on "Building Engineering Content Brands," meeting potential podcast guests and sponsors in person, and writing a post-conference analysis piece for the newsletter. The trip combined brand-building, relationship development, and content creation into a single, high-leverage event.
|
||||
|
||||
The conference took place in early September in London. Beyond the panel, the most valuable aspect was the hallway conversations — meeting people who have been reading the newsletter for years, connecting with potential sponsors face-to-face, and identifying guests for [[25q3-podcast-season-4]]. The post-conference newsletter issue became one of the highest-engagement pieces of the quarter.
|
||||
|
||||
## Goals
|
||||
|
||||
- Participate in the "Building Engineering Content Brands" panel discussion
|
||||
- Schedule at least 5 in-person meetings with current/potential sponsors during the conference
|
||||
- Identify and pitch 3+ potential guests for the upcoming podcast season
|
||||
- Write a post-conference analysis newsletter issue highlighting key themes and takeaways
|
||||
- Strengthen relationships with 10+ newsletter readers and community members attending the event
|
||||
|
||||
## Key decisions
|
||||
|
||||
- **Panel participation over keynote pitch.** Was offered the option to propose a keynote talk, but chose the panel format instead. A panel requires less preparation time, provides exposure to the other panelists' audiences, and positions Refactoring alongside other established content brands. The trade-off is less individual spotlight, but the networking value of sharing a stage with peers outweighs that.
|
||||
- **Three days, not just conference days.** Arrived one day before the conference for scheduled sponsor meetings and stayed one day after for follow-up conversations. The extra days doubled the relationship ROI of the trip.
|
||||
- **Post-conference piece as synthesis, not summary.** Rather than writing a play-by-play of conference talks (which dozens of attendees will do), wrote a synthesized analysis of the three biggest themes emerging across all talks. This is more valuable to readers and stands out in the content noise after any major conference.
|
||||
|
||||
## Notes
|
||||
|
||||
- The panel discussion went well. The other panelists represented a startup-focused newsletter (~80K subscribers), a podcast network, and a YouTube channel for developers. The conversation surfaced interesting differences in monetization strategies and audience engagement patterns. Several audience members approached afterward to discuss Refactoring's sponsorship model.
|
||||
- In-person sponsor meetings were extremely productive. Met with 6 sponsors (5 existing, 1 prospective), and the face-to-face time strengthened relationships in a way that video calls cannot. The prospective sponsor signed a 3-month Deep Dive package within a week of the conference. [[person-matteo-cellini]] prepared one-pagers for each meeting, which was a professional touch.
|
||||
- Identified 4 potential podcast guests, 3 of whom have since confirmed for [[25q3-podcast-season-4]]. In-person conversation is vastly more effective for podcast guest recruitment than cold email.
|
||||
- The post-conference newsletter issue ("Three Things I Learned at LeadDev London 2025") had the highest open rate of any issue in Q3 (48%) and generated significant discussion in the [[25q3-community-launch]] Discord. Conference analysis content consistently outperforms regular issues.
|
||||
- [[person-emma-wilson]] also attended and facilitated an introduction to several VPEs from UK-based scale-ups. These connections are valuable for both podcast guests and understanding the European engineering leadership landscape.
|
||||
- Total trip cost: approximately 1,800 GBP (flights, hotel, meals, conference ticket). The ROI through the new sponsor alone covered the trip cost several times over.
|
||||
@@ -1,38 +0,0 @@
|
||||
---
|
||||
aliases: ["Summer Cycling Peak Training"]
|
||||
Is A: Project
|
||||
Belongs to: "[[25q3]]"
|
||||
Advances: "[[responsibility-health-fitness]]"
|
||||
Status: Done
|
||||
Owner: "[[person-luca-rossi]]"
|
||||
---
|
||||
# Summer Cycling Peak Training
|
||||
|
||||
## Overview
|
||||
|
||||
This was the final intensive training block of the 2025 cycling season, preparing for a September gran fondo event. After the successful Dolomites trip in [[25q2-dolomites-trip]], this peak training block focused on sharpening form and building the high-end fitness needed for a competitive autumn event. The block ran from mid-July through late August, with the target event in the second week of September.
|
||||
|
||||
The training philosophy shifted from the high-volume base approach used earlier in the year to a high-intensity, lower-volume peak phase. Interval sessions became more specific (longer threshold efforts, race-simulation rides), while total weekly hours decreased to allow for recovery and adaptation. The strength training from [[25q1-strength-program]] continued in maintenance mode (2 sessions/week) to preserve the gains without adding fatigue.
|
||||
|
||||
## Goals
|
||||
|
||||
- Execute a 6-week peak training block with high-intensity focus
|
||||
- Hit a new season-best monthly volume in August (target: 1,400+ km)
|
||||
- Complete 3 race-simulation rides on courses with similar profile to the target event
|
||||
- Maintain FTP at or above 280W through the peak phase
|
||||
- Arrive at the target event rested and sharp (2-week taper before the event)
|
||||
|
||||
## Key decisions
|
||||
|
||||
- **Intensity over volume for the peak phase.** After building a strong aerobic base through the first half of the year (~6,000 km by end of June), the peak phase prioritized quality over quantity. Three key sessions per week: a VO2max interval session, a threshold ride, and a long endurance ride with race-pace segments. Total weekly hours dropped from 10-12 to 8-9.
|
||||
- **Two-week taper, not one.** Considered a shorter taper to maintain sharpness, but the Dolomites trip had left some residual fatigue. A two-week taper with gradually decreasing volume allowed full recovery while maintaining neuromuscular activation through short, sharp efforts.
|
||||
- **No new equipment or position changes.** Resisted the temptation to try new tires or adjust bike fit close to the event. Everything that worked in the Dolomites stays unchanged for the autumn event. Equipment experiments belong in the off-season.
|
||||
|
||||
## Notes
|
||||
|
||||
- August volume reached 1,420 km, hitting the target. The weekly structure of one high-intensity day, one threshold day, one long day, and recovery between worked well. The long rides (5-6 hours) on weekends were the most important sessions for event-specific preparation.
|
||||
- FTP tested at 282W in late August, a slight improvement from the 275W measured earlier in the year. The strength training may be contributing to this — improved core stability allows for more efficient power transfer, especially at higher intensities.
|
||||
- [[person-paco-furiani]] followed a similar peak block and they completed two of the three race-simulation rides together. Training with a partner on hard sessions added motivation and pacing discipline.
|
||||
- The main challenge during the peak block was managing fatigue alongside work commitments. August is a lighter newsletter period (similar reasoning as [[24q3-summer-reading-sprint]]), but podcast recording for [[25q3-podcast-season-4]] overlapped with the peak training weeks. Scheduling required careful coordination.
|
||||
- The 2-week taper felt luxurious but paid off. Arrived at the event feeling genuinely fresh — legs snappy, weight on target (71.5kg), and mentally eager. This is the best pre-race state achieved across all events so far.
|
||||
- [[measure-cycling-km-per-month]] for the full peak period (July-August): approximately 2,700 km combined. This brings the year-to-date total to approximately 9,500 km, on track for a year-end total exceeding 2024's 8,200 km.
|
||||
@@ -1,38 +0,0 @@
|
||||
---
|
||||
aliases: ["Podcast Season 4"]
|
||||
Is A: Project
|
||||
Belongs to: "[[25q3]]"
|
||||
Advances: "[[responsibility-podcast]]"
|
||||
Status: Open
|
||||
Owner: "[[person-luca-rossi]]"
|
||||
---
|
||||
# Podcast Season 4
|
||||
|
||||
## Overview
|
||||
|
||||
Season 4 tackles the most urgent topic in the engineering world right now: how AI is changing software engineering. With 8 planned episodes, the season explores the practical impact of AI on engineering teams — from coding assistants and AI-augmented code review to the shifting role of senior engineers and the organizational implications of dramatically increased developer productivity. The theme was selected during the [[25q2-team-retreat]] strategy session and reflects the audience's most-requested topic in community polls.
|
||||
|
||||
Production is following the established workflows from [[procedure-podcast-recording]] and [[procedure-podcast-editing]], with [[person-sara-ricci]] on post-production and [[person-paco-furiani]] on guest logistics. Three guests were recruited during [[25q3-leaddev-london]], and the remaining slots are being filled through network outreach. The season is currently in production with recording underway.
|
||||
|
||||
## Goals
|
||||
|
||||
- Record and publish 8 episodes on AI and its impact on software engineering
|
||||
- Secure guests who are actively building or deploying AI tools in engineering orgs (not just commentators)
|
||||
- Continue video production for select episodes (target: 4 out of 8 with video, up from 3 in Season 3)
|
||||
- Grow average episode downloads to 4,000 (up from Season 3's ~3,400)
|
||||
- Generate sponsor revenue of at least $3,000/month from podcast placements
|
||||
|
||||
## Key decisions
|
||||
|
||||
- **Practitioners over pundits.** The AI space is full of hot takes and hype. This season deliberately focuses on guests who are hands-on — CTOs implementing AI coding tools, engineers who have integrated AI into their workflows, and founders building AI developer tools. The practical angle differentiates from the noise.
|
||||
- **Expanded video to 4 episodes.** Season 3's video experiment showed promising YouTube numbers. Expanding to 4 video episodes (every other episode) increases YouTube content cadence without committing to full video production for every episode. The video episodes are selected based on guest visual engagement and topic appeal.
|
||||
- **AI tools as sponsors.** The season theme creates a natural alignment with AI tool sponsors. Actively targeting AI coding assistants and developer productivity tools for the podcast sponsorship slots. The topical alignment between content and sponsor should improve click-through rates.
|
||||
|
||||
## Notes
|
||||
|
||||
- Guest recruitment has been the strongest of any season. The AI topic is so timely that response rates on outreach are approximately 45% — nearly double Season 3. The challenge is now selecting the best guests rather than finding enough willing guests.
|
||||
- The first two recorded episodes are highly engaging. The opening episode, an interview with a CTO who replaced their entire QA team with AI-augmented testing, generated strong early reactions from the team during review. This kind of provocative-but-substantive content is what the season needs.
|
||||
- [[person-david-kim]] is returning as a guest for an episode on building AI developer tools — his perspective as both a builder and user of these tools bridges the practitioner-founder divide nicely.
|
||||
- Sponsor interest for the AI-themed season is strong. Two AI tool companies have committed to the full season (all 8 episodes), which secures the $3,000/month target. The thematic alignment between content and sponsor has made the pitch very natural.
|
||||
- The biggest production challenge is that AI topics evolve extremely fast. Episodes recorded in August may feel slightly dated by the time they air in October. Mitigating this by focusing conversations on principles and organizational dynamics rather than specific tool features.
|
||||
- This season, combined with the community discussions in [[25q3-community-launch]], is positioning Refactoring as a thought leader on AI's impact on engineering teams — a valuable positioning for both audience growth and sponsor value.
|
||||
@@ -1,34 +0,0 @@
|
||||
---
|
||||
aliases: ["Q3 2025"]
|
||||
Is A: Quarter
|
||||
Created at: "2025-07-01"
|
||||
Belongs to: "[[2025]]"
|
||||
Has: ["[[25q3-ebook]]", "[[25q3-community-launch]]", "[[25q3-podcast-season-4]]", "[[25q3-leaddev-london]]", "[[25q3-peak-training]]"]
|
||||
Status: Done
|
||||
---
|
||||
# Q3 2025
|
||||
|
||||
## Focus
|
||||
|
||||
Q3 was the most ambitious quarter yet. Three big launches — the ebook, the community, and podcast Season 4 — plus a keynote at LeadDev London and peak training for Stelvio. This was the quarter where I felt the tension between "scale everything" and "don't burn out." It worked, but just barely.
|
||||
|
||||
## Highlights
|
||||
|
||||
- Published [[25q3-ebook]] "The Engineering Leader's Playbook" — 12 chapters distilled from the best newsletter content, sold 1,400 copies in the first month at EUR 29
|
||||
- Launched [[25q3-community-launch]] on Circle with 850 founding members from the premium tier — weekly AMAs, peer groups, async Q&A
|
||||
- [[25q3-podcast-season-4]] opened with a special 3-part series on "Building Engineering Culture from Scratch" — 22k downloads in the first week
|
||||
- Delivered keynote at [[25q3-leaddev-london]] on "The Newsletter-to-Business Pipeline" to ~1,200 attendees — biggest stage yet
|
||||
- Completed [[25q3-peak-training]] block: 14 weeks of structured intervals, altitude simulation, and long weekend rides — FTP peaked at 285W
|
||||
- Newsletter hit 78k subscribers by end of September
|
||||
- [[measure-sponsorship-mrr]] reached EUR 20.1k — [[2025-reach-22k-mrr]] is close
|
||||
- Rode Stelvio from Bormio side in 1h52m — [[2025-ride-stelvio]] achieved
|
||||
|
||||
## Reflections
|
||||
|
||||
I'm not going to pretend this quarter was smooth. It was intense to the point of being unsustainable. Three major launches in 13 weeks, plus a keynote, plus peak cycling training — something had to give, and it was sleep. I averaged under 6 hours for most of August. [[person-paco-furiani]] flagged it during a 1:1, and he was right. I need to build more buffer into ambitious quarters.
|
||||
|
||||
That said, the outcomes were extraordinary. The ebook exceeded every expectation. I'd been skeptical about packaging newsletter content into a book — would people pay for what they'd already read for free? Turns out, curation and structure have enormous value. The people buying the ebook aren't the same people who read every issue; they want the distilled playbook, and they're happy to pay for it.
|
||||
|
||||
LeadDev London was a career highlight. Standing on that stage, looking out at 1,200 engineering leaders — many of whom I recognized from the newsletter subscriber list — was surreal. The talk went well, but the conversations afterward were even better. Three potential enterprise sponsors, two podcast guest leads, and a half-dozen people who said Refactoring changed how they lead their teams.
|
||||
|
||||
And then Stelvio. Climbing the most iconic pass in cycling, legs screaming, lungs burning, the summit appearing and disappearing behind switchbacks — it was everything I'd imagined. 1h52m from Bormio. Not fast by any serious cyclist's standard, but I didn't care. I made it. [[2025-ride-stelvio]] done. Now I need to rest — body and mind — before tackling Q4.
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user