---
title: "Academy Plugin"
description: "Course management, with a course → module → lesson hierarchy."
type: "docs"
category: "doc"
tags: []
authors: [Anonymous]
date: "2026-08-21"
last_update: "2026-08-21"
time_minutes: 3
draft: false
unlisted: false
url: "https://www.derafu.dev/docs/ui/content/academy"
---

# Academy plugin

Manages online courses with a three-level hierarchy: **course → module → lesson**. Each level is a content item on its own (with its own title, description, tags, etc.), built from the filesystem structure — see [Content hierarchy](./introduction#content-content-hierarchy).

```text
resources/content/academy/
  getting-started.md                       # Course.
  getting-started/
    introduction.md                        # Module.
    introduction/
      what-is-this.md                      # Lesson.
      how-it-works.md                      # Lesson.
```

## Configuration (`services.yaml`)

Enabled under `derafu.content.config.plugins.academy`:

```yaml
parameters:
    derafu.content.config:
        plugins:
            academy:
              path: 'resources/content/academy'
              academyTitle: 'Academy'
              academyDescription: 'Do you want to learn about a topic? Start a course with us!'
```

| Option | Type | Default | Description |
|---|---|---|---|
| `path` | string | `resources/content/academy` | Directory scanned for course content, relative to the website root. |
| `academyTitle` | string | `Academy` | Title used for the academy's own SEO metadata. |
| `academyDescription` | string | `Do you want to learn about a topic? Start a course with us!` | Description used for the academy's own SEO metadata. |
| `include` | array of glob patterns | `['**.{markdown,md}']` | Files considered content, relative to `path`. |
| `exclude` | array of glob patterns | `[]` | Files excluded even if matched by `include`. |
| `showReadingTime` | bool | `true` | Whether to show the estimated reading time. |
| `showLastUpdateAuthor` | bool | `true` | Whether to show who last updated the lesson. |
| `showLastUpdateTime` | bool | `true` | Whether to show when the lesson was last updated. |
| `tags` | array of strings, or string | `[]` | Predefined tags for the academy. |
| `onInlineTags` | `ignore`\|`log`\|`warn`\|`throw` | `warn` | What to do when a lesson uses a tag that isn't in the predefined `tags` list. |

## Content frontmatter

Courses and modules use exactly the [generic content frontmatter](./frontmatter), nothing added.

Lessons add one field:

| Field | Type | Default | Description |
|---|---|---|---|
| `test` | string | none | A reference to a JSON quiz attachment: either `?attachment=<filename>`, or a literal path ending in `/_attachments/<filename>` (see [Storage](./storage)) — both resolve to the same local attachment. Parsed into a structured test, shown as an interactive quiz on the lesson's own page, and rendered in full in the [PDF and Markdown exports](./exports). Also switches the lesson's sidebar icon. A `test` value that resolves to neither form is passed through as-is to the quiz widget instead, unparsed. |

`time` on a course or module is not read from its own frontmatter — it is always the sum of its lessons' `time` (explicit or estimated).

## Quiz JSON format

```json
{
    "title": "Introduction quiz",
    "description": "Check what you remember from this lesson.",
    "questions": [
        {
            "type": "multiple_choice",
            "text": "Which of these is correct?",
            "options": [
                { "text": "Option A", "is_correct": true },
                { "text": "Option B", "is_correct": false }
            ],
            "allow_multiple": false,
            "explanation": "Option A is correct because..."
        },
        {
            "type": "true_false",
            "text": "This statement is true.",
            "answer": true,
            "explanation": "..."
        }
    ]
}
```

| Field | Type | Description |
|---|---|---|
| `title` | string | Title of the test. |
| `description` | string | Optional description. |
| `questions[].type` | `multiple_choice`\|`true_false` | Question type. A `true_false` question needs no `options` — its two options ("True"/"False") are generated from `answer`. |
| `questions[].options[].is_correct` | bool | Whether this option is a correct answer. More than one can be `true` when `allow_multiple` is `true`. |
| `questions[].explanation` | string | Shown in the PDF/Markdown exports' answer key, and in the lesson's own interactive quiz when an answer is marked incorrect. |



---
Last updated on 21/08/2026

