April 29, 2026 · joel blogpost feature Meet the new blog feature on PropellerDeck — and yes, this post wrote itself. Well, almost. Let me explain. ## What we built PropellerDeck now ships with a first-class blogging system. It lives at `/blog` and is fully public — no login required to read. Behind the scenes, admins manage posts through the dashboard under *Content → Blogposts*, a new section gated by the `blogposts_manage` permission. The feature is straightforward to use: 1. Create a new post, give it a title and optional tags. 2. Write the content directly in a full-page Markdown editor (powered by EasyMDE), or hand the title and a prompt to the AI generator and let it draft something. 3. Hit **Publish**. Optionally let the system auto-create an announcement at the same time — so your users see the news on their dashboard home screen without you having to create two separate pieces of content. That's it. Posts are live at `/blog/<slug>` the moment you publish. ## Under the hood The data model is lean. A `Blogpost` has a UUID primary key, a unique slug, a JSONB `tags` array, a Markdown `body`, and a status field that cycles through three states: `DRAFT → GENERATING → DRAFT → PUBLISHED`. The `GENERATING` state is the interesting one. When you trigger AI generation, the backend calls **SmartTask** — PropellerDeck's Claude Code agent runner — and spawns a headless agent in an isolated workspace. That agent receives a structured prompt built from your title, tags, and any extra instructions you provided. Its job is to produce a `blogpost.md` file in its output directory. When the agent finishes, the SmartTask bridge service fires a completion hook. The `BlogpostService.handle_generation_complete` method reads the downloaded `.md` file (falling back to raw stdout if no file is found), writes the content into the post body, and flips the status back to `DRAFT`. A polling endpoint — `/admin/blogposts/api/<id>/generation-status` — lets the editor frontend track progress while the agent works. The public-facing pages are rendered with Jinja2 templates and ship Markdown content as a JSON blob to the browser, where `marked.js` plus `DOMPurify` handle rendering and sanitisation client-side. Both the list page and the individual post page adapt their layout based on whether the visitor is logged in: authenticated users get the standard dashboard topbar, anonymous visitors get the landing-page navigation with a new *Resources* dropdown that links to the blog. Dark and light mode are fully supported via CSS custom properties (`--color-text`, `--color-background`, etc.) inherited from the rest of the app's theme system. ## The meta moment This very post is the first live test of the feature. The blogpost title, tags, and the instruction to analyse the codebase were fed into the prompt builder in `BlogpostService._build_generation_prompt`. An instance of this agent (Claude Sonnet 4.6, running in a SmartTask workspace) cloned the repo, read through the relevant models, services, routes, templates, and git history, and produced this Markdown file — which the completion hook will read from the output directory and load straight into the draft editor. If you're reading this on `/blog/blogpost`, the pipeline worked end to end. ## What's next A few things are already on the roadmap: - **Image uploads** — the API endpoint and service method are in place (`/admin/blogposts/api/<id>/upload-image`), and EasyMDE is wired to use them. Images are served from a per-post directory under `data/blogpost_images/` with no auth required, so they display correctly on the public blog. - **Announcement auto-linking** — publishing a post can optionally create a linked announcement. The reverse direction (surfacing the blogpost link inside announcements on the welcome screen) is already live. - **Per-product configuration** — `blogposts_manage` is already included in every product's permission config, so the feature is available across PropellerDeck, StartupDeck, and every other deck variant without extra setup. The bones are solid. More content tooling will follow.