# blog-manager A local web app for managing the portfolio blog: create, edit and delete posts in a markdown editor with live preview, upload images, rename and merge categories and tags, and publish through the existing git and deploy pipeline. There is **no authentication**. The server listens on `0.0.0.0:3051`, so restrict access to that port with firewall rules. ## How it works - **Store**: `portfolio/data/blog-posts.json`. The site imports it as `BLOG_POSTS` in `data/content.ts`. The manager edits this file in place, in the mounted portfolio checkout. - **Images**: uploads go to `portfolio/public/img/blog//` and are referenced as `img/blog//`. - **Preview**: rendered with the site's own remark pipeline and styled with the site's own CSS, so it looks like the live post page. - **Publish**: 1. Commits only `data/blog-posts.json` and `public/img/blog/`. Other changes in the checkout are never included. 2. Runs `git push`. 3. Calls the site's `POST /api/deploy` (optional). This triggers `scripts/deploy-watch.sh` on the server. - **Safety**: writes are atomic. A save made from a stale copy, for example after a `git pull`, is rejected so you can reload. Browsers can't send cross-site writes, because requests whose `Origin` doesn't match the host are refused. ## Run with Docker ```bash cp .env.example .env # set UID/GID, git identity, push token, deploy secret docker compose up -d --build ``` Then open `http://:3051`. - The container mounts `../portfolio` at `/repo`. Set `PORTFOLIO_DIR` to use a different checkout. - Set `BLOG_MANAGER_UID` and `BLOG_MANAGER_GID` to the owner of the checkout (`id -u` / `id -g`). Otherwise, on Linux, files the manager writes end up owned by a different user, and git and `deploy-watch` may then fail with permission errors. - **Push credentials**: - For an HTTPS remote, set `GIT_PUSH_USER` and `GIT_PUSH_TOKEN`. git receives them through `scripts/git-askpass.sh`, and they're never written to disk. - Set `GIT_PUSH=false` to only commit and push by hand. - If the manager runs on the deploy host against the same checkout that `deploy-watch` pulls, that pull is a no-op fast-forward after a publish. ## Run without Docker ```bash npm install npm run dev # http://0.0.0.0:3051, uses ../portfolio unless REPO_DIR is set ``` ## Environment | Variable | Default | Purpose | |---|---|---| | `REPO_DIR` | `../portfolio` (`/repo` in Docker) | Portfolio git checkout | | `GIT_AUTHOR_NAME`, `GIT_AUTHOR_EMAIL` | none | Commit identity (needed unless the repo config has `user.*`) | | `GIT_PUSH` | `true` | `false` = commit only | | `GIT_PUSH_USER`, `GIT_PUSH_TOKEN` | none | HTTPS push credentials | | `DEPLOY_URL`, `DEPLOY_TRIGGER_SECRET` | none | Live-site redeploy hook; skipped when unset | | `BLOG_MANAGER_PORT`, `BLOG_MANAGER_UID`, `BLOG_MANAGER_GID`, `PORTFOLIO_DIR` | `3051`, `1000`, `1000`, `../portfolio` | Compose only | ## Notes - Categories come from the posts themselves. You create one by typing it into a post, and it disappears when no post uses it. Renaming a category to an existing one merges the two. - Renaming a post's slug moves its image folder and rewrites the references to it. - Every saved post goes live on the next publish; there are no drafts. - `lib/types.ts` mirrors `BlogPost` in `portfolio/data/content.ts`. Keep the two in sync.