Repository navigation
Releases
1.7.1
-
.node-versionand.tool-versionspin Node 26.13.0, the current LTS
release, and the GitHub Actions workflows run on Node 26, including the deploy
and course-check workflows a course project inherits. The requirement does not
change: Node 24 still works, so a course author on 24 need do nothing. -
The dependencies are upgraded to their newest releases, all within the range
package.jsonallows:@vscode/vsceto 4.1.0, eslint to 10.14.0, globals to
17.14.0, marked to 18.1.3 and turndown to 7.2.5, along with the transitive
tree. marked's two releases fix parser bugs, so unusual markdown (emphasis
delimiter runs, lazy lines in a blockquote, link labels) can render
differently. The pages shipped incourse/render exactly as before, and a
page already on Canvas changes only when a push next renders it. Run
npm installafter the upstream update, as the update script reminds you to.
npm auditstill reports advisories in the Docusaurus build and preview
toolchain and in eslint and@vscode/vsce.npm audit fixstill resolves the
Docusaurus ones by nesting an older Docusaurus, and the only fix it offers for
the other two is a downgrade, so leave it alone.
1.7.0
-
A push puts the alert icons back when they are gone from Canvas. It used to
trust.canvas-sync.jsonalone: an icon recorded under the current theme was
taken to exist, whatever had happened to the file since. So after
npx course reset-canvasfollowed by a push, without
npx course reset-sync-statein between, or after deleting the
/course-iconsfolder by hand in Canvas, every callout the push rendered
pointed at a file that no longer existed and showed a broken image, while
statusreported the course in sync. Each run that renders a page, an
assignment or a discussion now asks Canvas whether every recorded icon is
still there, one request per icon, and uploads any that Canvas answers is
gone, recording its new id. If Canvas cannot be asked, for a lapsed token or a
network fault, the recorded icon is kept and a warning names it, so the push
goes ahead as it did before.Only the pages a push renders pick up the new icons. After
reset-canvasthat
is every page, because the push creates them all again. After a hand-deleted
/course-icons, a page you have not changed since keeps its broken icon until
you next edit and push it. -
A selection export puts a page from a module subfolder at the same heading
level as every other page. A TOC file, a list of paths and--flagged, on the
whole course or with-m, all leave out the subfolder's own heading, but the
page under it kept the extra level it has in a whole-module export. In a PDF,
a Word document and an-f mdfile alike, such a page came out one level
below the other pages, under a heading that was never printed: across several
modules, a third-level heading straight under its module's title. Every page
of a selection is now a second-level heading under its module, or a
first-level heading when the selection covers one module. Whole-module and
whole-course exports are unchanged: they keep the subfolder headings, with the
pages one level below./study-pack-buildalways exports from a TOC file, and its check pairs the
raw and the compact pack page by page on those second-level headings, which is
also where theverbatim:prefixes apply. A verbatim page in a subfolder
escaped both, so the compact pack could condense it without the check
noticing. Regenerate any study pack whose recipe lists a subfolder page with
/study-pack-build. -
export -mandexport-toc -mtake several modules, and take them by number.
-mused to want the one full folder name, so exporting modules 3 to 5 as one
document meant writing a TOC file. It now takes a folder name as before, a
number (-m 03or-m 3), a range (-m 03-05) or a comma list (-m 03,05),
and several values in one go (-m 03 05), assync,pullandstatus
already did. The modules come out in course order, each once. A range skips a
missing number; one that matches nothing, an unknown number or name, and a
number two folders share are errors that name the culprit. Several modules
export as one document titled after the course, with a first-level heading per
module and their subfolder headings intact, to
exports/<course>-03-04-05.pdf; one module exports exactly as before. Because
-mnow takes several values,export -m 03-css course/x.mdreads the path
as a module and stops withNot a module, andexport course/x.md -m 03-css
stops too, where both used to export the module and drop the path without a
word. A module and item paths never combined in one export. The VS Code
extension passes a full folder name and is unaffected. -
sync -m,push -m,pull -mandstatus -mtake modules by number too, in
the formsexport -mtakes: a folder name, a number (-m 03), a range
(-m 03-05), a comma list (-m 03,05) or several values.push -mused to
take one folder name and no more; it now takes several modules, like the other
three. A number finds whatever the full folder name would: a module under
course/, one only the sync state remembers, such as a module deleted here,
and onsync,pullandstatusa Canvas module not yet pulled. A number or
range that matches nothing, and a number two folders share, stop the run with
an error naming it. A full folder name works exactly as before, including the
refusal when nothing answers to it, so a script or the VS Code extension
passing one is unaffected. -
move-module -m,rename-module -m,delete-module -m,new-item -m,
movetomodule-item --to-moduleandbuild-glossary -mtake a module by
number. Each used to want the full folder name;-m 03or-m 3now finds
the module whose folder starts with 03. These commands act on one module, so a
range or list that selects more than one (-m 03-05,-m 03,05) stops with
This command takes one moduleand names the folders it would have meant, and
a number two folders share is refused the same way. An unknown name or number
stops withModule not found, as an unknown name always did;build-glossary
now words it that way too, in place of its own
Module "…" not found in course/ directory. A number makes a typo easier to
commit:delete-module -m 3 -ydeletes whichever module is number 03, where a
mistyped name failed. It names the folder it deleted, as it always has, and it
deletes nothing on Canvas, so a commit brings the module back.new-item -m
and--to-moduleno longer take a path below the module, such as
-m 03-css/02-sub;-sand--to-subsectionname the subfolder. The VS Code
extension passes a full folder name and is unaffected.
1.6.0
-
The tutorial's Setting Up pages say where Coursewright comes from. What
You Need opened with the four installs and never named the project, and
Your Course Project linked it only as "the Coursewright project page", so
a visitor reading coursewright.md had to go back to the home page to find the
GitHub address. What You Need now opens with the address and says you get
Coursewright by copying that project rather than installing it, and Your
Course Project spells the address out in its first step. A course syncing
this repository to Canvas sees the two pages reported as modified locally
once. -
The website footer is set in
course.config.ymlinstead of
docusaurus.config.js. It used to read "Copyright © 2026" and nothing else,
and changing it meant editing a file every upstream update overwrites. A new
footer:block takes a notice of your own in one line of markdown (text,
with{year}for the year of the build), a Creative Commons licence button
linking to its deed (license, an SPDX identifier such asCC-BY-NC-SA-4.0),
and a "Made with Coursewright" link to the project (credit). The buttons
load from the Creative Commons press-kit mirror rather than shipping with the
template, because they are Creative Commons trademarks. The footer is the
website's alone: exports and Canvas do not carry it. The template's
course.config.ymlsetslicense: CC-BY-NC-SA-4.0to match
course/LICENSE.md. An existing course keeps its owncourse.config.yml,
which has nofooter:block, so its next build shows the same copyright line
plus the new credit, and no button. To drop the credit, addfooter:with
credit: false; to show the button, addlicense:with the identifier of the
licence in yourcourse/LICENSE.md.
The Site Footer has the details. -
npx course setupasks for the website footer's notice and licence, after the
export style. The wizard used to leave the newfooter:block alone, so the
one place a course states who wrote it and under which licence had to be found
and edited by hand. It now offers the current notice as the default (answer
nonefor no notice) and lets you pick the licence button from the seven
Creative Commons licences ornone, with a reminder to keep it in step with
course/LICENSE.md. An entry is written only when your answer changes it, and
every comment incourse.config.ymlsurvives. Two new flags answer the
questions from a script:--footer <text>and--license <id|none>, which
stops with an error naming the valid identifiers on anything else. The summary
line after the run names the licence. -
npx course setupwrites the coursetitle,tagline,languageand
themeat the top level ofcourse.config.ymlonly. It used to rewrite the
first line that set the key at any indentation, so a course with an active
labels:block that overridesglossary.titleand sits above the top-level
title:had its glossary heading replaced by the course name, and the course
name left as it was. Nested keys are now left alone.
1.5.4
- The dependencies are upgraded: dotenv to 18,
@vscode/vsceto 4, and
everything else to its newest release within the rangepackage.jsonallows.
.node-versionand.tool-versionspin Node 24.22.1, the newest 24 LTS
release. No command behaves differently. Runnpm installafter the upstream
update, as the update script reminds you to, so the project runs on the new
versions.npm auditstill reports advisories in the Docusaurus build and
preview toolchain; they wait on a Docusaurus release, andnpm audit fix
resolves them by nesting an older Docusaurus, so leave it alone.
1.5.3
-
A pull now downloads a file item's binary onto the path its
file_refnames
when that path is inside a_files/folder undercourse/, and leaves the
file_refas written. It used to download into the_files/beside the
wrapper, under the Canvas file's name, and repoint the wrapper there. So a
wrapper in a subfolder that names the module's own folder as
../_files/pack.md, the way/study-pack-buildwrites one, was pointed at a
second copy in a new_files/inside the subfolder, and afile_refinto the
sharedcourse/_files/was pointed at a module-local copy. The filename is
kept too, so a file renamed in Canvas no longer leaves the old copy behind. A
file_refthat leavescourse/or points outside every_files/folder is
still repointed, because the bytes come from Canvas and could otherwise land
over a page. -
Pushing a file item whose binary you edited keeps its module item. Canvas
gives new bytes uploaded under the same name a new file id and repoints the
module item at it by itself, but a push read the new id as a renamed binary,
deleted the module item and created another at the end of the module. So every
edited binary cost its item the id, and with it any/modules/items/:idlink
and completion requirement, and the nextstatusorsyncread the move as
Canvas reordering the module and offered to renumber the local files to match.
A push now reads the module item after the upload and leaves one Canvas
already repointed alone. A renamed binary still gets a new module item, now at
the position the old one held.
1.5.2
-
A pull no longer rewrites a file item's wrapper that already says what Canvas
says. A pull that brought down the binary regenerated the wrapper beside it as
well, so one whose title andfile_refhad not changed still came back
normalised:title: 'Syllabus'lost its quotes, a file without a final
newline gained one, keys were reordered and any body was dropped. The wrapper
is now rewritten only when its title, its type or the path itsfile_ref
resolves to would read differently. Afile_refinto the shared
course/_files/folder is still repointed at the module-local copy, as
before. -
Publishing or unpublishing a module in Canvas no longer makes the file items
in it read as changed on Canvas. Canvas locks a file when its module is
unpublished and unlocks it on publish, and each lock moves the file's
updated_at, which a file item's fingerprint hashed. So after one publish
click the next pull or sync downloaded every binary in the module again and,
before the entry above, rewrote its wrapper as well. A file item is now
fingerprinted by the file'smodified_at, which moves only when the file's
content is replaced, and thenewestconflict tiebreak reads the same
timestamp, so a publish click no longer makes Canvas look newer than a binary
replaced here.The first pull, push or sync after updating re-records the fingerprint of
every file item in.canvas-sync.jsonand touches nothing else, so expect a
diff in that file worth committing. A file item whose module was published or
unpublished since the last sync reads as changed on Canvas that one time, as
it would have without the update: the publish moved theupdated_atthe old
fingerprint was taken from.
1.5.1
-
Tables in PDF and DOCX exports size their columns to their content. A pipe
table with a row longer than 72 characters took its column widths from the
dashes in its separator row, which Prettier pads to the widest cell, markup
and link URLs included: a column of numbers beside a column of links came out
2% wide, and inline code ran out of its cell.filter.luanow resets the
widths, so Typst sizes the columns and Word autofits the table. -
A table column without explicit alignment is left-aligned in the PDF. It
inherited the centring of the block pandoc wraps every table in; an explicit
:---:or---:still wins. -
The Thomas More style no longer justifies text inside table cells. Its body
text is justified, and in a narrow cell that stretched the spaces and
hyphenated words, down to a two-word header split as "Metho-de".
1.5.0
-
A heading whose text opens with inline code,
## `void`for instance,
kept its level in every export, so a page's own sections sat at page level in
a combined PDF, DOCX or markdown export. The mask that hides code from the
heading shift blanked the code span along with the rest, and the shift then
saw no text after the hashes. The mask now decides only whether the hashes are
real; the text is read from the line itself. -
A new skill,
/study-pack-build, owns the study packs: one recipe per pack in
sources/study-packs/, the raw export kept inraw/next to it, and the
compact pack the course ships condensed from that export per the skill's
rulebook. A helper script checks that the compact keeps every heading, every
code block and every verbatim section, and lists the sections a re-run has to
re-condense. Packs were raw exports before, 125 to 190 KB each in a course of
twenty lessons: heavy for a chatbot's context and for a student's free tier.
.prettierignorenow covers the recipes and the raw exports. -
/ai-tutor-buildno longer builds the study packs itself: it proposes them
and hands the approved proposal to/study-pack-build, which skips its own
approval gate when handed one. The AI module's intro page now says a pack is a
condensed snapshot of the course.
1.4.0
-
PDF exports no longer split what reads as one thing. An alert, a code block or
a table up to half the page's text area, and a list up to a quarter, moves
whole to the next page instead of breaking; a taller one still breaks, with
Typst's own guard against a lone first or last line. An alert that does break
keeps its title with the body; the generic style's H2 rule and both styles'
H5/H6 labels are sticky like every other heading; andfilter.luakeeps a
paragraph that ends in a colon or is at most three words long ("Voorbeeld:",
"Bijv.") with the code, list, table or alert it introduces. A table taller
than the page used to run off it, because pandoc wraps every table in a figure
and Typst does not break figures: it now breaks with its header row repeated.
Thekeep-togetherhelper intemplate.typtakes aratio, so a forked
template can tune the threshold. The price is a few more pages, since a block
that moves whole leaves a gap above the break. -
Heading numbering is off by default, in both export formats and both shipped
styles;export.number_headings: trueincourse.config.ymlturns it on.
Every module or page title that carried a number of its own came out doubled
("1. 1. Je eerste website", "5. 5a. CSS"), becausetemplate.typnumbered
headings in Typst andreference.docxnumbered Heading 1-4 natively, and a
--flaggedexport numbered items "1.0.1." where it drops a subfolder heading.
With the key on, pandoc's--number-sectionsnumbers both formats the same
way, "1", "1.1". The native numbering is gone from both shipped
reference.docxfiles; a forked style keeps its own copy and should drop the
numPrfrom its heading styles too, or the DOCX doubles again with the key
on.--var section-numbering=1.1.still numbers a single PDF run with a Typst
pattern of your choice. -
The style sample rendered its alerts, link card, attachment and page break as
literal::: {...}text: an earlier Prettier pass had joined every fenced div
onto one line, which pandoc does not read as a div. The fences are back on
their own lines andexport-styles/sample.mdis in.prettierignore, since
Prettier's markdown parser has no notion of a fenced div. -
.github/workflows/release.ymlpublishes the GitHub release when av*tag
is pushed, titled with the version and carrying that version'sCHANGELOG.md
section as the notes. Step 5 of the release procedure was the only step with
no command attached, and both 1.2.0 and 1.3.0 were tagged without a release to
show for it, so pushing the tag is now the whole of it. It is gated to this
repository the waytest.ymlis, so a course project that tags its own
material hears nothing from it.scripts/changelog-section.jscuts the
section out and fails loudly when the changelog has none for the version, and
npm testnow checks that the version inpackage.jsonhas a section at all,
which catches a skipped step 1 before the tag is cut rather than after it is
public.
1.3.0
-
The theme comment in the shipped
course.config.ymlpointed at
docs/customization.md; the guide isdocs/customisation.md. A course made
from an earlier release keeps its own copy of the file, so fix the path there
by hand if it bothers you. -
Course: Check in the command palette runs
npx course checkin the shared
terminal, the way Course: Validate does. Palette only, like Course:
Build Glossary: no entry in the Course Manager tree or its menus.
check-linkshas no entry of its own, since Check runs it. -
npx course checkruns every pre-ship check in one go and exits non-zero
naming the ones that failed:validate;build-glossary --checkwhen the
course keeps a glossary at the default path, skipped and said so when it does
not, since the glossary command fails without one;check-links; and every
entry ofchecks.extraincourse.config.yml. Each runs as its own process,
so one failure does not stop the rest.npm run hooks:installinstalls it as
a git pre-push hook, and.github/workflows/course-checks.ymlruns it in
place ofvalidatealone, so a course's CI now covers glossary freshness, the
plain link check and the course's own extras as well. -
npx course check-linksreports every relative markdown or image link whose
target does not exist on disk, under the folderschecks.links.rootsnames
(coursealone by default) or a repeated--root.validateresolves the
links between the items it scans and nothing else, socourse/index.md,
course/LICENSE.md, a folder nested deeper than a module takes and any
markdown kept outsidecourse/went unchecked; this is the plain filesystem
check beside it. Code blocks and inline code are ignored, external URLs and
site-absolute paths are left alone, and the run lists every broken link and
exits non-zero when there is one. -
course.config.ymltakes achecks:section for the pre-ship checks.
links.rootslists the folderscheck-linksscans for broken relative links,
coursealone unless a course says otherwise;extralists the subcommands
checkruns after its built-in list, so a course that registered a command of
its own (a games build, say) has it gated by the same command and the same
pre-push hook without patching the tooling after every update. A value of the
wrong shape warns and falls back, entry by entry where it can, like every
other key in the file.