Skip to content

Migrating from Crystal's built-in doc generator#

This assumes you already have some project in Crystal, say, a file src/foo.cr.

src/foo.cr
module Foo
  # Description of *Bar*
  class Bar
    # Description of *f*
    def f(x : Int32)
    end
  end

  # Description of *g*
  #
  # See also: `Bar#f`
  def g(bar : Bar)
  end
end

Hosting on GitHub is also assumed, though that's easy to adapt.

We'll be working from the project's root directory (the one that contains src).

View the final file layout

Dependencies#

The dependencies that we'll be using can be installed like this:

$ pip install mkdocs-material mkdocstrings-crystal mkdocs-gen-files mkdocs-literate-nav mkdocs-section-index

This assumes you have Python installed, with pip available.

Tip

You might want to install these in a virtualenv (i.e. localized just to this project). Otherwise, they go to ~/.local/lib/python*.

And check out how to manage Python dependencies long-term.

Base config#

Let's configure MkDocs with mkdocstrings-crystal. Add/merge this config as your mkdocs.yml:

mkdocs.yml
site_name: My Project
site_url: https://username.github.io/my-project/
repo_url: https://github.com/username/my-project
edit_uri: blob/master/docs/

theme:
  name: material
  icon:
    repo: fontawesome/brands/github

extra_css:
  - css/mkdocstrings.css

plugins:
  - search
  - gen-files:
      scripts:
        - docs/gen_doc_stubs.py
  - mkdocstrings:
      default_handler: crystal
      watch: [src]
  - literate-nav:
      nav_file: SUMMARY.md
  - section-index

markdown_extensions:
  - pymdownx.highlight
  - deduplicate-toc
  - toc:
      permalink: "#"
Why configure like this
gen-files plugin
Crystal's API generator automatically creates one HTML file per Crystal class. mkdocstrings doesn't do anything like that by itself, instead giving you the ability to present a story and perhaps mention several items per page. However, in this guide we choose to do a 1:1 migration and don't want to manually create all those pages (e.g. a page Foo/Bar/index.md containing just ::: Foo::Bar, so on and so on). Continued
literate-nav plugin
Right now it's not doing anything, we'll get back to it.
section-index plugin
In Crystal's API doc sites we are used to having double functionality behind clicking an item in the left side navigation: it both opens a type's page and expands its sub-types. Well, in MkDocs world that's very much non-standard. But we bring that back with this plugin.
deduplicate-toc extension
This is actually an integral part of mkdocstrings-crystal. Read more.
toc: permalink: "#"
Add an "#" anchor link after every heading so it's easy to link back to it. Not required, but very common, and Crystal doc generator does it too.
extra_css
Don't forget to copy and include the recommended styles.

Important

The "literate-nav" plugin must appear before "section-index" in the list, because it overwrites the nav.

Generate doc stub pages#

Add a script that will automatically populate a page for each type that exists in your project, with appropriate navigation. We're not populating the actual content, just inserting a small placeholder into each file for mkdocstrings to pick up.

docs/gen_doc_stubs.py
# Generate virtual doc files for the mkdocs site.
# You can also run this script directly to actually write out those files, as a preview.

import mkdocs_gen_files

# Get the documentation root object
root = mkdocs_gen_files.config["plugins"]["mkdocstrings"].get_handler("crystal").collector.root

# For each type (e.g. "Foo::Bar")
for typ in root.walk_types():
    # Use the file name "Foo/Bar/index.md"
    filename = "/".join(typ.abs_id.split("::") + ["index.md"])
    # Make a file with the content "# ::: Foo::Bar\n"
    with mkdocs_gen_files.open(filename, "w") as f:
        print(f"# ::: {typ.abs_id}", file=f)

    # Link to the type itself when clicking the "edit" button on the page.
    if typ.locations:
        mkdocs_gen_files.set_edit_path(filename, typ.locations[0].url)

This script is picked up to run seamlessly as part of the normal site build, because we had configured the gen-files plugin, which is already explained above.

Tip

Find more advanced examples of such scripts in Showcase.

Add a normal page#

docs/index.md
# Introduction

Welcome to my project!

Make sure to check out the [API documentation](Foo/),
particularly [Foo#g(bar)][].

We linked directly to an identifier here, and mkdocstrings knows which page it's on and automatically links that. See: identifier linking syntax.

View the site#

That's it -- we're ready!

$ mkdocs build  # generate from docs/ into site/
$ mkdocs serve  # live preview

Setting up navigation long-term#

This is finally where the literate-nav plugin comes in. Eventually you'll want to define an order for your pages to appear in the navigation, but if you have so many generated API doc pages, that becomes infeasible. MkDocs doesn't have any way to infer only parts of the navigation, only all or nothing. So, let's start a nav file.

docs/SUMMARY.md
* [Introduction](index.md)
* [API](Foo/)

First presumably you'd have a concrete list of pages, and as the last item, perhaps you'd leave the whole Foo/ sub-structure to be inferred.

See "Partly inferred nav" examples in Showcase.

Also check out the "navigation tabs" theme feature.

Really preserving the URLs#

(This is optional.)

We've been generating URLs such as /Foo/Bar/ (or /Foo/Bar/index.html). But to actually keep all the URLs exactly like crystal doc's site (for a non-breaking migration), we choose to make them look like /Foo/Bar.html instead.

Add this to the top of mkdocs.yml -- this is the actual change making it so:

use_directory_urls: false

But if you make this change by itself, MkDocs will no longer be able to group them into subsections nicely by default; it just doesn't see /Foo.md as the index page for the section containing /Foo/*.md, instead they'll be confusingly split into two.

So the navigation now has to be specified fully explicitly. As we've been generating the stub files, let's also generate the nav file itself. The file docs/gen_doc_stubs.py will gain these additions:

docs/gen_doc_stubs.py
# Generate virtual doc files for the mkdocs site.
# You can also run this script directly to actually write out those files, as a preview.

import mkdocs_gen_files

# Get the documentation root object
root = mkdocs_gen_files.config["plugins"]["mkdocstrings"].get_handler("crystal").root

# Start a navigation collection (to be filled as we go along)
nav = mkdocs_gen_files.Nav()

# For each type (e.g. "Foo::Bar")
for typ in root.walk_types():
    # Use the file name "Foo/Bar.md"
    filename = "/".join(typ.abs_id.split("::")) + ".md"
    # Make a file with the content "# ::: Foo::Bar\n"
    with mkdocs_gen_files.open(filename, "w") as f:
        print(f"# ::: {typ.abs_id}", file=f)

    # Link to the type itself when clicking the "edit" button on the page
    if typ.locations:
        mkdocs_gen_files.set_edit_path(filename, typ.locations[0].url)

    # Append to the nav: "    * [Bar](Foo/Bar.md)"
    nav[typ.abs_id.split("::")] = filename

# Append the nav to a "literate nav" file
with mkdocs_gen_files.open("SUMMARY.md", "a") as nav_file:
    nav_file.writelines(nav.build_literate_nav())