README.md

# gssg

[![Package Version](https://img.shields.io/hexpm/v/gssg)](https://hex.pm/packages/gssg)
[![Hex Docs](https://img.shields.io/badge/hex-docs-ffaff3)](https://hexdocs.pm/gssg/)

Batteries-not-included, templating library agnostic SSG library for Gleam.

## Features

- Simple API: Just four functions to build a complete static site
- Type-safe: Leverages Gleam's type system for compile-time guarantees
- Flexible: Works with any page type (Lustre elements, strings, custom types)
- Dynamic routes: Generate multiple pages from data (perfect for blog posts)
- Zero config: No configuration files needed, just code

## Installation

```sh
gleam add gssg
```

## Quick Start

```gleam
import gleam/dict
import gleam/io
import gleam/string
import lustre/element
import gssg/ssg

pub fn main() -> Nil {
  let result =
    ssg.new("priv", element.to_document_string)
    |> ssg.add_static_route("/", home_view())
    |> ssg.add_static_route("/about", about_view())
    |> ssg.build()

  case result {
    Ok(_) -> io.println("Build successful!")
    Error(e) -> io.println("Build failed: " <> string.inspect(e))
  }
}

fn home_view() {
  element.html([], [
    element.head([], [element.title([], "Home")]),
    element.body([], [element.h1([], [element.text("Welcome!")])]),
  ])
}

fn about_view() {
  element.html([], [
    element.head([], [element.title([], "About")]),
    element.body([], [element.h1([], [element.text("About Us")])]),
  ])
}
```

This generates:

- `priv/index.html` - Your home page
- `priv/about/index.html` - Your about page

## Usage

### Creating a site

Start by creating an SSG instance with an output directory and a render function:

```gleam
import lustre/element
import gssg/ssg

let site = ssg.new("output", element.to_document_string)
```

The render function converts your page type to HTML strings. For Lustre elements, use `element.to_document_string`.

### Adding static routes

Add individual pages with `add_static_route`:

```gleam
ssg.new("priv", element.to_document_string)
|> ssg.add_static_route("/", home.view())
|> ssg.add_static_route("/projects", projects.view())
|> ssg.add_static_route("/contact", contact.view())
```

Each route generates a file at `{path}/index.html`.

### Adding dynamic routes

Generate multiple pages from a collection of data:

```gleam
import gleam/dict
import markup/markup

pub fn main() -> Nil {
  // Parse blog posts from markdown files
  let assert Ok(posts) = markup.parse_dir("data/posts")

  ssg.new("priv", element.to_document_string)
  |> ssg.add_static_route("/", home.view())
  |> ssg.add_static_route("/blog", blog_index.view(posts))
  |> ssg.add_dynamic_route("/blog", dict.from_list(posts), blog_post.view)
  |> ssg.build()
}
```

If `posts` contains:

- `#("hello-world", post1)`
- `#("getting-started", post2)`

This generates:

- `priv/blog/hello-world/index.html`
- `priv/blog/getting-started/index.html`

### Building the site

Call `build()` to render and write all files:

```gleam
let result = ssg.build(site)

case result {
  Ok(_) -> io.println("Build successful!")
  Error(ssg.CannotWriteFile(path, reason)) -> {
    io.println("Failed to write: " <> path)
  }
}
```

## Complete Example

Here's a complete example with static pages and a blog:

```gleam
import gleam/dict
import gleam/io
import gleam/string
import lustre/element
import markup/markup
import pages/home
import pages/writing/post
import pages/writing/writing
import gssg/ssg

pub fn main() -> Nil {
  let assert Ok(posts) = markup.parse_dir("data/posts")

  let result =
    ssg.new("priv", element.to_document_string)
    |> ssg.add_static_route("/", home.view())
    |> ssg.add_static_route("/projects", markup.from("data/pages/projects.dj"))
    |> ssg.add_static_route("/writing", writing.view(posts))
    |> ssg.add_dynamic_route("/writing", dict.from_list(posts), post.view)
    |> ssg.build()

  case result {
    Ok(_) -> io.println("build successful.")
    Error(e) -> io.println("build failed: " <> string.inspect(e))
  }
}
```

## License

This project is licensed under the Apache-2.0 License.