Commits
Three phases deployed in one day: assistant, voice, and sections
Three milestones in a single afternoon: the creation assistant, audio with comments
Today has been the most intense day of the project: three phases deployed in a single afternoon, with their tweaks and more than one fix on the fly. I'm writing it all down here so tomorrow I won't remember any of it.
Milestone 1: the "From idea to published" assistant
The first deployment has been the unified creation assistant at /admin/editor/. Three possible origins: search with Tavily, specific URLs, or my own base content. Online review with SEO improvement and featured image generation using the endpoint I already had.
No schema or backend changes: frontend only. npm run check with 0 errors, successful build, and deployment to fxserver with docker compose up -d --build. The E2E validation via HTTP returned HTML 200 from the wizard and the editor asset with the expected text.
The first bug arrived promptly: creating a document with Keyword + Tavily ended in 504 because the synchronous POST takes longer than the proxy. The solution was to move the flow to a short job with polling from the assistant, without keeping an open request.
Milestone 2: audio, comments, and chat with AI
The second deployment adds the voice and community layer: configurable embeddings usage, ingestion of published articles into knowledge, public endpoint /api/public/ask with citations, TTS to audio_url, public comments with moderation by AI and its panel. Alembic migration included for articles.audio_url and the comments table.
Verification: python -m unittest with 13 tests passing green, /api/health and /api/articles returning 200, and a full public article loaded. /api/public/ask returns 400 explicitly because there's still no embeddings usage assigned in Settings → AI: the route works, it just needs to be configured.
This is where the longest series of fixes of the afternoon came. The MiniMax voice rejected long texts (text too long), so now they're split into 4,000-character chunks and the audios are concatenated. It also rejected the Spanish_Female voice (voice id not exist): now there's a list of fallback voices and the backend tries the next one if it fails. And if there's no configured provider, public articles offer "Listen to the article" using the browser's Web Speech API: zero server cost.
The public chat has also been redesigned: messaging-style bubbles, visible notice that you're talking to an AI, minimal markdown with HTML escaping (bold, italics, code, links), and cleanup of <think> and <thought> blocks that some models let slip through. Plus, chat and voice have separate configuration: its own chat usage and voice_id in settings, without touching code.
Milestone 3: configurable sections
The third deployment brings sections: sections migration + articles.section_id, CRUD in Settings → Sections, selector in the editor, filter in the articles table, dynamic public navigation, and a /<section> page that reuses the blog listing. Tests green and full E2E validation: create section, see it public, delete it, and verify the 404.
Afterwards, two UX tweaks from the commits: "Articles" stays as a blog view without section, each section has its own item in the admin menu, and a couple of router fixes (missing import and listing for editors).
Milestone 4, first batch
To wrap up, the foundations of the versions phase: autosave with 10-second debounce and indicator, article_versions table with snapshots and restoration from the editor, publish_at column with public query filtering publish_at <= now(), date selector in the editor, "Scheduled" badge, and a real editorial queue in the rail.
The second batch adds verified editor/admin roles to the backend, user management, global Cmd+K search, a copilot in the editor, and translated cloning via translation usage.
SEO that I found broken
Reviewing the review phase I found that robots.txt was pointing to a sitemap generated with localhost and that the static sitemap included admin routes. Now /sitemap.xml is generated dynamically with home, blog, public sections, and published articles; robots.txt points there and blocks /admin/, and SITE_URL uses the public domain for canonical and sitemap. Things you only see when you look.