This project is a personal blog at blog.nakom.is, built as a React/Vite single-page application served via CloudFront from a private S3 bucket. Blog post content lives in a separate private repository linked as a git submodule, keeping the infrastructure and content concerns cleanly separated.
- Architecture Diagram
- Repository Layout
- Blog Content
- Components
- Web App
- Infrastructure
- Architecture Diagrams
web/— React/Vite blog frontend (TypeScript)web/content/— git submodule pointing to the private blog-content repoweb/scripts/— build and deployment scriptsinfra/— AWS CDK infrastructure (TypeScript)docs/architecture/— architecture diagram source (draw.io) and exported SVG.githooks/— git hooks (SVG auto-generation on commit)
Blog post markdown files live in the private blog-content repo, linked as a git submodule at web/content/. This keeps post content separate from the public infrastructure code.
Clone with submodules:
git clone --recurse-submodules git@github.com:nakomis/blog-app.gitOr, if already cloned:
git submodule update --initThe BlogCertStack creates an ACM certificate for blog.nakom.is in us-east-1, as CloudFront requires certificates to be in that region. DNS validation is used, automatically creating the validation records in the nakom.is Route53 hosted zone.
This stack must be deployed before the main BlogStack since the certificate ARN is passed as a cross-stack reference.
The BlogStack creates a private S3 bucket (blog-nakom-is-eu-west-2-<account>) to store the built web assets. The bucket blocks all public access; objects are served exclusively via CloudFront using Origin Access Control (OAC), so the bucket never needs a public-read policy.
The deployment script syncs two things to the bucket:
web/dist/— the compiled React application (JS, CSS, HTML, images)web/content/blog/→s3://<bucket>/posts/— raw markdown files, served at runtime so the app can fetch post content without a rebuild
The CloudFront distribution in BlogStack sits in front of the S3 bucket and handles:
- HTTPS enforcement — all HTTP requests are redirected to HTTPS
- SPA routing — 403 and 404 responses from S3 are rewritten to serve
index.htmlwith a 200 status, allowing React Router to handle client-side routes - Compression — assets are gzip/brotli compressed
- Caching — the
CACHING_OPTIMIZEDmanaged cache policy is used for all behaviours
The ACM certificate is attached to the distribution, covering blog.nakom.is.
A DNS A alias record is created in the existing nakom.is hosted zone pointing blog.nakom.is at the CloudFront distribution. The hosted zone is looked up by domain name rather than imported by ID, so no manual configuration is required.
At build time, web/scripts/buildContent.ts reads all markdown files from web/content/blog/, processes them through a unified/remark/rehype pipeline (with frontmatter extraction and syntax highlighting), and writes a generated TypeScript file (web/src/content.generated.ts) containing all post HTML and metadata as a typed constant. This file is imported directly by the React app, so no runtime markdown parsing is needed for the post content itself.
At runtime, the app also fetches raw markdown files from S3 (/posts/<slug>.md) — this allows the post list and content to reflect whatever is synced to S3, including posts added after the last frontend build.
cd web
npm install
npm run dev # local dev server on http://localhost:5173cd web
npm run build # build to web/dist/
bash scripts/deploy.sh # sync dist/ and content/blog/ to S3, invalidate CloudFrontcd infra
npm install
AWS_PROFILE=nakom.is-admin cdk synth
AWS_PROFILE=nakom.is-admin cdk deploy BlogCertStack # us-east-1 cert (first time only)
AWS_PROFILE=nakom.is-admin cdk deploy BlogStack # eu-west-2 S3/CloudFront/Route53docs/architecture/blog-app.drawio is the source for the diagram at the top of this README. The SVG is auto-generated on commit by the pre-commit hook in .githooks/pre-commit.
To activate the hook after cloning:
git config core.hooksPath .githooksTo regenerate the SVG manually:
/Applications/draw.io.app/Contents/MacOS/draw.io -x docs/architecture/blog-app.drawio -f svg -s 1 docs/architecture/blog-app.svgRequires the draw.io desktop app to be installed at /Applications/draw.io.app.