A minimalist personal diary that lives inside your own GitHub repository.
No backend. No database. No server costs. Just you, your browser, and Git.
Git is your storage layer, not your backup layer.
- 🔐 You own your data — every entry is a plain Markdown file in your repo.
- 🪶 Zero infrastructure — a single Blazor WASM bundle, hostable on GitHub Pages.
- 🧘 Distraction-free — one page, one editor, one keystroke to save.
- 🕰 Full Git history — every diary edit is a commit. Time-travel included, for free.
- 🚫 No vendor lock-in — clone the repo,
catyour.mdfiles, walk away anytime.
| ✍️ Write | Daily entries, live Markdown editing |
| 💾 Autosave | 2-second debounce → IndexedDB → GitHub |
| 📅 Browse | Navigate history by year / month / day |
| 🔍 Search | Instant full-text search across every entry |
| 👀 Preview | Toggle between raw Markdown and rendered view |
| 📶 Offline | Keep writing without a network; sync resumes automatically |
| ⚔️ Conflicts | Detected on SHA mismatch — pick Overwrite or Reload |
| 🎨 Dark theme | Handcrafted CSS, no Bootstrap, no heavy UI kit |
+---------------------+
| Browser |
+----------+----------+
|
v
+---------------------+
| Blazor WASM App |
+----------+----------+
|
+----------------+
| |
v v
+----------------+ +----------------+
| IndexedDB | | GitHub REST |
| (drafts/cache) | | (source truth) |
+----------------+ +----------------+
Tech stack: Blazor WebAssembly (.NET 10) · Markdig · Blazored.LocalStorage · GitHub REST API · IndexedDB
Data layout in your repo:
Diary/
2026/
07/
12.md ← one Markdown file per day
Each file is just Markdown — no frontmatter, no proprietary schema:
# 2026-07-12
Today I set up GitDiary. It was surprisingly easy!This section walks you through going from zero to writing your first entry.
- Sign in to GitHub and go to Create a new repository.
- Fill in the form:
- Repository name: anything you like —
my-diaryis a good default. - Visibility: Private is strongly recommended (this repo will hold your personal writing).
- Initialize this repository with a README: ✅ tick it, so the default branch exists.
- Repository name: anything you like —
- Click Create repository.
- Note three values you'll need shortly:
Owner— your GitHub username or organization nameRepo— the repository name you just choseBranch— usuallymain
💡 You can use an existing repository too. GitDiary only writes inside a top-level
Diary/folder, so it won't collide with the rest of your files.
GitDiary talks to GitHub directly from your browser and needs a token with write access to that one repository.
- Open Settings → Developer settings → Personal access tokens → Fine-grained tokens.
- Click Generate new token.
- Configure it:
- Token name:
GitDiary - Expiration: whatever you're comfortable with (90 days, 1 year, custom…)
- Repository access: Only select repositories → pick the diary repo from step 1.
- Repository permissions:
- Contents → Read and write ✅ (required)
- Everything else can stay
No access.
- Token name:
- Click Generate token and copy the value immediately — GitHub will never show it again.
⚠️ Treat this token like a password. It's stored only in your browser'slocalStorage; GitDiary never sends it anywhere except toapi.github.com.
- Open GitDiary in your browser (see Run locally below, or use your hosted deployment).
- The Setup Wizard appears on first launch. Fill in:
- Owner → your GitHub username
- Repository → the repo you created
- Branch →
main(or whatever your default branch is) - Personal Access Token → paste the token from step 2
- Click Test & Save. GitDiary will:
- ✅ read your repo tree (verifies token + repo)
- ✅ perform a tiny write test (verifies the
Contents: Read and writepermission)
- Wizard closes → the editor opens on today's entry. Start typing!
🔁 Need to change repo or rotate your token later? Click the ⚙️ button in the sidebar to reopen the wizard.
If you want to build and run GitDiary yourself:
# Prerequisites: .NET 10 SDK
cd src/GitDiary.Client
dotnet build # 0 warnings enforced
dotnet run # http://localhost:5016
# or, for hot reload:
dotnet watch runRun the tests (the Markdown sanitizer suite is the security-critical one):
dotnet test tests/GitDiary.TestsPublish a static build (deployable to GitHub Pages / Netlify / Cloudflare Pages / any static host):
dotnet publish src/GitDiary.Client -c Release -o publish
# REQUIRED. Without this the app hangs forever on the loading spinner.
python3 .github/scripts/pin_importmap_csp.py publish/wwwroot/index.html
# static output lives in: publish/wwwroot/
⚠️ Do not skip the second command. The app's CSP deliberately omitsscript-src 'unsafe-inline', so that a hypothetical XSS in the Markdown preview cannot execute and read your PAT out oflocalStorage. Butdotnet publishgenerates an inline import map whose contents change on every build, and the browser blocks it unless the CSP carries its hash. The script computes that hash and pins it. Skip it and Blazor never boots — the page just spins, with no error shown. CI (deploy.yml) runs it for you; a manual publish does not.
- Write — open GitDiary, today's entry is already selected. Type freely.
- Save — happens automatically 2 seconds after your last keystroke, or press
Ctrl + S. - Preview — toggle to see the rendered Markdown.
- Browse — pick any past date from the left sidebar.
- Search — hit the search box to full-text search across every entry.
- Offline — lose your connection? Keep writing. The status bar shows Pending, and GitDiary re-syncs when you're back online.
That's it. Your diary lives in Git; you can git clone it, grep it, back it up, or read it in plain text forever.
src/GitDiary.Client/
├── Components/ # Blazor UI components
│ ├── DiaryEditor.razor
│ ├── SearchBox.razor
│ ├── SetupWizard.razor
│ ├── Sidebar.razor
│ └── StatusBar.razor
├── Infrastructure/ # Result<T>, PathHelper
├── Layout/ # MainLayout
├── Models/ # DiaryEntry, SyncState, RepositoryConfig, ...
├── Pages/ # Home.razor
├── Services/ # GitHubApiClient, DiaryRepository, SyncService, SearchService, ...
├── Stores/ # DiaryStore, SettingsStore
└── wwwroot/css/ # Custom dark-theme CSS
Architecture is enforced by convention:
UI (Components/Pages) → Store → Repository → GitHub API
The UI never talks to GitHubApiClient directly — always through DiaryStore.
- v1.1 — Tags & Favorites
- v1.2 — Image attachments
- v1.3 — GitHub OAuth Device Flow (no more manual PATs)
- v2.0 — GitLab / Gitea / Forgejo support
GitDiary is intentionally minimal (KISS / YAGNI). The following are out of scope, and PRs that add them will be politely declined:
- Rich-text / WYSIWYG editing. Markdown is the format. If you want WYSIWYG, use a different app. There is a small syntax-insertion toolbar (bold / italic / lists / …), but the source-of-truth remains raw Markdown.
- Binary asset upload beyond the roadmap. External image links (
) already cover the case; a general-purpose upload pipeline (MIME sniffing, size caps, gallery UI, GitHub Contents-API base64 limits, repo bloat) is not something this project will grow. - Multi-user, sharing, collaboration. A GitHub repo is a single-user data store — if you want to share,
git pushand grant repo access. - Any backend service. GitDiary is a static site by design; adding a server violates the "zero infrastructure" premise.
- Plugin / extension systems. They almost always outgrow their host.
- Analytics / telemetry. The CSP explicitly blocks it and that is not going to change.
If you're not sure whether an idea fits, please open an Issue before writing code — I'd rather say no early than after you've invested effort.
Issues and PRs are welcome. Please read docs/prd.md and docs/tech-design.md before proposing architectural changes.
MIT © GitDiary contributors