Skip to content

Latest commit

 

History

9 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Mahi

A static site generator written in Swift, with a SwiftUI-flavored API. You describe your site entirely in Swift — no HTML, CSS, or JavaScript to write — and Mahi renders it to a static dist/ tree: deduplicated CSS, clean-URL pages, full server-rendered HTML, and a tiny generated runtime for instant navigation.

import Mahi

@main
struct MySite: Site {
    var config = SiteConfig(
        name: "My Book",
        languages: ["en", "vi"],   // first is the default
        theme: .default
    )

    var body: some Routes {
        Layout(Shell()) {                     // fixed header/footer for the whole site
            Page("/", Home())
            Layout("book", BookShell()) {     // fixed sidebar for the /book branch
                Page("intro", Intro())
                Page("chapter-1", Chapter1())
            }
            Page("about", About()).title("About")
        }
    }
}

Highlights

  • SwiftUI-shaped DSL. View with an @ViewBuilder body, primitives (Text, VStack, HStack, ZStack, Link, Image, Grid, List, Section, HTMLTag), and familiar modifiers (.padding(), .background(), .foregroundStyle(), .frame(), .font(), …).
  • Environment & theming. A real EnvironmentValues / @Environment / .environment(_:_:) system. Theme emits :root custom properties with automatic light/dark via prefers-color-scheme plus an explicit toggle.
  • Atomic CSS. Every unique style collapses to one .m<hash> class, and the whole site shares a single deduplicated assets/site.css.
  • Nested routing. Layout/Outlet wrap branches of the route tree. Each page renders full static HTML with its complete layout chain, so it works without JavaScript.
  • Instant navigation. A ~3 KB generated client router swaps only the deepest shared outlet — shared headers and sidebars stay put — with CSS view transitions. It falls back to normal navigation when JS is off.
  • i18n with String Catalogs. Drop a Localizable.xcstrings next to your site; Text("home.title") resolves per locale. Mahi builds dist/en/…, dist/vi/…, a root redirect, and hreflang alternates.
  • Components. NavBar (responsive, zero-JS collapse), Footer, Card, Hero, Accordion (<details>), Tabs, and DarkModeToggle. Component scripts and CSS are emitted only on pages that use them.
  • Zero third-party dependencies in the core. MahiCore uses only Foundation; the CLI adds swift-argument-parser; the dev server uses Network.framework and FSEvents.

The CLI

Build the tool once from this repo:

swift build              # produces .build/debug/mahi

Then, from a site package:

mahi init my-site        # scaffold a new site package
mahi build               # build to dist/  (--release, --base-url, --out)
mahi dev --port 3000     # build + serve + live-reload while you edit

mahi build and mahi dev never inspect your Swift types — they simply run your site's own executable (via swift run), forwarding a small set of --mahi-* arguments that the Site protocol understands.

How it fits together

A site is its own SwiftPM executable that depends on one product, Mahi. @main struct MySite: Site gets a main() that parses --mahi-* and runs the builder. The packages:

  • MahiCore — the DSL and render engine (View/ViewBuilder, modifiers, environment, theme, Node IR, HTML renderer, atomic stylesheet, routing, i18n, the generated runtime, and the site builder).
  • MahiComponents — higher-level views built on MahiCore.
  • Mahi — an umbrella that re-exports both; the single dependency a site declares.
  • MahiCLI — the mahi executable.

Internationalization

Place Localizable.xcstrings (the Xcode 15+ String Catalog format) at your site root. Text keys resolve against it for the active locale, with %@/%lld interpolation and basic plural variations; unknown keys fall back to the literal string. LanguageSwitcher links the current page across locales.

WebAssembly islands

An island is an interactive component whose behavior is written in Swift and compiled to WebAssembly — the rest of the page stays fully static. Conform to IslandView: give it a stable island name, optional props, and a static fallback view.

struct Counter: IslandView {
    static let island = "Counter"
    let start: Int
    var props: IslandProps { ["start": .int(start)] }
    var fallback: some View {          // renders at build time (SEO + no-JS)
        HStack(spacing: 16) {
            Button(""); Text(verbatim: String(start)).bold(); Button("+")
        }
    }
}

At build time the fallback ships inside a hydration marker (<div data-mahi-island="Counter" data-mahi-src="…/Counter.wasm" data-mahi-props="…">). When a compiled assets/islands/Counter.wasm is present, the framework-generated loader (included only when an island is used) streams and instantiates it and hands over the mount element and props via the small Mahi island ABI (mount_id / props / set_html / set_text_of / on_click + a mahi_dispatch callback). Without the wasm, the static fallback simply stays — so islands always degrade gracefully.

Producing the .wasm needs the Swift SDK for WebAssembly, a separate toolchain:

swift sdk install <webassembly-sdk-bundle-url>   # one-time, matches your Swift

With the SDK installed, mahi build compiles each used island — it expects an executable product named after the island (targeting the ABI; see Examples/book-site/Islands/Counter) — and copies the result into dist/assets/islands/. Without the SDK it prints a notice and ships fallback only. Rendering, props, feature-gating, and hydration wiring all work today; only the binary production depends on the external toolchain.

Example

See Examples/book-site — a small bilingual (en/vi) book with a global shell, a nested table-of-contents sidebar, dark mode, and the component set. Build it with mahi build or run mahi dev inside that folder.

Tests

swift test

Covers rendering snapshots, atomic-class dedup, environment propagation, nested routing and outlet stability, the String Catalog parser, the runtime assembler, and the components.

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages