---
description: Read source documents, metadata, and output destinations
output-file: pages.html
title: pages

---



<!-- WARNING: THIS FILE WAS AUTOGENERATED! DO NOT EDIT! -->

`octavo.pages` reads a directory of markdown files and notebooks into [`Page`](pages.md#page) objects. Each page holds its markdown, frontmatter settings, and HTML output path. For example, `api/01_config.ipynb` becomes a page with destination `api/config.html`.

Create a [`Page`](pages.md#page) directly when you already have markdown, or use [`scan`](pages.md#scan) to read all documents under a directory:

```python
page = Page('api/01_config.md', '# Configuration\n\nProject settings.')
page.dest  # 'api/config.html'

pages = scan('docs')
[(p.rel, p.dest) for p in pages]  # source and output paths
```

Scanning converts notebooks to markdown using their saved outputs; it does not run their cells unless you supply a processor that does so. Markdown `{python}` blocks, however, execute during scanning. Hidden files and directories, whose names begin with `.` or `_`, are skipped, as are paths matching the `exclude` globs that [`source_files`](pages.md#source_files) takes.

This module reads sources and determines destinations; it does not render or write the site. `octavo.site` uses the collected pages to build navigation, resolve cross-page links, and write the output.

## Frontmatter

Frontmatter is a YAML block at the start of a document, enclosed by `---` lines. It supplies settings such as a page title, sort order, and old URLs to redirect.

`frontmatter` returns the parsed settings and the remaining document body. YAML numbers and lists retain their types.

This tutorial declares a numeric sort order and one old URL in `aliases`.

```python
text = """---
title: End-To-End Walkthrough
order: 1
aliases:
  - /tutorial.html
---

Build a package from scratch, starting with a single notebook.
"""
meta,body = frontmatter(text)
meta
```

```output
{'title': 'End-To-End Walkthrough', 'order': 1, 'aliases': ['/tutorial.html']}
```

Without frontmatter, the settings are empty and the document body is retained.

```python
bare,barebody = frontmatter('# Hand written\n\nNo frontmatter here.\n')
test_eq(bare, {})
barebody
```

```output
'# Hand written\n\nNo frontmatter here.\n'
```

## Pages

A page’s output keeps its source directory but drops a leading numeric prefix from the filename. For example, `api/01_config.md` produces `api/config.html`. This lets authors order source files without including those numbers in public URLs.

Pass `nm` to choose a different output name without changing the source filename.

---

### Page

```python
def Page(
    rel:str, # Path of the document relative to the source root
    text:str, # Its Markdown, frontmatter included
    meta:dict=None, # Frontmatter; read from `text`, through `snake_keys`, when not given
    nm:str=None, # Output name without extension; derived from `rel` when not given
):
```

*A source document, its metadata, and its output destination*

---

### page_nm

```python
def page_nm(
    rel
):
```

*Output name of `rel`: its stem without any numeric prefix*

```python
p = Page('tutorials/01_tutorial.md', text)
test_eq(p.dest, 'tutorials/tutorial.html')
test_eq(p.title, 'End-To-End Walkthrough')
p
```

```output
tutorials/01_tutorial.md -> tutorials/tutorial.html
```

Without a frontmatter title, a page takes its title from a leading level-1 heading, as Quarto does. HTML comments before the heading, such as a generated-file notice, are skipped. A page with neither uses its output name. Pass `nm="configuration"` to produce `api/configuration.html` from `api/01_config.md`.

```python
q = Page('explanations/why_nbdev.md', '<!-- Generated from notes.json -->\n# Why nbdev\n\nSome prose.\n')
test_eq(q.title, 'Why nbdev')
cfg = Page('api/01_config.md', 'Settings live here.\n', nm='configuration')
test_eq((cfg.dest, cfg.title), ('api/configuration.html', 'configuration'))
q
```

```output
explanations/why_nbdev.md -> explanations/why_nbdev.html
```

## Scanning a source tree

A site's source files are every file below its root except hidden paths, `octavo.yml`, and paths matching the site's `exclude` globs. A path is hidden when a component begins with `.` or `_`, which covers `.git`, `.ipynb_checkpoints` and `_docs`. Each source file becomes either a page or an asset: [`scan`](pages.md#scan) reads the Markdown documents and notebooks, and [`Site.build`](site.md#site.build) copies the rest, such as images, stylesheets and downloads, without rendering them and at the same relative path.

[`source_files`](pages.md#source_files) lists them. It does not enter hidden or excluded directories.

---

### source_files

```python
def source_files(
    root:str, # Directory of source documents
    exclude:NoneType=None, # Glob patterns, relative to `root`, of paths to leave out
):
```

*Paths relative to `root` of the files a site publishes: not hidden, not `octavo.yml`, and not matching `exclude`*

---

### hidden

```python
def hidden(
    rel
):
```

*Is any part of `rel` a dotfile or underscore-prefixed?*

This source tree holds three Markdown documents, a notebook and an image, plus `octavo.yml` and a `_docs` directory left over from an earlier build, which are not source files.

```python
src = Path(tempfile.mkdtemp())
mkdoc(src/'getting_started.md', {'title':'Getting Started'})
mkdoc(src/'api/01_config.md', {'title':'config', 'order':1})
mkdoc(src/'tutorials/tutorial.md', {'title':'End-To-End Walkthrough', 'aliases':['/tutorial.html']})
nb = dict2nb(cells=[mk_cell('# Notebook page', 'markdown')], metadata={}, nbformat=4, nbformat_minor=5)
write_nb(nb, src/'api/02_notebook.ipynb')
mkdoc(src/'_docs/stale.md', {'title':'Left over from an earlier build'})
(src/'octavo.yml').write_text('title: nbdev\n')
(src/'images').mkdir()
(src/'images/logo.svg').write_text('<svg/>')
files = source_files(src)
test_eq(files, ['api/01_config.md', 'api/02_notebook.ipynb', 'getting_started.md', 'images/logo.svg', 'tutorials/tutorial.md'])
files
```

```output
['api/01_config.md', 'api/02_notebook.ipynb', 'getting_started.md', 'images/logo.svg', 'tutorials/tutorial.md']
```

`exclude` patterns are matched against paths relative to the root. A pattern that matches a directory leaves out everything under it, so `tutorials` drops the walkthrough. In these patterns `*` also matches `/`, so `*.svg` matches SVG files at any depth.

```python
test_eq(source_files(src, ['tutorials', '*.svg']), ['api/01_config.md', 'api/02_notebook.ipynb', 'getting_started.md'])
```

[`scan`](pages.md#scan) reads the Markdown documents and notebooks among the source files. Markdown `{python}` blocks execute during reading. Notebook conversion uses saved content and outputs. A caller can supply `nbproc` to process notebooks first, such as nbdev’s generated API documentation and directives.

---

### scan

```python
def scan(
    root:str, # Directory of source documents
    nbproc:NoneType=None, # Optional callable applied to notebooks before conversion
    exclude:NoneType=None, # Glob patterns of paths to leave out; see `source_files`
):
```

*Every Markdown document and notebook under `root`, as [`Page`](pages.md#page)s*

---

### ipynb2md

```python
def ipynb2md(
    path:str, # Notebook to convert
    nbproc:NoneType=None, # Optional callable that processes the notebook in place
):
```

*Convert a notebook to document Markdown with code-cell publishing directives*

```python
seen = []
def mark_nb(nb): seen.append(len(nb.cells))
pages = scan(src, nbproc=mark_nb)
test_eq(seen, [1])
pages
```

```output
[api/01_config.md -> api/config.html, api/02_notebook.ipynb -> api/notebook.html, getting_started.md -> getting_started.html, tutorials/tutorial.md -> tutorials/tutorial.html]
```

Each scanned page already has the destination and title needed for navigation.

```python
{o.dest: o.title for o in pages}
```

```output
{'api/config.html': 'config',
 'api/notebook.html': 'Notebook page',
 'getting_started.html': 'Getting Started',
 'tutorials/tutorial.html': 'End-To-End Walkthrough'}
```

The same pages supply redirect destinations for their declared aliases.

```python
{a: o.dest for o in pages for a in o.aliases}
```

```output
{'/tutorial.html': 'tutorials/tutorial.html'}
```

Markdown sources can generate content with `{python}` fences. Blocks share a namespace within one document, while ordinary `python` fences remain displayed examples. Scanning keeps the frontmatter and the computed Markdown together in `Page.text`; both output formats use that text without executing it again.

````python
mkdoc(src/'api/01_config.md', {'title':'config', 'order':1}, body=r'''
```{python}
name,score = 'Ada',42
```

```{python}
print(f"| Name | Score |\n| --- | ---: |\n| {name} | {score} |")
```

```python
raise RuntimeError('This is a displayed example')
```

Literal {{name}} remains prose.
''')
computed = next(o for o in scan(src) if o.rel=='api/01_config.md')
test_eq(computed.meta, {'title':'config', 'order':1})
assert '| Ada | 42 |' in computed.text
assert '```{python}' not in computed.text
assert '```python' in computed.text and 'raise RuntimeError' in computed.text
assert '{{name}}' in computed.text
print(computed.text)
````

````output
---
title: config
order: 1
---


| Name | Score |
| --- | ---: |
| Ada | 42 |

```python
raise RuntimeError('This is a displayed example')
```

Literal {{name}} remains prose.
````

## Generated files

A build can include files produced by another tool. For example, nbdev can execute `images/circles.svg.py` and pass the resulting SVG to Octavo as a [`GeneratedFile`](pages.md#generatedfile).

The object records the source path, output path, and text to write. The site builder excludes the source script from copied assets. It rejects generated destinations that collide with pages, copied assets, or other generated files.

---

### GeneratedFile

```python
def GeneratedFile(
    src:str, # Path of the source it was generated from, relative to the source root
    dest:str, # Path to write, relative to the site root
    content:str, # File contents as text
):
```

*A file produced in memory, written verbatim to the output*

This generated file writes `images/circles.svg` and excludes `images/circles.svg.py` from copied assets.

```python
gen = GeneratedFile('images/circles.svg.py', 'images/circles.svg', '<svg/>\n')
test_eq(gen.src, 'images/circles.svg.py')
test_eq(gen.dest, 'images/circles.svg')
gen
```

```output
images/circles.svg.py -> images/circles.svg
```
