Blueprint is a study project, not a product. No App Store launch, no startup pitch, no polished marketing site. It is me learning iOS architecture in public: a small SwiftUI app called Discover (nearby places via Geoapify) and a plain site where I write down decisions as the codebase grows.
At some point the repo needed a real site, not Markdown files you only read on GitHub. This post is about that site: ios-blueprint.vercel.app. Functional and readable, not trying to wow anyone. Sidebar, typography, diagrams. That is enough.
The iOS app matters, but the interesting engineering problem for this write-up is how to ship a static site when your generator is a Swift executable and your host only serves files.
Why not Next.js (this time)
Most of my web projects use Next.js. Blueprint is different: the same person maintaining the iOS code and the site, on a Mac, already living in Xcode and SwiftPM.
I did not want a second stack (Node, npm, React) just to render Markdown I was already writing for the repository. I wanted:
- Markdown in
Documentation/as the source of truth - A build step that turns that into HTML + CSS
- No runtime server, no JavaScript framework on the reader's side
- A pipeline I could run locally with one command and reproduce in CI
That pushed me toward Saga, a Swift static site generator from Loopwerk. Not a Markdown converter. A code-first SSG where you wire readers, templates, and writers in Swift.
Saga is not Markdown-only
This is worth saying clearly because Blueprint can make it look otherwise.
Saga ships with Markdown readers (I use Parsley), but that is a plugin choice, not the ceiling. You pick readers for whatever format you want, render HTML with Swim templates in Swift, and style with CSS or Tailwind. Saga can also generate pages from code with no content file on disk: landing pages, indexes, 404s, data pulled from an API. The site is a Swift program. Markdown is just one input.
Blueprint uses Markdown because I am writing architecture notes and ADRs. That fit the content. It is not because Saga limits you to plain .md pages.
For what a polished personal site looks like with the same stack, see rychillie.pages.dev. Rychillie open-sourced the whole thing (Rychillie.net): Swift, Saga, a beautiful layout. Thank you for putting it out there. That project is incredible, and it is what gave me the idea to build Blueprint's site this way in the first place.
Could I rebuild luizmello.dev entirely in Swift with Saga, CSS, and Tailwind if I wanted? Yes. I use Next.js for that site today because it already exists and the ecosystem fits how I ship there. Blueprint was the experiment: same person, iOS repo, one language for the generator. Different project, different tradeoff.
The pipeline in one sentence
Markdown lives in Documentation/Content/en/. A Swift executable in Website/Sources/Website/ configures Saga: readers, templates, Tailwind, Mermaid. Output lands in Website/deploy/. GitHub Actions builds on macOS, uploads the folder, and a second job runs vercel deploy --prod from that directory.
No Saga on Vercel's build servers. Vercel only serves files something else already produced.
Saga 3 and the Website package
Blueprint uses Saga 3 (from: "3.0.0" in Package.swift). The migration from Saga 2 is real: pre/post build logic moves into hooks, and the CLI no longer takes --watch / --output flags on saga dev.
The site generator is not a config file. It is Swift code:
try await Saga(input: "../Documentation/Content/en", output: "deploy")
.beforeRead { saga in
// compile Tailwind when sources change
}
.ignoreChanges("output.css")
.register(folder: "guides", /* readers + Swim templates */)
.afterWrite { _ in
// copy CSS + vercel.json into deploy/
}
.run()beforeRead runs Tailwind via SwiftTailwind before Saga parses Markdown. afterWrite copies output.css into deploy/static/ and writes a minimal vercel.json (clean URLs, trailing slashes) next to the generated HTML.
Locally I use brew install loopwerk/tap/saga and ./scripts/saga dev --port 3000. CI skips Homebrew entirely and runs swift build plus the compiled Website binary. Same outcome, ~80 seconds saved per workflow run.
Templates with Swim, not React
HTML comes from Swim templates in templates.swift: a shell with sidebar navigation, section indexes, footer, and Mermaid script tags. Navigation entries are driven by SiteCatalog.swift so adding a page means editing Markdown and registering it in the catalog. Annoying, but explicit. I prefer that over magic folder inference for a project whose goal is to teach structure.
Pages embed Mermaid diagrams in Markdown. Saga's pipeline renders them at build time; the site loads Mermaid from a CDN in the layout. No client-side React, no hydration cost. Just HTML and a small JS bundle for diagram rendering.
Tailwind without a Node toolchain
Tailwind input lives at Documentation/Content/en/static/input.css. SwiftTailwind downloads the Tailwind CLI version pinned in code (4.2.1), compiles with minify, and Saga copies the result into deploy/static/output.css.
Templates reference hashed paths via Saga.hashed("/static/output.css") so cache busting stays automatic when CSS changes.
This was a deliberate choice: one language ecosystem for the site generator and the iOS app. The build still needs macOS (Saga + Tailwind wrapper), which shapes CI.
GitHub Actions: macOS builds, Ubuntu deploys
.github/workflows/website.yml splits concerns:
- Build job (
macos-latest): restore SPM cache →swift build→ runWebsite→ uploadWebsite/deploy/as an artifact - Deploy job (
ubuntu-latest): download artifact →vercel deploy --prodfromWebsite/deploy/
Path filters mean pushes that only touch the root README.md or Swift sources do not run the website workflow. Content changes trigger the site; iOS CI is separate.
Caching Website/.build and SwiftPM's download cache cut build time on my runner from ~47s to ~22s on a cache hit. The first run populates the cache; content-only commits reuse the compiled executable.
Vercel lessons (read this if you use Git + CLI)
Website/deploy/ is gitignored. If Vercel's Git integration deploys on every push to main, production becomes an empty site. 404 everywhere. I learned this the hard way after a README-only commit promoted a broken deployment.
Fix: Settings → Build and Deployment → Ignored Build Step → Don't build anything. Only GitHub Actions publishes production. Root Directory and Output Directory stay empty; the CLI uploads pre-built HTML from Website/deploy/.
Also: do not run vercel deploy from inside Website/ when Root Directory was set to Website. The CLI resolves paths like Website/deploy/Website and fails. Deploy from Website/deploy/ with an empty root, or from the repo root with the correct path. Pick one model and stick to it.
What about the iOS app?
Discover is a two-tab SwiftUI app (Discover + Favorites), NavigationStack, @Observable view models, use cases, repositories, a file-backed POI cache, SwiftData favorites, and Swift Testing on the parts that matter for learning.
Blueprint records that code in ADRs and architecture pages. Work in progress throughout. Fine to read along, not meant to copy blindly into production.
If you care about the app layers, start at Architecture Overview. If you care about the site itself, the meta section under Website walks through Saga, Tailwind, and deploy.
What I'd do differently
- Set Vercel Ignored Build Step on day one. Would have saved an evening of "why is production 404 after README?"
- Skip Homebrew Saga in CI from the start. The
Websiteexecutable is what CI runs; thesagaCLI is a dev convenience. - Document path filters earlier. iOS CI and Website CI should not both run for every commit.
Final thoughts
Blueprint's site is small: Markdown, Swift, static HTML, Vercel. No database, no SSR, no edge functions. That is the point. The complexity lives in the content and the iOS architecture, not in the hosting story or the visual design.
Building the generator in Swift felt natural for a Swift project. Saga 3's hooks map cleanly to "compile CSS, then read Markdown, then copy assets." CI is mostly "run the binary on macOS, ship the folder."
Saga is a general static site generator. Blueprint happens to publish architecture notes from Markdown, but the same pipeline could power a portfolio, a blog, or a full personal site with custom Swim templates and no .md files at all. If you already live in Swift and you are tired of bolting on a JavaScript SSG you touch twice a year, it is worth a look. Just do not expect Blueprint itself to be a showcase website. It is a notebook with a build pipeline. For the showcase, see Rychillie's.
Technical Resources
- Live site: ios-blueprint.vercel.app
- GitHub: github.com/luizmellodev/Blueprint
- Website README: Website/README.md
- Saga: getsaga.dev
- Inspiration (Saga site done right): rychillie.pages.dev · source
- Deploy notes: Build & Preview