Sign in

    astra

    Mountable Mars middleware for static site generation

    astra
    ssg
    mars
    moonbit
    middleware
    Download zip
    Author
    Version
    0.23.0
    License
    MIT
    Last updated
    5 months ago
    Downloads
    824

    #mizchi/astra

    Static site generator in MoonBit. Renders Markdown / MDX documentation pages and bundles the assets needed to display them (loader, default CSS, Shiki syntax highlighting). The default workflow is astra build → static tree → any CDN; astra dev runs a local preview server.

    Internally astra is implemented as Mars middleware so the same renderer can also be mounted on a long-running Mars server (for sol embedding or ISR-style on-demand revalidation), but that path is optional — astra build does not open a network listener and the build output is fully deployable as a static bundle.

    deps: mars + markdown + luna (no edge to sol)

    #Install

    Library:

    // moon.mod.json { "deps": { "mizchi/astra": "0.20.0", "mizchi/mars": "0.3.10", "mizchi/luna": "0.20.0" } }

    CLI (binary):

    moon install mizchi/astra/cmd/astra # → $MOON_HOME/bin/astra # or via npm pnpm add -g @luna_ui/astra # 0.20.0

    #Quick start — CLI (default path)

    # Static dump (writes <out>/<url>/index.html for every page URL) astra build --out ./dist # Dev server against the current directory's docs/ tree astra dev --port 3000

    astra build walks every URL the in-process middleware can serve and writes the rendered body to <out>/<url-to-disk-path>. No network listener is opened during the build — dispatch goes through the testing harness (@testing.invoke). Deploy the resulting tree to any static host (Cloudflare Workers Static Assets, GitHub Pages, S3, etc).

    A working example lives at astra/examples/sol_docs/ — a docs site with i18n, blog, MDX, components, and parity tests against the sol-built output.

    #Optional — mount on a Mars server

    Reach for this only when you need on-demand rendering inside a long-running server (sol embedding, ISR with revalidate, dynamic content). For a pure docs / blog site the static path above is enough.

    import "mizchi/astra/middleware" as middleware
    import "mizchi/mars" as mars

    async fn main {
    let cfg = @astra.SsgConfig::default()
    let mw = @middleware.create(cfg, cwd=".")
    let app = @mars.Server::new()
    app.all("/*", mw.handler())
    app.listen(port=3000)
    }

    Middleware::handler() returns a Mars Handler that responds to GET on every page URL the document tree exposes plus the asset URLs in @assets.list_asset_urls(). Middleware::list_urls() returns the union, which the build CLI uses to crawl.

    #How dev and build share one Middleware

    Both modes go through the same Middleware::handler(). The build crawler calls @testing.invoke(handler, path=url) for each entry in mw.list_urls() and writes the response body to disk. So if a page renders correctly via astra dev, it is also correct in the static dump — there is no second renderer to keep in sync.

    #Configuration

    astra.config.json is the primary config file. sol.config.json is read as a fallback when astra.config.json is missing (kept for compatibility with projects that started under sol's SSG mode before astra extraction). New projects should write astra.config.json.

    The config drives:

    • docs_dir — where to scan markdown sources (default docs)
    • output_dir — where astra build writes (default dist)
    • theme — colors, fonts, header/footer/sidebar variants
    • i18n — locales and routing
    • islands — JSX/TSX hydration components
    • meta_files — sitemap, robots, OGP defaults

    The full schema is @astra.SsgConfig; astra/docs/parity-notes.md lists fields the static-build path honors that the middleware path does not yet.

    #Known parity gaps with sol's pre-extraction SSG

    astra/docs/parity-notes.md lists every place the middleware-served output diverges from a copy_static_assets-style static build (utility CSS extraction, sitemap generation, Cloudflare adapter manifests, etc.). None affect the rendered HTML body of pages today; closing the remaining gaps is tracked there.

    #License

    MIT

    ClsHints

    CLS (Cumulative Layout Shift) optimization hints Used to prevent layout shifts when components load

    ComponentEmbed

    Web Component embedding in markdown Custom elements (names containing -) are registered via customElements.define

    Frontmatter

    Frontmatter parsed from markdown file

    IslandEmbed

    Island embedding in markdown (legacy, prefer ComponentEmbed)

    JsxComponentEmbed

    JSX component embedding in MDX Before rendering, converted to ComponentEmbed (WebComponent)

    MdNode

    Markdown node types

    MdxAttrValue

    MDX attribute value types

    Navigation link for prev/next

    OutlineConfig

    Outline (table of contents) configuration

    BuildContext

    pub(all) struct BuildContext {
    config : SsgConfig
    pages : Array[PageMeta]
    sidebar : Array[SidebarGroup]
    cwd : String
    doc_tree : DocumentTree?
    is_dev : Bool
    }

    Build context for site generation

    BuildContext::new

    fn BuildContext::new(config : SsgConfig, cwd : String) -> BuildContext

    Create new build context

    BuildContext::url

    fn BuildContext::url(self : BuildContext, path : String) -> String

    Resolve a URL with the site's base_url prefix. Use this for all internal link generation.

    ContentType

    pub(all) enum ContentType {
    Markdown
    Html
    Component
    TsxComponent
    Mdx
    } derive(Eq,
    Debug
    )

    Content type for source files

    CssUtilitiesConfig

    pub(all) struct CssUtilitiesConfig {
    enabled : Bool
    split : Bool
    inline_threshold : Int
    source_dirs : Array[String]
    }

    CSS utilities configuration for static extraction and optimization

    CssUtilitiesConfig::default

    Default CSS utilities configuration

    DeployTarget

    pub(all) enum DeployTarget {
    Static
    Cloudflare
    GithubPages
    Vercel
    Netlify
    Deno
    Node
    } derive(Eq,
    Debug
    )

    Deploy target for build output

    DeployTarget::default

    fn DeployTarget::default() -> DeployTarget

    Default deploy target

    DocumentTree

    pub(all) struct DocumentTree {
    site : SiteInfo
    pages : Array[PageInfo]
    root : TreeNode
    }

    Root structure representing an entire site

    DocumentTree::get_page

    fn DocumentTree::get_page(self : DocumentTree, id : String) -> PageInfo?

    Get PageInfo by page ID

    DocumentTree::get_page_by_url

    fn DocumentTree::get_page_by_url(self : DocumentTree, url_path : String) -> PageInfo?

    Get PageInfo by URL path

    DocumentTree::get_pages_by_locale

    fn DocumentTree::get_pages_by_locale(self : DocumentTree, locale : String) -> Array[PageInfo]

    Get pages filtered by locale

    DocumentTree::get_pages_sorted_by_date

    fn DocumentTree::get_pages_sorted_by_date(self : DocumentTree, limit? : Int) -> Array[PageInfo]

    Get pages sorted by date (newest first)

    DocumentTree::new

    fn DocumentTree::new(site~ : SiteInfo, pages? : Array[PageInfo], root? : TreeNode) -> DocumentTree

    FooterColumn

    pub(all) struct FooterColumn {
    title : String
    items : Array[FooterLink]
    } derive(ToJson,
    FromJson
    )

    Footer column (group of links)

    FooterConfig

    pub(all) struct FooterConfig {
    message : String?
    copyright : String?
    links : Array[FooterColumn]
    top : Array[FooterElement]
    bottom : Array[FooterElement]
    } derive(ToJson,
    FromJson
    )

    Footer configuration

    FooterElement

    pub(all) enum FooterElement {
    Columns
    Message
    Copyright
    SocialLinks
    Custom(String)
    } derive(Eq, ToJson,
    Debug
    ,
    FromJson
    )

    Footer element types
    pub(all) struct FooterLink {
    label : String
    href : String
    } derive(ToJson,
    FromJson
    )

    Footer link

    HeaderConfig

    pub(all) struct HeaderConfig {
    left : Array[HeaderElement]
    center : Array[HeaderElement]
    right : Array[HeaderElement]
    sticky : Bool
    } derive(ToJson,
    FromJson
    )

    Header configuration with element slots

    HeaderConfig::default

    fn HeaderConfig::default() -> HeaderConfig

    Default header configuration

    HeaderElement

    pub(all) enum HeaderElement {
    Logo
    NavLinks
    Search
    ThemeToggle
    SocialLinks
    LangSwitcher
    Spacer
    Custom(String)
    } derive(Eq, ToJson,
    Debug
    ,
    FromJson
    )

    Header element types for compositional header layout

    Heading

    pub(all) struct Heading {
    level : Int
    text : String
    id : String
    }

    Heading information (for TOC, llms.txt)

    Heading::new

    fn Heading::new(level~ : Int, text~ : String, id? : String) -> Heading

    I18nConfig

    pub(all) struct I18nConfig {
    default_locale : String
    locales : Array[LocaleConfig]
    }

    Internationalization configuration

    I18nConfig::default

    fn I18nConfig::default() -> I18nConfig

    Default i18n configuration (English only)

    IslandsConfig

    pub(all) struct IslandsConfig {
    dir : String
    base_path : String
    }

    Islands configuration for SSG (legacy)

    IslandsConfig::default

    fn IslandsConfig::default() -> IslandsConfig

    Default Islands configuration (uses docs/components convention)

    LocaleConfig

    pub(all) struct LocaleConfig {
    code : String
    label : String
    path : String
    } derive(ToJson,
    FromJson
    )

    Locale configuration

    MetaFilesConfig

    pub(all) struct MetaFilesConfig {
    sitemap : Bool
    feed : Bool
    llms_txt : Bool
    }

    Meta files generation configuration Controls generation of sitemap.xml, feed.xml (RSS), and llms.txt

    MetaFilesConfig::default

    Default meta files configuration Only sitemap is enabled by default as it's most commonly needed
    pub(all) struct NavItem {
    text : String
    link : String
    icon : String?
    items : Array[NavItem]
    } derive(ToJson,
    FromJson
    )

    Navigation item for top navbar
    fn NavItem::new(text : String, link : String) -> NavItem

    Default empty NavItem
    pub(all) struct NavigationConfig {
    spa : Bool
    view_transitions : Bool
    keyboard : Bool
    } derive(ToJson,
    FromJson
    )

    Navigation configuration for SPA-like behavior

    Default navigation configuration

    OgpConfig

    pub(all) struct OgpConfig {
    site_url : String?
    image : String?
    twitter_handle : String?
    twitter_card : String?
    }

    OGP (Open Graph Protocol) configuration

    OgpConfig::default

    fn OgpConfig::default() -> OgpConfig

    Default OGP configuration

    PageInfo

    pub(all) struct PageInfo {
    id : String
    url_path : String
    source_path : String
    title : String
    description : String
    locale : String
    renderer_type : RendererType
    updated_at : String
    content_md : String
    headings : Array[Heading]
    sort_key : String
    }

    Complete information for a single page

    PageInfo::canonical_url

    fn PageInfo::canonical_url(self : PageInfo, base_url : String) -> String

    Generate canonical URL

    PageInfo::new

    fn PageInfo::new(id~ : String, url_path~ : String, source_path? : String, title? : String, description? : String, locale? : String, renderer_type? : RendererType, updated_at? : String, content_md? : String, headings? : Array[Heading], sort_key? : String) -> PageInfo

    PageMeta

    pub(all) struct PageMeta {
    source_path : String
    url_path : String
    content_type : ContentType
    renderer_type : RendererType
    frontmatter :
    Frontmatter

    last_modified : String?
    locale : String
    canonical_path : String
    sort_key : String
    }

    Page metadata for routing and generation

    PageMeta::new

    fn PageMeta::new(source_path : String, url_path : String, content_type? : ContentType, renderer_type? : RendererType, locale? : String, canonical_path? : String, sort_key? : String) -> PageMeta

    Create PageMeta from source path

    PageMeta::with_frontmatter

    Set frontmatter

    RendererType

    pub(all) enum RendererType {
    MarkdownRenderer
    HtmlRenderer
    LunaRenderer
    ReactRenderer
    PreactRenderer
    ClientOnlyRenderer
    MdxRenderer
    } derive(Eq,
    Debug
    )

    Renderer types supported by Sol SSG

    RendererType::default

    fn RendererType::default() -> RendererType

    Default renderer type

    SidebarConfig

    pub(all) enum SidebarConfig {
    Auto
    Manual(Array[SidebarGroup])
    }

    Sidebar configuration

    SidebarGroup

    pub(all) struct SidebarGroup {
    text : String
    link : String?
    collapsed : Bool
    items : Array[SidebarItem]
    } derive(ToJson,
    FromJson
    )

    Sidebar group

    SidebarItem

    pub(all) enum SidebarItem {
    Link(String, String)
    Group(SidebarGroup)
    } derive(ToJson,
    FromJson
    )

    Sidebar item

    SiteInfo

    pub(all) struct SiteInfo {
    title : String
    description : String
    base_url : String
    language : String
    updated_at : String
    }

    Site-wide metadata

    SiteInfo::new

    fn SiteInfo::new(title~ : String, description? : String, base_url? : String, language? : String, updated_at? : String) -> SiteInfo

    pub(all) struct SocialLink {
    icon : String
    link : String
    } derive(ToJson,
    FromJson
    )

    Social link configuration

    SsgConfig

    pub(all) struct SsgConfig {
    docs_dir : String
    output_dir : String
    title : String
    base_url : String
    nav : Array[NavItem]
    sidebar : SidebarConfig
    islands : IslandsConfig?
    theme : ThemeConfig
    navigation : NavigationConfig
    i18n : I18nConfig
    exclude : Array[String]
    trailing_slash : Bool
    ogp : OgpConfig
    ogp_text : String?
    head_snippets : Array[String]
    body_snippets : Array[String]
    prod_body_snippets : Array[String]
    sanitize_html : Bool
    deploy_target : DeployTarget
    spa_routes : Array[String]
    components_dir : String?
    css_utilities : CssUtilitiesConfig?
    meta_files : MetaFilesConfig
    }

    SSG Configuration from sol.config.json

    SsgConfig::default

    fn SsgConfig::default() -> SsgConfig

    Default SSG configuration

    ThemeConfig

    pub(all) struct ThemeConfig {
    primary_color : String?
    secondary_color : String?
    accent_color : String?
    success_color : String?
    warning_color : String?
    error_color : String?
    text_color : String?
    text_muted_color : String?
    heading_color : String?
    link_color : String?
    link_hover_color : String?
    background_color : String?
    surface_color : String?
    sidebar_bg_color : String?
    input_bg_color : String?
    code_bg_color : String?
    border_color : String?
    focus_ring_color : String?
    logo : String?
    header : HeaderConfig?
    footer : FooterConfig?
    social_links : Array[SocialLink]
    }

    Theme configuration

    ThemeConfig::default

    fn ThemeConfig::default() -> ThemeConfig

    Default theme configuration Luna theme: Moon-inspired gold color

    TreeNode

    pub(all) enum TreeNode {
    Section(String, String, Array[TreeNode])
    Page(String, RendererType)
    }

    Tree node representing hierarchical structure

    TreeNode::flatten

    fn TreeNode::flatten(self : TreeNode) -> Array[String]

    Flatten TreeNode to array of page IDs

    build_localized_url

    fn build_localized_url(canonical_path : String, locale : String, i18n : I18nConfig) -> String

    Build localized URL path from canonical path and locale e.g., ("/guide/intro", "ja", config) -> "/ja/guide/intro" ("/guide/intro", "en", config) -> "/guide/intro"

    build_sort_key

    fn build_sort_key(path : String) -> String

    Build sort key from path (preserves structure for proper sorting) e.g., "00_guide/01_intro.md" -> "00000000_guide/00000001_intro.md"

    capitalize_first

    fn capitalize_first(s : String) -> String

    Capitalize first letter of a string

    compare_strings_lex

    fn compare_strings_lex(a : String, b : String) -> Int

    Compare two strings with proper lexicographic ordering

    detect_locale_from_path

    fn detect_locale_from_path(path : String, i18n : I18nConfig, trailing_slash? : Bool) -> (String, String)

    Detect locale from file path using directory prefix or filename suffix e.g., "ja/guide/intro.md" -> ("ja", "/guide/intro/") // directory-based "guide/intro.ja.md" -> ("ja", "/guide/intro/") // suffix-based "guide/intro.md" -> ("en", "/guide/intro/") // default locale

    extract_locale_suffix

    fn extract_locale_suffix(filename : String, locale_codes : Array[String]) -> (String, String)?

    Extract locale suffix from filename "intro.ja.md" → Some(("ja", "intro.md")) "intro.md" → None

    file_to_url_path

    fn file_to_url_path(file_path : String, trailing_slash? : Bool) -> String

    Convert file path to URL path (strips numeric prefixes) 00_guide/01_getting-started.md -> /guide/getting-started/ (with trailing slash) 00_guide/01_getting-started.md -> /guide/getting-started (without trailing slash) docs/index.md -> /

    generate_llms_txt

    fn generate_llms_txt(tree : DocumentTree) -> String

    Generate llms.txt from DocumentTree Reference: https://llmstxt.org/

    generate_rss

    fn generate_rss(tree : DocumentTree, limit? : Int) -> String

    Generate RSS 2.0 feed from DocumentTree

    generate_sitemap

    fn generate_sitemap(tree : DocumentTree) -> String

    Generate sitemap.xml from DocumentTree

    get_dir_name

    fn get_dir_name(path : String) -> String

    Get directory part of a path

    get_filename

    fn get_filename(path : String) -> String

    Get filename from path "guide/intro.ja.md" → "intro.ja.md"

    get_locale_config

    fn get_locale_config(i18n : I18nConfig, locale_code : String) -> LocaleConfig?

    Get locale config by code

    get_locale_url_prefix

    fn get_locale_url_prefix(i18n : I18nConfig, locale_code : String) -> String

    Get URL prefix for a locale Returns empty string for default locale, or the locale path otherwise

    get_url_dir

    fn get_url_dir(url_path : String) -> String

    Get directory part of URL path

    is_default_locale

    fn is_default_locale(i18n : I18nConfig, locale_code : String) -> Bool

    Check if a locale is the default locale

    pad_number

    fn pad_number(n : Int, width : Int) -> String

    Pad number with leading zeros

    parse_int_simple

    fn parse_int_simple(s : String) -> Int

    Simple integer parser (positive only)

    parse_numeric_prefix

    fn parse_numeric_prefix(name : String) -> (Int, String)

    Parse numeric prefix from a path segment (e.g., "00_guide" -> (0, "guide")) Returns (sort_order, stripped_name). If no prefix, returns (max_int, original)

    parse_ssg_config

    fn parse_ssg_config(json_str : String) -> SsgConfig?

    Parse SSG config directly (sol.config.json format)

    parse_ssg_from_sol_config

    fn parse_ssg_from_sol_config(json_str : String) -> SsgConfig?

    Parse SSG config from sol.config.json content

    replace_filename

    fn replace_filename(path : String, new_filename : String) -> String

    Replace filename in path ("guide/intro.ja.md", "intro.md") → "guide/intro.md"

    resolve_base_url

    fn resolve_base_url(base_url : String, path : String) -> String

    Resolve a URL path with the site's base_url prefix.
    • Absolute paths starting with "/" get base_url prepended
    • External URLs (http://, https://) pass through unchanged
    • Anchor-only links (#...) pass through unchanged
    • base_url "/" is a no-op

    Examples: resolve_base_url("/sol.mbt/", "/intro/") => "/sol.mbt/intro/" resolve_base_url("/", "/intro/") => "/intro/" resolve_base_url("/sol.mbt/", "https://example.com") => "https://example.com" resolve_base_url("/sol.mbt/", "#section") => "#section"

    strip_locale_from_url

    fn strip_locale_from_url(url_path : String, i18n : I18nConfig) -> (String, String)

    Strip locale prefix from URL path e.g., "/ja/guide/intro/" with i18n config -> "/guide/intro/"

    strip_numeric_prefixes

    fn strip_numeric_prefixes(path : String) -> String

    Strip numeric prefixes from all segments of a path e.g., "00_guide/01_intro.md" -> "guide/intro.md"

    url_to_output_path

    fn url_to_output_path(url_path : String) -> String

    Convert URL path to output file path / -> index.html /guide/intro -> guide/intro/index.html