You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
No place on the site to publish longer-form writing. Blogging on Medium/dev.to hands content ownership and SEO to a third party, and a personal blog on the site reinforces it as an engineering showcase (the site itself is part of the portfolio).
Proposed change
Build blog posts as first-class Astro pages, not a CMS. Each post is its own .astro file under frontend/src/pages/[...lang]/blog/ with exported metadata (title, pubDate, updatedDate, summary, tags, draft flag). Git is the source of truth for post content.
Scope
Post pages: one .astro file per post, exporting metadata constants. Shared BlogPostLayout for nav, header, SEO tags.
Index page: /blog listing all non-draft posts, sorted by pubDate, using import.meta.glob over the posts directory.
RSS feed: /rss.xml endpoint fed from the same glob. Summary-only feed (description, not content:encoded) to drive readers to the site.
Reactions: emoji reactions per post (like / heart / insightful / etc.), backed by our own backend.
Comments: threaded comments per post, backed by our own backend.
Reactions + comments via our backend
Reactions and comments are served by a new module in our existing .NET backend, not a third-party widget. Rationale: the site is an engineering showcase and the backend is part of the portfolio — adding a domain that exercises event sourcing, auth, and a typed wire contract is on-theme. Giscus / Utterances were considered and rejected for that reason.
Shape of the backend module:
New Kalandra.Blog domain with append-only event streams per post slug for comments and reactions. Separate streams (UUIDv5 from slug) so high-volume reaction events don't share a replay path with comments.
Reaction toggle semantics decide add vs. remove from the replayed stream.
API: BlogController with [Authorize] on POST endpoints (only signed-in users comment or react) and anonymous GETs. Parallel API error enum to keep the wire contract stable.
Frontend wrappers: vanilla-JS BlogReactions and BlogComments Astro components that call the backend, with optimistic toggles and auth-dialog hooks for signed-out viewers.
Cross-posting
Each post is the canonical URL. Selective cross-posting to dev.to/Medium later with rel=canonical pointing back here is a follow-up, not part of this issue.
Files
frontend/src/pages/[...lang]/blog/index.astro — new, post list
frontend/src/pages/[...lang]/blog/<slug>.astro — new, one file per post
frontend/src/components/BlogPostLayout.astro — new, shared post chrome
frontend/src/pages/rss.xml.ts — new, RSS endpoint
frontend/src/components/BlogReactions.astro / BlogComments.astro — new, talk to the backend
Problem
No place on the site to publish longer-form writing. Blogging on Medium/dev.to hands content ownership and SEO to a third party, and a personal blog on the site reinforces it as an engineering showcase (the site itself is part of the portfolio).
Proposed change
Build blog posts as first-class Astro pages, not a CMS. Each post is its own
.astrofile underfrontend/src/pages/[...lang]/blog/with exported metadata (title,pubDate,updatedDate, summary, tags, draft flag). Git is the source of truth for post content.Scope
.astrofile per post, exporting metadata constants. SharedBlogPostLayoutfor nav, header, SEO tags./bloglisting all non-draft posts, sorted bypubDate, usingimport.meta.globover the posts directory./rss.xmlendpoint fed from the same glob. Summary-only feed (description, notcontent:encoded) to drive readers to the site.Reactions + comments via our backend
Reactions and comments are served by a new module in our existing .NET backend, not a third-party widget. Rationale: the site is an engineering showcase and the backend is part of the portfolio — adding a domain that exercises event sourcing, auth, and a typed wire contract is on-theme. Giscus / Utterances were considered and rejected for that reason.
Shape of the backend module:
Kalandra.Blogdomain with append-only event streams per post slug for comments and reactions. Separate streams (UUIDv5 from slug) so high-volume reaction events don't share a replay path with comments.BlogControllerwith[Authorize]on POST endpoints (only signed-in users comment or react) and anonymous GETs. Parallel API error enum to keep the wire contract stable.BlogReactionsandBlogCommentsAstro components that call the backend, with optimistic toggles and auth-dialog hooks for signed-out viewers.Cross-posting
Each post is the canonical URL. Selective cross-posting to dev.to/Medium later with
rel=canonicalpointing back here is a follow-up, not part of this issue.Files
frontend/src/pages/[...lang]/blog/index.astro— new, post listfrontend/src/pages/[...lang]/blog/<slug>.astro— new, one file per postfrontend/src/components/BlogPostLayout.astro— new, shared post chromefrontend/src/pages/rss.xml.ts— new, RSS endpointfrontend/src/components/BlogReactions.astro/BlogComments.astro— new, talk to the backendfrontend/src/blog/{posts.ts,types.ts}— new, metadata glob + contractfrontend/src/i18n/{cs,en}/blog.json— new, translations for blog UI chromebackend/src/Kalandra.Blog/**— new domain module (events, commands, queries, registration)backend/src/Kalandra.Api/**— newBlogController+ DTOsOut of scope
.astropages are sufficient; MDX can be added later if authoring ergonomics suffer)