Static site generator · ottogen

Otto-matically build sites with Otto

Otto renders AsciiDoc into a static site, and its conventions come from Jekyll: layouts, includes, data files, posts, drafts, permalinks, and custom collections. Install the ottogen gem, run otto init, and you have a site you can build and serve locally. This page is the whole manual, from the install line to an AsciiDoc primer. It needs Ruby 3.0 or newer.

MIT licensed · Ruby 3.0 or newer · Everything below comes from the repository README, the gemspec, and the CLI source.

01 Part 1 of 3, Getting Otto running

What Otto is

An AsciiDoc-powered static site generator with Jekyll-style conventions: layouts, includes, data files, posts, drafts, permalinks, and custom collections.

  • AsciiDoc

    Otto uses AsciiDoc markup to generate HTML.

  • Jekyll-style conventions

    Layouts, includes, data files, posts, drafts, permalinks, and custom collections.

  • Built-in server

    otto serve serves _build/ on port 8778. otto watch rebuilds when a file changes.

02 Part 1 of 3, Getting Otto running

Install

One gem, one executable. The gem is ottogen; the command it installs is otto.

shellinstall
gem install ottogen

Otto requires Ruby 3.0 or newer.

03 Part 1 of 3, Getting Otto running

Quickstart

Five lines take you from an empty directory to a site in a browser.

shellquickstart
gem install ottogen
mkdir mysite && cd mysite
otto init
otto build
otto serve
open http://127.0.0.1:8778/

Open http://127.0.0.1:8778/. You should see the welcome page, wrapped in the default layout.

04 Part 1 of 3, Getting Otto running

Commands

The whole surface of the CLI, in nine lines.

Otto commands and what each one does
CommandDescription
otto init [DIR]Scaffold a new site (current dir if omitted)
otto buildRender the site to _build/
otto build --draftsInclude posts from _drafts/
otto watchRebuild on file change
otto serveServe _build/ on port 8778
otto generate PAGECreate a new page in pages/
otto post "Title"Create a new dated post in _posts/
otto cleanDelete _build/
otto doctorSanity-check project layout
  • Creates files: init, generate, post
  • Renders: build, watch, serve
  • Deletes _build/: clean
05 Part 1 of 3, Getting Otto running

Project layout

otto init writes this tree. Each directory has one job, and the leading underscore marks the ones Otto reads rather than copies.

my-site/scaffold
my-site/
├── .otto                 # marker
├── config.yml            # site config
├── assets/               # copied verbatim into _build/
├── pages/                # AsciiDoc pages, output mirrors path
├── _layouts/             # ERB layouts (.html.erb)
├── _includes/            # ERB partials
├── _data/                # YAML/JSON files exposed as site.data.*
├── _posts/               # YYYY-MM-DD-slug.adoc
└── _drafts/              # undated drafts (excluded by default)
06 Part 1 of 3, Getting Otto running

Configuration

Site-wide settings live in config.yml at the project root.

config.ymldefaults
title: My Otto Site
description: Things I write
url: https://example.com
baseurl: ""

permalink: /:year/:month/:day/:slug/

collections:
  recipes:
    output: true

The permalink setting accepts five tokens: :year, :month, :day, :slug, and :title. If a template ends in /, Otto writes <path>/index.html and you get a pretty URL.

07 Part 2 of 3, Site conventions

Pages and posts

Both are AsciiDoc files with YAML front matter, and both go through the same rendering pipeline. The difference is where they live and how you name them.

Authoring a page

Pages live under pages/. The output path mirrors the source path, so pages/about.adoc becomes /about.html.

pages/about.adocpage
---
layout: default
title: About
---
= About

This is the about page.

The --- block is YAML front matter, and its values reach you two ways:

In the AsciiDoc body
{page_title}, {page_url}, {site_title}, and the rest.
In an ERB layout
<%= page.title %>, <%= site.title %>.

Authoring a post

Run otto post "My First Post" to scaffold a post. It creates _posts/YYYY-MM-DD-my-first-post.adoc and fills in the front matter for you.

Posts share the page pipeline, and they add three behaviors:

  • Otto derives the date and the slug from the filename.
  • Every post appears in site.posts, sorted by date descending.
  • tags: and categories: in front matter feed site.tags and site.categories, which are hashes keyed by tag or category.
08 Part 2 of 3, Site conventions

Layouts and includes

A layout wraps rendered AsciiDoc in HTML. A partial is a fragment you pull into a layout. Both are ERB.

Layouts

Layouts live in _layouts/. The default scaffold gives you _layouts/default.html.erb.

_layouts/default.html.erblayout
<!DOCTYPE html>
<html>
  <head><title><%= page.title %></title></head>
  <body><%= content %></body>
</html>

Every layout is an ERB template with three locals:

content
The rendered AsciiDoc body of the page or post.
page
The page or post object, carrying its front matter, its URL, and so on.
site
The site config and data, including the title, the posts, and the collections.

Layouts can chain, because a layout may declare front matter of its own:

_layouts/post.html.erbchained layout
---
layout: default
---
<article><%= content %></article>

So Otto wraps a page using layout: post in _layouts/post.html.erb, then wraps that result in _layouts/default.html.erb.

Includes (partials)

Drop reusable HTML or ERB into _includes/. Then call <%= partial 'name.html' %> from any layout, or from another partial.

_includes/partials
_includes/
  header.html
  footer.html

Partials see the same site, page, and content as the calling layout.

09 Part 2 of 3, Site conventions

Data files

Otto exposes anything in _data/ as site.data.<filename>.

_data/nav.ymldata
- title: Home
  url: /
- title: About
  url: /about
_includes/nav.htmlusage
<nav>
  <% site.data.nav.each do |item| %>
    <a href="<%= item['url'] %>"><%= item['title'] %></a>
  <% end %>
</nav>

Otto reads both YAML and JSON.

10 Part 2 of 3, Site conventions

Collections

A collection is a group of documents that is neither a page nor a post. Declare it in config.yml, then create the matching directory.

config.ymlcollection
collections:
  recipes:
    output: true

Files in _recipes/*.adoc become site.recipes, an array of items. With output: true they render to /recipes/<slug>.html. With output: false they are available to layouts, but Otto does not write them to disk.

11 Part 2 of 3, Site conventions

Permalinks

Set a URL on one document, or a default for every post.

To customize one document, put permalink in its front matter:

front matterper document
---
permalink: /custom/path.html
---

To set a default for every post, put it in config.yml:

config.ymlglobal default
permalink: /:year/:month/:day/:slug/

A template ending in / produces a pretty URL, because Otto writes <path>/index.html instead of a bare .html file. Five tokens are available: :year, :month, :day, :slug, and :title.

12 Part 2 of 3, Site conventions

Drafts

Drafts live in _drafts/<slug>.adoc, with no date prefix on the filename.

Default

Otto excludes drafts from a build. Pass --drafts to include them.

shellopt in
otto build --drafts
otto watch --drafts

When you include them, drafts take today's date, which sorts them to the top of site.posts.

13 Part 2 of 3, Site conventions

Health checks

One command tells you whether the project layout is intact.

shellcheck
otto doctor

otto doctor verifies the project layout and reports any missing files.

14 Part 3 of 3, AsciiDoc

AsciiDoc primer

A working subset of AsciiDoc, enough to be productive. The full reference is at docs.asciidoctor.org.

Document structure

adocstructure
= Document title
:author: Ada Lovelace
:revdate: 2026-05-01

== Section 1

A paragraph.

== Section 2

Another paragraph.

= Title is the document title, and ==, ===, and deeper are subsections. An attribute below the title, in the form :key: value, sets a document-wide variable.

Paragraphs and text formatting

adocinline
A normal paragraph. *Bold*, _italic_, `monospace`, ~subscript~, ^superscript^.

A second paragraph separated by a blank line.

Links

adoclinks
https://example.com[Example]
link:about.html[About this site]

Lists

adoclists
* unordered
* bullet
** nested

. ordered
. one
. two

term:: definition
another term:: another definition

Code blocks

adocsource block
[source,ruby]
----
def hello
  puts "world"
end
----

Admonitions

adocadmonitions
NOTE: A short note in line.

[WARNING]
====
A longer warning block
that spans multiple lines.
====

Five levels are available: NOTE, TIP, IMPORTANT, WARNING, and CAUTION.

Tables

adoctables
|===
| Column 1 | Column 2

| cell A1  | cell A2
| cell B1  | cell B2
|===

Images

adocimages
image::pictures/cat.jpg[A cat, 400, 300]

The first argument is alt text, and the numbers are width and height.

Includes

adocincludes
include::shared/disclaimer.adoc[]

An AsciiDoc include pulls file content in at conversion time. Paths are relative to the including file.

Attributes in content

Reference site and page metadata anywhere in the body:

adocattributes
This is {site_title}, written by {site_author}.
You're reading "{page_title}" — see all posts at {site_url}/blog.

Otto prefixes these keys with site_, for values from config.yml, or page_, for values from the document's own front matter.

IDs and roles

adocids and roles
[#main-section,role="lead"]
== Hello

That adds id="main-section" and class="lead" to the rendered section.