No description
  • Astro 58.4%
  • CSS 25.8%
  • TypeScript 13.2%
  • JavaScript 2%
  • Dockerfile 0.6%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
2026-09-21 09:46:46 -05:00
.github chore(deps): bump the ci-dependencies group across 1 directory with 5 updates 2026-09-14 05:45:05 +00:00
public fix: remove old quartz public files 2026-05-12 11:35:15 -05:00
src fix(blog): social media urls 2026-09-10 15:55:31 +00:00
.envrc fixing conflicts 2026-01-26 15:09:31 -06:00
.gitattributes add gitattributes for windows 2023-08-02 20:59:56 -07:00
.gitignore feat: migrate blog to Astro with new design and content pages 2026-05-12 10:41:04 -05:00
.node-version Node 22 (#1997) 2025-05-28 16:20:59 -07:00
.npmrc add engines field 2023-08-20 08:57:56 -07:00
.prettierignore fix notes 2023-08-07 23:57:24 -07:00
.prettierrc Use semi: false for prettier config 2022-05-02 08:57:25 -07:00
astro.config.mjs feat: migrate blog to Astro with new design and content pages 2026-05-12 10:41:04 -05:00
devbox.json fixing conflicts 2026-01-26 15:09:31 -06:00
devbox.lock fixing conflicts 2026-01-26 15:09:31 -06:00
Dockerfile fix: right dockerfile and nginx 2026-05-12 10:41:09 -05:00
LICENSE.txt add base structure 2021-07-18 09:35:42 -04:00
nginx.conf docs: update blog content 2026-05-12 2026-05-12 12:15:19 -05:00
package-lock.json chore(deps): bump the production-dependencies group with 2 updates 2026-09-21 05:43:55 +00:00
package.json chore(deps): bump the production-dependencies group with 2 updates 2026-09-21 05:43:55 +00:00
README.md docs: update blog content 2026-08-04 2026-08-04 23:51:12 -06:00
Taskfile.yaml docs: update blog content 2026-05-11 2026-05-12 10:41:09 -05:00
tsconfig.json feat: migrate blog to Astro with new design and content pages 2026-05-12 10:41:04 -05:00

blog.mvaldes.dev

Bilingual (EN/ES) personal blog. Built with Astro 6, shipped as a static site behind nginx in a container.

Stack

  • Astro 6 with the Content Layer API
  • JetBrains Mono typography, custom CSS (no Tailwind, no UI lib)
  • One posts collection, language selected per-post via a lang field
  • Per-language RSS feeds, hreflang alternates, sitemap with i18n
  • Shiki syntax highlighting (github-dark-dimmed)
  • Static build → nginx → container image

Quick start

npm install
npm run dev      # local dev server on http://localhost:4321
npm run build    # output to ./dist
npm run preview  # serve the build
npm run sync     # regenerate astro:content types after schema changes

Node 22 (see .node-version). devbox + direnv are wired up: cd into the repo and npm install runs automatically.

There's also a Taskfile.yaml:

task dev      # npm run dev
task build    # npm run build
task clean    # rm -rf dist
task sync     # rsync posts in from the Obsidian vault
task publish  # git add . && commit "docs: update blog content <date>" && push

Project layout

src/
├── content/
│   └── posts/               # all posts (.md / .mdx), EN and ES together
├── content.config.ts        # collection schema (Zod)
├── i18n.ts                  # UI strings + locale helpers
├── lib/
│   └── content.ts           # getPosts, getTranslationMap helpers
├── layouts/
│   ├── Base.astro           # html lang, meta, hreflang
│   └── Post.astro           # single-post layout
├── components/
│   ├── Header.astro         # nav + language switcher
│   ├── Footer.astro
│   ├── PostCard.astro
│   └── Sidebar.astro
├── pages/
│   ├── index.astro          # redirects to /en
│   ├── talks.astro
│   ├── projects.astro
│   ├── video.astro
│   ├── en/
│   │   ├── [...page].astro  # paginated post list, 8 per page
│   │   ├── about.astro
│   │   ├── posts/[...slug].astro
│   │   ├── tags/[tag].astro
│   │   └── rss.xml.ts
│   └── es/...               # mirror of en/
└── styles/
    └── global.css

Dockerfile                    # node build → nginx runtime
nginx.conf                    # server config baked into the image

Cluster manifests are not in this repo. The blog runs on k3s, but the Deployment/Service/IngressRoute live in the gitops repo alongside everything else Flux reconciles.

Adding a post

Drop a .md (or .mdx) file in src/content/posts/. The filename becomes the URL slug, and the post lands under /en/posts/<slug> or /es/posts/<slug> depending on its lang.

Frontmatter

---
lang: en                  # required — "en" or "es", decides which site section it lands in
title: Self Hosted in 2026
description: Consolidating hardware and software for the homelab
pubDate: 2026-08-04
draft: false
tags:
  - homelab
---

Required: lang, title, description, pubDate. Optional: tags (defaults to []), draft (defaults to false), updatedDate, cover, translationKey.

Schema lives in src/content.config.ts — that's the source of truth.

Drafts

Set draft: true. The post stays in the repo, doesn't build, doesn't appear in the post list, RSS, or the sitemap.

Linking translations

If you write the same post in both languages, give both files the same translationKey:

# src/content/posts/cilium-migration.md
lang: en
translationKey: cilium-migration-2026

# src/content/posts/migracion-cilium.md
lang: es
translationKey: cilium-migration-2026

Each post then links to its translation, and the language switcher in the header routes you to the equivalent post instead of the home page. Posts without a translationKey are language-only and sit happily next to translated posts in the feed — which is currently every post.

MDX

Rename to .mdx to import components. Useful for callouts or interactive demos.

Writing flow

Posts are drafted in Obsidian (~/Obsidian/wiki/Blog/, or the WSL path on Windows) and rsync'd into src/content/posts/ with task sync. Frontmatter in the vault must already carry lang — sync doesn't add it. Then task publish commits and pushes, which triggers the build.

Deployment

Local container build

docker build -t blog-mvaldes .
docker run -p 8080:80 blog-mvaldes
# http://localhost:8080

nginx listens on port 80 inside the container (the EXPOSE 8080 line in the Dockerfile is stale and doesn't match nginx.conf).

Pushes are built into a tagged image by CI and rolled out to k3s by Flux from a separate gitops repo.

i18n notes

  • UI strings live in src/i18n.ts. Add new strings to both en and es blocks or the type checker will complain.
  • Dates use Intl.DateTimeFormat via the formatDate(date, lang) helper.
  • The language switcher reads Astro.url.pathname and the page's translationHref prop. On a post page, this resolves to the equivalent post in the other language via translationKey. Elsewhere, it falls back to the other language's home.
  • hreflang alternates only emit when a translation actually exists. No 404s for "not yet translated" pages.

TODO

  • Open Graph default image at public/og-default.png (currently 404s)
  • Search — Pagefind is easy to wire up here, runs on the static build
  • Obsidian wiki-link handling — task sync copies posts verbatim, so [[links]] render as literal text
  • Fix the EXPOSE / listen port mismatch between Dockerfile and nginx.conf

License

Content: all rights reserved. Code: MIT — do what you want with the scaffold.