Port Igniter
Get Started

Course Files

The plain-file course format behind every course: export and import zips, write courses by hand or with an AI assistant, and validate them with courses:check.

Product Support

Every PortIgniter LMS course is a folder of plain JSON files, with lesson text in Markdown. The builder reads and writes these files, and so can you. This section documents the format, so you can:

  • Move courses between sites by exporting and importing zips.
  • Keep courses in Git and review changes like code.
  • Write or generate a course outside the builder, by hand, with a script, or with an AI assistant, and import it as a draft.

The format is strict on purpose. Every file is checked against a schema, unknown fields are rejected, and nothing is guessed or repaired. If a file follows these pages, it imports and publishes.

Export and import

Export a course from the builder toolbar, or from its card under Admin > Courses. You get a zip of the course’s draft with all of its files and assets. Learners, enrollments, and progress are never included.

Import a zip under Admin > Courses > Import a course. An import:

  • Always creates a new, unpublished draft. It never overwrites an existing course, and it never publishes anything. Review it in the builder, then click Publish.
  • Never collides. If the course ID is already taken, the import becomes <id>-2, <id>-3, and so on, and " (imported)" is added to its title. Import the same zip twice, and you get two independent copies.
  • Is all or nothing. Every file is checked before anything is written. A failed import leaves nothing behind.

An import only needs to be structurally valid, so you can import an unfinished course and complete it in the builder. Publishing applies the full checks.

Imports are treated as untrusted. Paths, file types, file sizes, and the number of files are all checked, and assets get the same content checks as uploads.

Archive layout

manifest.json                       recommended
course.json                         required
items/<lesson-id>.lesson.json       one per lesson
quizzes/<quiz-id>.quiz.json         one per quiz
assignments/<id>.assignment.json    one per assignment
banks/<bank-id>.bank.json           question banks
assets/<file>                       images, video, audio, captions, documents
  • Put course.json at the top of the zip, not inside a wrapping folder.
  • Name every file after its ID. The lesson what-is-git lives in items/what-is-git.lesson.json, and the id inside the file must match.
  • Use UTF-8 without a byte-order mark, and valid JSON with no comments or trailing commas.
  • Every item listed in course.json must have its file. Anything unrecognized in the zip is ignored with a warning.
  • Limits: 5,000 files, 1 GB unpacked, and 5 MB per JSON file.

manifest.json identifies the format:

{ "format": "portigniter-course", "format_version": 1, "course_id": "git-first-steps", "title": "Git First Steps" }

To build the zip, run this from inside the course folder, so the paths have no prefix:

cd my-course && zip -r ../my-course.zip . -x '.*' -x '__MACOSX/*'

Identifiers

IDs are permanent. Progress, unlock rules, and quiz history all point at them, so once learners have used an ID, never rename it. Titles can change freely.

Kind Pattern Used for
ID Lowercase letters, digits, and dashes, starting with a letter or digit, up to 63 characters Course, module, item, bank
Local ID Lowercase letters, digits, dashes, and underscores, up to 40 characters Block, question, option, and the parts of a question
Blank ID Lowercase letters, digits, and underscores, up to 20 characters Blanks in a fill-in-the-blanks question
Objective ID Letters, digits, and dashes, up to 20 characters Learning objectives, such as o1 or LO-2

IDs must be unique within their scope:

  • Module IDs are unique in the course.
  • Item IDs, covering lessons, quizzes, and assignments together, are unique across the whole course.
  • Block IDs are unique within a lesson, and question IDs within a bank.
  • Option and part IDs are unique within their question.

Check a course before importing

The courses:check command validates a zip or folder exactly as Publish would, without importing anything. It lists every problem, prints a summary, and exits with an error code on failure, so it fits neatly into a script or CI job.

docker compose cp my-course.zip php:/tmp/my-course.zip
docker compose exec php php spark courses:check /tmp/my-course.zip

Add --json for machine-readable output. Always give an absolute path.

Writing courses with an AI assistant

The format was designed to be handed to an AI assistant. Give the assistant the pages in this section, or the full authoring specification included with the source code, along with your topic and source material, and ask it to produce a course zip. Then run courses:check on the result, import it, and review it in the builder before publishing.

The source code also includes:

  • The authoring specification, docs/COURSE_AUTHORING_SPEC.md, written as a contract for an assistant to follow.
  • A complete example course, docs/course-authoring/example/, that uses every block type and all ten question types. When in doubt, copy its shapes.
  • The JSON Schemas, in src/schemas/, which are the final word on every field.

A few tips from building courses this way:

  • Have the assistant write learning objectives and an outline first, and approve them before it writes lessons.
  • Build one module at a time, and run courses:check after each.
  • Never let it invent binary files. If it can’t produce a real image or video, it should leave the block out and tell you what’s missing.
  • Check every command and output in a lab yourself. Assistants are confident even when they’re wrong.

In this section

Top