statica-cli 0.49.0

A blazingly fast static site generator that builds on just HTML
use std::fs;
use std::path::{Path, PathBuf};

use anyhow::{bail, Context, Result};

use crate::config::{StaticaConfig, CONFIG_FILE};
use crate::style;

pub fn run(name: &str) -> Result<()> {
    let root = PathBuf::from(name);
    scaffold(&root, name)?;

    eprintln!(
        "{} {}",
        style::success("created"),
        style::bold(root.display().to_string())
    );
    eprintln!("  {}", style::dim(format!("cd {name} && statica")));
    eprintln!("  {}", style::dim(format!("statica build {name}")));
    Ok(())
}

fn scaffold(root: &Path, name: &str) -> Result<()> {
    if root.exists() {
        bail!("path already exists: {}", root.display());
    }

    for dir in [
        root.join("content").join("i18n"),
        root.join("[locale]"),
        root.join("public"),
    ] {
        fs::create_dir_all(&dir).with_context(|| format!("create {}", dir.display()))?;
    }

    write(
        &root.join(CONFIG_FILE),
        &format!(
            "{}\n[i18n]\nenabled = true\ndefault = \"en\"\nlocales = [\"en\", \"fr\", \"pt\"]\ndir = \"content/i18n\"\n",
            StaticaConfig::default_toml()
        ),
    )?;
    fs::copy(
        Path::new(env!("CARGO_MANIFEST_DIR"))
            .join("../..")
            .join("logo/statica-badge-green.png"),
        root.join("public/statica-logo.png"),
    )
    .with_context(|| format!("write {}", root.join("public/statica-logo.png").display()))?;

    write(
        &root.join("content/i18n/en.json"),
        r#"{
  "home": {
    "title": "statica starter",
    "guide": "Guide",
    "github": "Star on GitHub",
    "copyright": "© 2026 statica. Just HTML."
  }
}
"#,
    )?;
    write(
        &root.join("content/i18n/fr.json"),
        r#"{
  "home": {
    "title": "starter statica",
    "guide": "Guide",
    "github": "Suivre sur GitHub",
    "copyright": "© 2026 statica. Juste HTML."
  }
}
"#,
    )?;
    write(
        &root.join("content/i18n/pt.json"),
        r#"{
  "home": {
    "title": "starter statica",
    "guide": "Guia",
    "github": "Marcar no GitHub",
    "copyright": "© 2026 statica. Apenas HTML."
  }
}
"#,
    )?;
    write(
        &root.join("[locale]/index.html"),
        r##"<!doctype html>
<html lang="${i18n.locale}" data-bind="{i18n}">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <title data-t="${i18n.home.title}">statica starter</title>
    <style>
      :root {
        color-scheme: light;
        font-family:
          Karla, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont,
          "Segoe UI", sans-serif;
        background: #f8fafc;
        color: #0f172a;
      }
      body {
        margin: 0;
        min-height: 100vh;
        display: grid;
        place-items: center;
        background-image:
          linear-gradient(color-mix(in oklab, currentColor 4%, transparent) 1px, transparent 1px),
          linear-gradient(90deg, color-mix(in oklab, currentColor 4%, transparent) 1px, transparent 1px);
        background-size: 28px 28px;
      }
      main {
        display: grid;
        justify-items: center;
        gap: 1.5rem;
        width: min(100% - 2rem, 34rem);
        text-align: center;
      }
      .logo {
        display: grid;
        place-items: center;
        gap: 1rem;
      }
      .logo-mark {
        width: clamp(6rem, 18vw, 9rem);
        height: clamp(6rem, 18vw, 9rem);
        object-fit: contain;
        filter: drop-shadow(0 1rem 1.8rem color-mix(in oklab, #089868 22%, transparent));
      }
      h1 {
        margin: 0;
        font-size: clamp(2rem, 8vw, 4.5rem);
        line-height: 1;
        letter-spacing: -0.06em;
        font-weight: 950;
      }
      nav {
        display: flex;
        flex-wrap: wrap;
        justify-content: center;
        gap: 0.75rem;
      }
      a {
        color: #089868;
        font-weight: 700;
        text-decoration-thickness: 0.08em;
        text-underline-offset: 0.25em;
      }
      .locales {
        gap: 0.5rem;
        font-size: 0.9rem;
      }
      footer {
        color: #64748b;
        font-size: 0.9rem;
      }
    </style>
  </head>
  <body>
    <main>
      <div class="logo" aria-label="statica">
        <img class="logo-mark" src="../public/statica-logo.png" alt="statica logo" />
        <h1 data-t="${i18n.home.title}">statica starter</h1>
      </div>

      <nav aria-label="Project links">
        <a href="https://github.com/akaizn-junior/statica/blob/main/docs/guide.md" data-t="${i18n.home.guide}">Guide</a>
        <a href="https://github.com/akaizn-junior/statica" data-t="${i18n.home.github}">Star on GitHub</a>
      </nav>

      <nav class="locales" aria-label="Languages">
        <a href="../en/">English</a>
        <a href="../fr/">Français</a>
        <a href="../pt/">Português</a>
      </nav>

      <footer data-t="${i18n.home.copyright}">© 2026 statica. Just HTML.</footer>
    </main>
  </body>
</html>
"##,
    )?;
    write(
        &root.join("README.md"),
        &format!(
            r#"# {name}

**Just HTML.** A blazingly fast static site generator that builds on just HTML

Install the CLI with Rust (`cargo install statica-cli --locked`) or npm (`npm i -D @statica/cli`), then:

```bash
statica
statica build
```

Settings live in `statica.toml` (optional; defaults apply if missing).

- Pages are every `**/index.html` (folder = route).
- `[locale]/index.html` emits localized pages for English, French, and Portuguese.
- Translation catalogs live in `content/i18n/{{locale}}.json`.
- Pages bind canonical roots such as `{{i18n}}` before use.
- Scalar text goes in `data-t="${{i18n.home.title}}"`.
"#
        ),
    )?;

    Ok(())
}

fn write(path: &Path, contents: &str) -> Result<()> {
    fs::write(path, contents).with_context(|| format!("write {}", path.display()))
}

#[cfg(test)]
mod tests {
    use super::*;
    use statica::{build, BuildOptions, I18nOptions};

    fn temp_dir() -> PathBuf {
        std::env::temp_dir().join(format!(
            "statica-new-{}-{}",
            std::process::id(),
            std::time::SystemTime::now()
                .duration_since(std::time::UNIX_EPOCH)
                .unwrap()
                .as_nanos()
        ))
    }

    #[test]
    fn scaffold_builds_without_authoring_errors() {
        let root = temp_dir();
        scaffold(&root, "starter").unwrap();

        let mut opts = BuildOptions::new(&root);
        opts.out_dir = root.join(".website");
        opts.i18n = I18nOptions {
            enabled: true,
            default_locale: "en".into(),
            locales: vec!["en".into(), "fr".into(), "pt".into()],
            dir: "content/i18n".into(),
            ..Default::default()
        };
        build(&opts).unwrap();

        let home = fs::read_to_string(root.join(".website/en/index.html")).unwrap();
        assert!(home.contains("statica starter"));
        assert!(home.contains("../public/statica-logo.png"));
        assert!(home.contains("https://github.com/akaizn-junior/statica/blob/main/docs/guide.md"));
        assert!(home.contains("Star on GitHub"));
        assert!(root.join(".website/public/statica-logo.png").exists());
        let fr = fs::read_to_string(root.join(".website/fr/index.html")).unwrap();
        assert!(fr.contains("starter statica"));
        let pt = fs::read_to_string(root.join(".website/pt/index.html")).unwrap();
        assert!(pt.contains("Apenas HTML"));

        let _ = fs::remove_dir_all(root);
    }
}