README.md

# Sayfa

[![Hex Version](https://img.shields.io/hexpm/v/sayfa.svg)](https://hex.pm/packages/sayfa)
[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
[![Elixir](https://img.shields.io/badge/elixir-~%3E%201.18-purple.svg)](https://elixir-lang.org/)

A simple, extensible static site generator built in Elixir. **Sayfa** means "page" in Turkish.

[Turkce README / Turkish README](README.tr.md)

---

## Table of Contents

- [What is Sayfa?](#what-is-sayfa)
- [Features](#features)
- [Requirements](#requirements)
- [Quick Start](#quick-start)
- [Content Types](#content-types)
- [Front Matter](#front-matter)
- [Layouts & Templates](#layouts--templates)
- [Blocks](#blocks)
- [Themes](#themes)
- [Multilingual Support](#multilingual-support)
- [Feeds & SEO](#feeds--seo)
- [Configuration](#configuration)
- [CLI Commands](#cli-commands)
- [Project Structure](#project-structure)
- [Extensibility](#extensibility)
- [Roadmap](#roadmap)
- [Contributing](#contributing)
- [License](#license)

---

## What is Sayfa?

Sayfa follows a **two-layer architecture**:

1. **Sayfa** (this package) — A reusable Hex package with the core static site generation engine: markdown parsing, template rendering, feed generation, block system, and more.
2. **Your site** — A project that depends on Sayfa via `{:sayfa, "~> 0.1"}`. You bring your content, theme, and configuration; Sayfa handles the build.

```
┌──────────────────────────────────────────────────────┐
│                  YOUR WEBSITE                        │
│   content/     themes/     lib/blocks/    config/    │
└──────────────────────────┬───────────────────────────┘
                           │ {:sayfa, "~> 0.1"}
                           ▼
┌──────────────────────────────────────────────────────┐
│                  SAYFA (Hex Package)                 │
│  Builder, Content, Markdown, Feed, Sitemap, Blocks   │
└──────────────────────────────────────────────────────┘
```

### Design Philosophy

- **Simple** — Convention over configuration. Sensible defaults, minimal boilerplate.
- **Extensible** — Blocks, hooks, content types, and themes are all pluggable via behaviours.
- **Fast** — Markdown parsing powered by MDEx (Rust NIF). Incremental builds with caching.
- **No Node.js** — Uses standalone TailwindCSS CLI and Pagefind. Pure Elixir + Rust.

---

## Features

### Core
- Markdown with syntax highlighting (MDEx, Rust NIF)
- YAML front matter with typed fields + `meta` catch-all
- Two-struct content pipeline (`Raw` -> `Content`) for maximum flexibility

### Content Organization
- 5 built-in content types (posts, notes, projects, talks, pages)
- Categories and tags with auto-generated archive pages
- Pagination with configurable page size
- Collections API (filter, sort, group, recent)

### Templates & Theming
- Three-layer template composition (content -> layout -> base)
- 9 built-in blocks (hero, header, footer, social links, TOC, recent posts, tag cloud, reading time, code copy)
- Theme inheritance (custom -> parent -> default)
- EEx templates with `@block` helper

### Internationalization
- Directory-based multilingual support
- Per-language URL prefixes (`/tr/posts/...`)

### SEO & Feeds
- Atom/RSS feed generation
- Sitemap XML
- Pagefind static search integration
- SEO meta tags (Open Graph, description)

### Developer Experience
- `mix sayfa.new` project generator
- Dev server with file watching and hot reload
- Draft preview mode
- Build caching for incremental rebuilds
- Verbose logging with per-stage timing

---

## Requirements

| Requirement | Version | Notes |
|-------------|---------|-------|
| Elixir | ~> 1.18 | OTP 27+ |
| Rust | Latest stable | Required for MDEx NIF compilation |

Rust is a **hard requirement** — MDEx compiles a native extension for fast markdown parsing.

---

## Quick Start

```bash
# Install Sayfa's archive (for mix sayfa.new)
mix archive.install hex sayfa

# Create a new site
mix sayfa.new my_blog
cd my_blog
mix deps.get

# Build the site
mix sayfa.build

# Or start the dev server
mix sayfa.serve
```

Your site will be generated in the `output/` directory. The dev server runs at `http://localhost:4000` with hot reload.

---

## Content Types

Sayfa ships with 5 built-in content types. Each maps to a directory under `content/` and a URL prefix:

| Type | Directory | URL Pattern | Default Layout |
|------|-----------|-------------|----------------|
| Post | `content/posts/` | `/posts/{slug}/` | `post` |
| Note | `content/notes/` | `/notes/{slug}/` | `post` |
| Project | `content/projects/` | `/projects/{slug}/` | `page` |
| Talk | `content/talks/` | `/talks/{slug}/` | `page` |
| Page | `content/pages/` | `/{slug}/` | `page` |

No dates in URLs — keeps them clean and evergreen.

### Filename Convention

```
# Dated content (posts, notes)
2024-01-15-my-post-title.md  →  /posts/my-post-title/

# Undated content (projects, pages)
my-project.md                →  /projects/my-project/
about.md                     →  /about/
```

### Custom Content Types

Implement the `Sayfa.Behaviours.ContentType` behaviour:

```elixir
defmodule MyApp.ContentTypes.Recipe do
  @behaviour Sayfa.Behaviours.ContentType

  @impl true
  def name, do: :recipe

  @impl true
  def directory, do: "recipes"

  @impl true
  def url_prefix, do: "recipes"

  @impl true
  def default_layout, do: "page"

  @impl true
  def required_fields, do: [:title]
end
```

---

## Front Matter

Content files use YAML front matter delimited by `---`:

```yaml
---
title: "Building a Static Site Generator"   # Required
date: 2024-01-15                            # Required for posts/notes
slug: custom-slug                           # Optional (default: from filename)
lang: en                                    # Optional (default: site default)
description: "A brief description"          # Optional, used for SEO
categories: [elixir, tutorial]              # Optional
tags: [static-site, beginner]              # Optional
draft: false                                # Optional (default: false)
layout: custom_layout                       # Optional (default: content type's default)
---

Your markdown content here.
```

### Field Reference

| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `title` | String | *required* | Page title |
| `date` | Date | `nil` | Publication date (YYYY-MM-DD) |
| `slug` | String | from filename | URL slug |
| `lang` | Atom | site default | Content language |
| `description` | String | `""` | SEO description |
| `categories` | List | `[]` | Category names |
| `tags` | List | `[]` | Tag names |
| `draft` | Boolean | `false` | Exclude from production builds |
| `layout` | String | type default | Layout template name |

Any unrecognized fields are stored in the `meta` map and accessible in templates via `@content.meta["field_name"]`.

---

## Layouts & Templates

Sayfa uses a **three-layer composition** model:

1. **Content body** — Markdown rendered to HTML
2. **Layout template** — Wraps the content, places blocks (e.g., `post.html.eex`)
3. **Base template** — HTML shell (`<html>`, `<head>`, etc.), inserts `@inner_content`

### Selecting a Layout

A page selects its layout via front matter:

```yaml
---
title: "Welcome"
layout: home
---
```

Resolution order:
1. `layout` field in front matter
2. Content type's `default_layout`
3. `page` (fallback)

### Default Layouts

| Layout | Used For | Typical Blocks |
|--------|----------|----------------|
| `home.html.eex` | Homepage | hero, recent_posts, tag_cloud |
| `post.html.eex` | Single post/note | reading_time, toc, social_links |
| `page.html.eex` | Static pages | content only |
| `list.html.eex` | Content listings | pagination |
| `base.html.eex` | HTML wrapper | header, footer |

### Template Variables

All templates receive these assigns:

| Variable | Type | Description |
|----------|------|-------------|
| `@content` | `Sayfa.Content.t()` | Current content (nil on list pages) |
| `@contents` | `[Sayfa.Content.t()]` | All site contents |
| `@site` | `map()` | Resolved site configuration |
| `@block` | `function` | Block rendering helper |
| `@inner_content` | `String.t()` | Rendered inner HTML (base layout only) |

---

## Blocks

Blocks are reusable EEx components invoked via the `@block` helper:

```eex
<%= @block.(:hero, title: "Welcome", subtitle: "My Elixir Blog") %>
<%= @block.(:recent_posts, limit: 5) %>
<%= @block.(:tag_cloud) %>
```

### Built-in Blocks

| Block | Atom | Description |
|-------|------|-------------|
| Hero | `:hero` | Hero section with title and subtitle |
| Header | `:header` | Site header with navigation |
| Footer | `:footer` | Site footer |
| Social Links | `:social_links` | Social media link icons |
| Table of Contents | `:toc` | Auto-generated TOC from headings |
| Recent Posts | `:recent_posts` | List of recent posts |
| Tag Cloud | `:tag_cloud` | Tag cloud with counts |
| Reading Time | `:reading_time` | Estimated reading time |
| Code Copy | `:code_copy` | Copy button for code blocks |

### Custom Blocks

Implement the `Sayfa.Behaviours.Block` behaviour:

```elixir
defmodule MyApp.Blocks.Banner do
  @behaviour Sayfa.Behaviours.Block

  @impl true
  def name, do: :banner

  @impl true
  def render(assigns) do
    text = Map.get(assigns, :text, "Welcome!")
    ~s(<div class="banner">#{text}</div>)
  end
end
```

Register custom blocks in your site config:

```elixir
config :sayfa, :site,
  blocks: [
    Sayfa.Blocks.Hero,
    MyApp.Blocks.Banner
  ]
```

Then use it in templates:

```eex
<%= @block.(:banner, text: "Hello from my custom block!") %>
```

---

## Themes

### Default Theme

Sayfa ships with a minimal, documentation-style default theme. It includes all 5 layouts and basic CSS.

### Custom Themes

Create a theme directory in your project:

```
themes/
  my_theme/
    layouts/
      post.html.eex    # Override specific layouts
    assets/
      css/
        custom.css
```

Set it in config:

```elixir
config :sayfa, :site,
  theme: "my_theme"
```

### Theme Inheritance

Custom themes inherit from a parent. Any layout not overridden falls back to the parent theme:

```elixir
config :sayfa, :site,
  theme: "my_theme",
  theme_parent: "default"
```

---

## Multilingual Support

Sayfa uses a directory-based approach for multilingual content:

```
content/
  posts/
    hello-world.md          # English (default)
  tr/
    posts/
      merhaba-dunya.md      # Turkish
```

### Configuration

```elixir
config :sayfa, :site,
  default_lang: :en,
  languages: [
    en: [name: "English"],
    tr: [name: "Turkce", path: "/tr"]
  ]
```

### URL Patterns

```
English (default):  /posts/hello-world/
Turkish:            /tr/posts/merhaba-dunya/
```

---

## Feeds & SEO

### Atom Feeds

Sayfa generates Atom XML feeds automatically:

```
/feed.xml              # All content
/feed/posts.xml        # Posts only
/feed/notes.xml        # Notes only
```

### Sitemap

A `sitemap.xml` is generated at the root of the output directory containing all published pages.

### Search

Sayfa integrates with [Pagefind](https://pagefind.app/) for static search. Pagefind runs as a post-build step and generates a client-side search index — no server required.

### SEO Meta Tags

Templates automatically include Open Graph and description meta tags based on front matter fields.

---

## Configuration

Site configuration lives in `config/site.exs`:

```elixir
import Config

config :sayfa, :site,
  # Basic
  title: "My Site",
  description: "A site built with Sayfa",
  author: "Your Name",
  base_url: "https://example.com",

  # Content
  content_dir: "content",
  output_dir: "output",
  posts_per_page: 10,
  drafts: false,

  # Language
  default_lang: :en,
  languages: [en: [name: "English"]],

  # Theme
  theme: "default",
  theme_parent: "default",

  # Dev server
  port: 4000,
  verbose: false
```

### Configuration Reference

| Key | Type | Default | Description |
|-----|------|---------|-------------|
| `title` | String | `"My Site"` | Site title |
| `description` | String | `""` | Site description |
| `author` | String | `nil` | Site author |
| `base_url` | String | `"http://localhost:4000"` | Production URL |
| `content_dir` | String | `"content"` | Content source directory |
| `output_dir` | String | `"output"` | Build output directory |
| `posts_per_page` | Integer | `10` | Pagination size |
| `drafts` | Boolean | `false` | Include drafts in build |
| `default_lang` | Atom | `:en` | Default content language |
| `languages` | Keyword | `[en: [name: "English"]]` | Available languages |
| `theme` | String | `"default"` | Active theme name |
| `theme_parent` | String | `"default"` | Parent theme for inheritance |
| `port` | Integer | `4000` | Dev server port |
| `verbose` | Boolean | `false` | Verbose build logging |

---

## CLI Commands

### `mix sayfa.new`

Generate a new Sayfa site:

```bash
mix sayfa.new my_blog
mix sayfa.new my_blog --theme minimal --lang en,tr
```

### `mix sayfa.build`

Build the site:

```bash
mix sayfa.build
mix sayfa.build --drafts              # Include draft content
mix sayfa.build --verbose             # Detailed logging
mix sayfa.build --output _site        # Custom output directory
mix sayfa.build --source ./my_site    # Custom source directory
```

### `mix sayfa.serve`

Start the development server:

```bash
mix sayfa.serve
mix sayfa.serve --port 3000           # Custom port
mix sayfa.serve --drafts              # Preview drafts
```

The dev server watches for file changes and rebuilds automatically.

---

## Project Structure

A generated Sayfa site looks like this:

```
my_site/
├── config/
│   ├── config.exs
│   └── site.exs                # Site configuration
│
├── content/
│   ├── posts/                  # Blog posts
│   │   └── 2024-01-15-hello-world.md
│   ├── notes/                  # Quick notes
│   ├── projects/               # Portfolio projects
│   ├── talks/                  # Talks/presentations
│   ├── pages/                  # Static pages
│   │   └── about.md
│   └── tr/                     # Turkish translations
│       └── posts/
│
├── themes/
│   └── my_theme/               # Custom theme (optional)
│       └── layouts/
│
├── static/                     # Copied as-is to output
│   ├── images/
│   └── favicon.ico
│
├── lib/                        # Custom blocks, hooks, content types
│
├── output/                     # Generated site (git-ignored)
│
└── mix.exs
```

---

## Extensibility

Sayfa is designed to be extended via three behaviours:

### Blocks

Reusable template components. See the [Blocks](#blocks) section.

### Hooks

Inject custom logic into the build pipeline at 4 stages:

```elixir
defmodule MyApp.Hooks.InjectAnalytics do
  @behaviour Sayfa.Behaviours.Hook

  @impl true
  def stage, do: :after_render

  @impl true
  def run({content, html}, _opts) do
    {:ok, {content, html <> "<script>/* analytics */</script>"}}
  end
end
```

Register hooks in config:

```elixir
config :sayfa, :hooks, [MyApp.Hooks.InjectAnalytics]
```

**Hook stages:**

| Stage | Input | Description |
|-------|-------|-------------|
| `:before_parse` | `Content.Raw` | Before markdown rendering |
| `:after_parse` | `Content` | After parsing, before template |
| `:before_render` | `Content` | Before template rendering |
| `:after_render` | `{Content, html}` | After template rendering |

### Content Types

Define how content is organized. See [Custom Content Types](#custom-content-types).

---

## Roadmap

Future plans for Sayfa:

- Image optimization (automatic resizing, WebP conversion)
- TailwindCSS integration (build step)
- Dark mode toggle in default theme
- Deployment helpers (GitHub Pages, Netlify, Vercel)
- Plugin system for third-party extensions
- Asset fingerprinting

---

## Contributing

Contributions are welcome! See [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines.

```bash
git clone https://github.com/furkanural/sayfa.git
cd sayfa
mix deps.get
mix test
```

---

## License

MIT License. See [LICENSE](LICENSE) for details.