Execution reference for adding a frontmatter-driven blog to AI-Native Engineers. Blog posts are plain Markdown (.md) files, not MDX. Sessions remain MDX; do not conflate the two content types. This document is the source of truth for scope, order, acceptance criteria, and risks. Implementation code follows this file; if code and this doc disagree, update one or the other before merging.
We ship one phase at a time. Each phase is validated before the next starts. No skipping ahead, no half-finished work leaking forward.
| Decision | Choice | Notes |
|---|---|---|
| URL | /blog/ |
English routes later: /en/blog/ |
| v1 locale | PT-BR only | English is Phase 8, after PT v1 ships |
| Content format | Markdown (.md) + frontmatter |
Content Collections + Zod (same infra as sessions; sessions stay MDX) |
| Collection | Separate blog collection |
Do not extend sessions; different schema, layout, and file type |
| Layout | BlogPostLayout.astro |
Lighter than SessionLayout.astro; no SectionNav, ProgressTracker, NextSessionCTA, ReferencesList |
| CMS | None | Repo + .md files; PR is the publishing workflow |
| Reading time | Manual in frontmatter (optional) | Match sessions pattern; no auto-calculation in v1 |
flowchart LR
subgraph content [Content]
PostFiles["src/content/blog/*.md"]
Schema["content.config.ts"]
end
subgraph lib [Lib]
BlogLib["src/lib/blog.ts"]
I18n["getBlogHref in i18n.ts"]
end
subgraph pagesPT [Pages PT v1]
Index["/blog/"]
Post["/blog/slug/"]
end
PostFiles --> Schema
Schema --> BlogLib
BlogLib --> Index
BlogLib --> Post
Post --> Layout["BlogPostLayout.astro"]
Key files (by phase):
| Phase | Files created or touched |
|---|---|
| 1 | src/content.config.ts, src/content/blog/ |
| 2 | src/lib/blog.ts, src/lib/i18n.ts |
| 3 | src/layouts/BlogPostLayout.astro |
| 4 | src/pages/blog/[slug].astro |
| 5 | src/pages/blog/index.astro |
| 6 | src/pages/index.astro or src/components/Footer/Footer.astro |
| 7 | src/content/blog/*.md (real post) |
| 8 | src/pages/en/blog/, schema extension |
Phases are sequential. Dependencies flow top to bottom.
Phase 1 Collection and Schema
↓
Phase 2 Helpers and i18n
↓
Phase 3 Post Layout
↓
Phase 4 Post Route (PT)
↓
Phase 5 Blog Index (PT)
↓
Phase 6 Discovery and SEO
↓
Phase 7 First Real Post ← PT v1 complete
↓
Phase 8 English (post-v1)
v1 milestone: Phase 7 done. Phase 8 and backlog items are explicitly post-v1.
| Risk | Impact | Mitigation | Phase |
|---|---|---|---|
| Blog posts mixed into sessions collection | Wrong template, wrong nav, curriculum pollution | Dedicated blog collection and routes under /blog/ |
1 |
| Draft posts published by mistake | Unfinished content goes live | draft: true default in sample; filter draft === false in getStaticPaths and index |
1, 4, 5 |
| Layout inherits session-only UI | Bloated pages, confusing UX | Explicit exclusion list in Phase 3; no imports from SessionLayout zones | 3 |
| Prose styles diverge from sessions | Inconsistent reading experience | Copy .session-content prose rules once into .blog-content; do not fork later without reason |
3 |
| Blog confused with aulas (lessons) | Editorial and navigation blur | Distinct URL, layout, file type (.md vs .mdx), and tone; Phase 7 review against TONE.md |
7 |
| MDX used for blog by default | Unnecessary complexity for prose-only posts | Loader accepts **/*.md only in v1; MDX reserved for backlog if a post needs React islands |
1 |
| Blog unreachable after launch | Zero traffic | Phase 6 entry point (home or footer); validate ≤2 clicks | 6 |
| Invalid frontmatter breaks production build | Deploy failure | Zod schema in Content Collections; test by breaking frontmatter in Phase 1 | 1 |
| i18n added too early | Scope creep, duplicate work | PT-only until Phase 7 ships; EN in Phase 8 | 8 |
| Empty index with all drafts | Broken-looking /blog/ |
Empty-state copy in Phase 5; document that at least one draft: false post is required for index cards |
5 |
| Sitemap or OG tags missing | Poor discoverability and sharing | Phase 6 explicit checks on dist/sitemap*.xml and <meta> tags |
6 |
Objective: Register blog posts as a validated Content Collection, separate from sessions.
-
blogcollection insrc/content.config.tswithglobloader (pattern: '**/*.md',base: './src/content/blog') - Zod schema:
title,slug,lang(pt-BR|en),description,publishedAt,updatedAt(optional),draft(defaultfalse),tags(default[]),author(default'AI-Native Engineers') -
translationKey(optional string) in schema for Phase 8, unused in v1 - Directory
src/content/blog/ - One sample post
.mdwithdraft: true
npm run buildpasses with no errors- Invalid frontmatter (missing field or wrong type) fails the build
- Sample post is returned by
getCollection('blog')
npm run build
npm run lint- Temporarily remove a required frontmatter field from the sample post; confirm build fails
- Restore the sample post; confirm build passes
Objective: Centralize blog query logic and URL helpers before building pages.
-
src/lib/blog.tswith:getPublishedPosts(lang)— filtersdraft === false, sortspublishedAtdescendinggetPostBySlug(lang, slug)— safe lookup, returnsundefinedif not found or draft
-
getBlogHref(lang, slug?)insrc/lib/i18n.ts(mirrorsgetSessionHref) - PT-BR UI strings in
ui['pt-BR']: blog breadcrumb label, published date label, tags label
npm run buildandnpm run lintpassgetPublishedPosts('pt-BR')returns an empty array when only draft posts existgetBlogHref('pt-BR')resolves to/blog/getBlogHref('pt-BR', 'my-slug')resolves to/blog/my-slug/
npm run build
npm run lint- Confirm href helpers account for
BASE_URLvia existingwithBase()pattern
Objective: A lightweight post template that reuses site chrome without session-specific UI.
-
src/layouts/BlogPostLayout.astro - Reuse:
BaseLayout,SectionBlock,Breadcrumbs,Badge,Footer - Do not include:
SectionNav,ProgressTracker,NextSessionCTA,ReferencesList,Discussion - Prose styles copied from
.session-contentinSessionLayout.astrointo.blog-content - Hero: title,
description,publishedAt(andupdatedAtwhen present), tags
- Layout compiles with all required props
- Prose is readable at 375px and 1024px (headings, lists, blockquote, code, tables)
<title>and meta description come from frontmatter- Breadcrumb trail: Home → Blog → Post title
- Wire layout in Phase 4 (or a temporary dev-only route) and open a post in the browser
- View Source: confirm
<title>,meta name="description", and breadcrumb markup - Resize viewport to 375px and 1024px
Objective: Serve published PT-BR posts at static /blog/{slug}/ URLs.
-
src/pages/blog/[slug].astro -
getStaticPaths: only posts wherelang === 'pt-BR'anddraft === false -
render(post)+BlogPostLayout+<Content /> - Sample post flipped to
draft: false
/blog/{slug}/renders full Markdown body- Unknown slug returns the existing 404 page
- Posts with
draft: truedo not generate a static route - View Source shows static HTML content (no JS required for body text)
npm run dev- Open
/blog/{slug}/for the sample post - Open
/blog/nonexistent-slug/and confirm 404 - View Source on the post page
Objective: List published posts chronologically at /blog/.
-
src/pages/blog/index.astro - Post cards via existing
Card.astro: title, description, date, tags - Index hero: "Blog" heading plus one line of editorial context
- Empty state message when zero published posts exist
/blog/lists posts newest first- Each card links to
/blog/{slug}/ - Draft posts never appear on the index
- Layout is mobile-first and visually consistent with Neo Brutalism
npm run dev- Open
/blog/with one published post - Add a second published post with an earlier
publishedAt; confirm sort order - Set all posts to
draft: true; confirm empty state renders
Objective: Make the blog findable without typing the URL and shareable with correct previews.
- One entry point to
/blog/: home page section or footer link (pick at implementation time; record the choice in a comment or this doc) - Per-page OG tags via
BaseLayout(title + description on index and posts) - Confirm
@astrojs/sitemapincludes/blog/and post URLs after build
- User reaches the blog from the home page in at most 2 clicks
- Sharing a post produces correct title and description in OG meta
dist/sitemap*.xmllists/blog/and each published post URL
npm run build- Click from home (or footer) to
/blog/ - Inspect
<meta property="og:title">andog:descriptionon index and a post - Open
dist/sitemap-0.xml(or equivalent) and search for/blog/
Objective: Ship one editorially real PT-BR post that reads as blog content, not curriculum.
- Replace the sample post with real Markdown content per
TONE.md - Complete frontmatter: title, slug, description, publishedAt, tags
- Editorial pass: acronyms explained, no em dashes, blog tone (more timely/opinionated) vs lesson tone (structured curriculum)
- Published post has no placeholders or lorem ipsum
npm run buildpasses and GitHub Pages deploy succeeds- Post is editorially distinct from the three sessions at
/sessions/
- Full read-through against
TONE.mdchecklist npm run build- Push to main and confirm live URL on GitHub Pages
PT v1 is complete when Phase 7 passes.
Objective: Extend the blog to English with hreflang alternates, matching the sessions i18n pattern.
- Posts with
lang: 'en'and optionaltranslationKeylinking PT/EN pairs -
src/pages/en/blog/index.astro -
src/pages/en/blog/[slug].astro -
alternateLinksinBaseLayoutfor blog pages (same pattern as sessions) - EN UI strings in
ui.en - One EN post (translation or original)
/en/blog/and/en/blog/{slug}/render correctly<link rel="alternate" hreflang="...">is correct whentranslationKeymatches across locales- Language switch in the header navigates to the translated post or a sensible fallback
npm run dev- Browse
/en/blog/and an EN post - Inspect
<head>for alternate links - Toggle language in the site header from a PT post with an EN translation
Not in scope until PT v1 (Phase 7) ships:
| Item | Notes |
|---|---|
| RSS feed | @astrojs/rss at /blog/rss.xml |
| Giscus comments | Reuse Discussion.astro with term page:blog:pt-BR:{slug} |
| Tag filter page | Only when post volume justifies it |
| Pagination | Only when index exceeds ~10–15 posts |
| Headless CMS | Out of scope; .md files in repo is the workflow |
| MDX for a single post | Only if a post needs embedded React islands; default format stays .md |
| Auto reading time | Keep manual optional field like sessions |
- Changes to existing sessions, harness chapters, or
/projetocontent - Refactoring
ROADMAP.md(main site roadmap; separate document) - Newsletter, auth, search, or gamification
Every phase in this document follows the same structure:
| Section | Purpose |
|---|---|
| Objective | Why the phase exists (intent) |
| What We Ship | Concrete deliverables (scope) |
| Done When | Testable acceptance criteria |
| How to Validate | Commands, URLs, and manual checks to confirm done |
Do not mark a phase complete until every Done When item passes.