Zum Inhalt springen

Parameter und Query

Auftrag

Martina Hostettler möchte Links verschicken können.

Als Mitglied möchte ich einem anderen Mitglied einen Link auf ein Ding oder auf eine gefilterte Liste schicken, damit wir beide dasselbe vor uns haben.

Die Akzeptanzkriterien lauten:

  1. Jedes Ding hat eine Seite unter /items/ und seiner Nummer, etwa /items/6.
  2. Suchtext, Kategorie und Sortierung stehen in der Adresse, /?q=säge&category=Werkzeug zeigt die Sägen aus dem Werkzeug.
  3. Der Zurück-Knopf des Browsers führt vom Ding zurück zur gefilterten Liste.

Lernziele

Nach diesem Kapitel kannst du

  • Routen mit Parametern festlegen,
  • Parameter mit useRoute lesen und als Props übergeben,
  • die Query einer Adresse lesen und schreiben,
  • mit router.push und router.replace im Code navigieren,
  • reagieren, wenn der Router eine Komponente für einen neuen Parameter wiederverwendet.

Ein Doppelpunkt macht aus einem Teil des Pfads einen Parameter. Die Route /items/:id passt auf /items/1 wie auf /items/6, und route.params.id liefert die Nummer aus der Adresse.

App.vue

App.vue
<script setup>
import { RouterLink, RouterView, useRoute } from 'vue-router'
const route = useRoute()
</script>
<template>
<nav>
<RouterLink :to="{ name: 'catalog' }">Katalog</RouterLink>
</nav>
<p>Adresse: {{ route.fullPath }}</p>
<RouterView />
</template>
<style scoped>
nav {
display: flex;
flex-wrap: wrap;
gap: 1rem;
}
</style>

CatalogView.vue

CatalogView.vue
<script setup>
import { RouterLink } from 'vue-router'
import { things } from './things.js'
</script>
<template>
<h1>Katalog</h1>
<ul>
<li v-for="thing in things" :key="thing.id">
<RouterLink :to="{ name: 'item', params: { id: thing.id } }">{{ thing.name }}</RouterLink>
</li>
</ul>
</template>

ItemView.vue

ItemView.vue
<script setup>
import { computed } from 'vue'
import { useRoute } from 'vue-router'
import { things } from './things.js'
const route = useRoute()
// Params always arrive as text, the ids of the things are numbers
const thing = computed(() => things.find((entry) => entry.id === Number(route.params.id)))
</script>
<template>
<p>route.params.id: {{ JSON.stringify(route.params.id) }}</p>
<h1 v-if="thing">{{ thing.name }}</h1>
<p v-else>Dieses Ding gibt es nicht.</p>
</template>

main.js

main.js
import { createApp } from 'vue'
import App from './App.vue'
import router from './router.js'
createApp(App).use(router).mount('#app')

router.js

router.js
import { createMemoryHistory, createRouter } from 'vue-router'
import CatalogView from './CatalogView.vue'
import ItemView from './ItemView.vue'
export default createRouter({
history: createMemoryHistory(),
routes: [
{ path: '/', name: 'catalog', component: CatalogView },
// :id is a param, /items/6 shows the page with the id 6
{ path: '/items/:id', name: 'item', component: ItemView },
],
})

things.js

things.js
// A few things of the catalog
export const things = [
{ id: 1, name: 'Bohrmaschine', deposit: 20 },
{ id: 2, name: 'Zelt für vier Personen', deposit: 50 },
{ id: 5, name: 'Stichsäge', deposit: 20 },
{ id: 6, name: 'Beamer', deposit: 80 },
{ id: 15, name: 'Kreissäge', deposit: 40 },
]

Die Zeile über der Überschrift zeigt den Wert mit Anführungszeichen, denn Parameter kommen immer als Text an. Die Nummern der Dinge sind dagegen Zahlen, darum wandelt Number() den Parameter vor dem Vergleich um. Ein Link zur Seite eines Dings nennt die Route mit ihrem Namen und gibt den Parameter dazu, { name: 'item', params: { id: 6 } }.

Teile einer Adresse Zwei Adressen, in ihre Teile zerlegt. Oben /items/6, davon ist /items/ der feste Teil der Route /items/:id und 6 der Parameter, den route.params.id als Text liefert. Unten /?q=säge&category=Werkzeug&sort=deposit, davon ist / der Pfad, und die Query besteht aus q, category und sort, die route.query.q, route.query.category und route.query.sort liefern. /items/ fester Teil der Route /items/:id 6 Parameter route.params.id = '6' / Pfad ?q=säge Query route.query.q &category=Werkzeug Query route.query.category &sort=deposit Query route.query.sort

Eine Komponente, die useRoute liest, funktioniert nur mit einem Router. Einfacher ist es, wenn die Route ihr die Werte als Props gibt. props: true übergibt die Parameter, wie sie sind, also als Text. Eine Funktion bekommt die Route und gibt die Props zurück, und dabei kann sie den Parameter umwandeln.

App.vue

App.vue
<script setup>
import { RouterLink, RouterView, useRoute } from 'vue-router'
const route = useRoute()
</script>
<template>
<nav>
<RouterLink :to="{ name: 'catalog' }">Katalog</RouterLink>
</nav>
<p>Adresse: {{ route.fullPath }}</p>
<RouterView />
</template>
<style scoped>
nav {
display: flex;
flex-wrap: wrap;
gap: 1rem;
}
</style>

CatalogView.vue

CatalogView.vue
<script setup>
import { RouterLink } from 'vue-router'
import { things } from './things.js'
</script>
<template>
<h1>Katalog</h1>
<ul>
<li v-for="thing in things" :key="thing.id">
<RouterLink :to="{ name: 'item', params: { id: thing.id } }">{{ thing.name }}</RouterLink>
</li>
</ul>
</template>

ItemView.vue

ItemView.vue
<script setup>
import { computed } from 'vue'
import { things } from './things.js'
// The component knows nothing of the router, it only gets a prop
const props = defineProps({ id: { type: Number, required: true } })
const thing = computed(() => things.find((entry) => entry.id === props.id))
</script>
<template>
<p>Prop id: {{ JSON.stringify(id) }}</p>
<h1 v-if="thing">{{ thing.name }}</h1>
<p v-else>Dieses Ding gibt es nicht.</p>
</template>

main.js

main.js
import { createApp } from 'vue'
import App from './App.vue'
import router from './router.js'
createApp(App).use(router).mount('#app')

router.js

router.js
import { createMemoryHistory, createRouter } from 'vue-router'
import CatalogView from './CatalogView.vue'
import ItemView from './ItemView.vue'
export default createRouter({
history: createMemoryHistory(),
routes: [
{ path: '/', name: 'catalog', component: CatalogView },
{
path: '/items/:id',
name: 'item',
component: ItemView,
// The route turns the param into a number and passes it as a prop
props: (route) => ({ id: Number(route.params.id) }),
},
],
})

things.js

things.js
// A few things of the catalog
export const things = [
{ id: 1, name: 'Bohrmaschine', deposit: 20 },
{ id: 2, name: 'Zelt für vier Personen', deposit: 50 },
{ id: 5, name: 'Stichsäge', deposit: 20 },
{ id: 6, name: 'Beamer', deposit: 80 },
{ id: 15, name: 'Kreissäge', deposit: 40 },
]

ItemView bekommt jetzt eine Zahl und weiss nichts mehr vom Router. Ein Test kann die Komponente mit mount(ItemView, { props: { id: 6 } }) prüfen, ohne Adresse und ohne Router. Die Dokumentation rät, die Funktion ohne eigenen Zustand zu schreiben, denn der Router ruft sie nur bei einem Wechsel der Route auf.

Führt ein Link von /items/1 nach /items/2, behält der Router die Komponente und ändert nur den Parameter. Code in <script setup> läuft dann kein zweites Mal.

App.vue

App.vue
<script setup>
import { RouterLink, RouterView, useRoute } from 'vue-router'
const route = useRoute()
</script>
<template>
<nav>
<RouterLink :to="{ name: 'catalog' }">Katalog</RouterLink>
</nav>
<p>Adresse: {{ route.fullPath }}</p>
<RouterView />
</template>
<style scoped>
nav {
display: flex;
flex-wrap: wrap;
gap: 1rem;
}
</style>

CatalogView.vue

CatalogView.vue
<script setup>
import { RouterLink } from 'vue-router'
import { things } from './things.js'
</script>
<template>
<h1>Katalog</h1>
<ul>
<li v-for="thing in things" :key="thing.id">
<RouterLink :to="{ name: 'item', params: { id: thing.id } }">{{ thing.name }}</RouterLink>
</li>
</ul>
</template>

ItemView.vue

ItemView.vue
<script setup>
import { computed, watch } from 'vue'
import { RouterLink, useRoute } from 'vue-router'
import { things } from './things.js'
const route = useRoute()
console.log('ItemView entsteht')
// From /items/1 to /items/2 Vue keeps the component, only the param changes
watch(
() => route.params.id,
(id) => console.log('neue id', id),
)
const index = computed(() => things.findIndex((entry) => entry.id === Number(route.params.id)))
const next = computed(() => things[(index.value + 1) % things.length])
</script>
<template>
<h1 v-if="index > -1">{{ things[index].name }}</h1>
<p v-else>Dieses Ding gibt es nicht.</p>
<RouterLink :to="{ name: 'item', params: { id: next.id } }">Nächstes Ding</RouterLink>
</template>

main.js

main.js
import { createApp } from 'vue'
import App from './App.vue'
import router from './router.js'
createApp(App).use(router).mount('#app')

router.js

router.js
import { createMemoryHistory, createRouter } from 'vue-router'
import CatalogView from './CatalogView.vue'
import ItemView from './ItemView.vue'
export default createRouter({
history: createMemoryHistory(),
routes: [
{ path: '/', name: 'catalog', component: CatalogView },
{ path: '/items/:id', name: 'item', component: ItemView },
],
})

things.js

things.js
// A few things of the catalog
export const things = [
{ id: 1, name: 'Bohrmaschine', deposit: 20 },
{ id: 2, name: 'Zelt für vier Personen', deposit: 50 },
{ id: 5, name: 'Stichsäge', deposit: 20 },
{ id: 6, name: 'Beamer', deposit: 80 },
{ id: 15, name: 'Kreissäge', deposit: 40 },
]

Die Konsole meldet «ItemView entsteht» nur beim ersten Ding. Danach meldet nur noch der Watcher die neue Nummer. Was aus dem Parameter berechnet wird, steht darum in computed, und was beim Wechsel geschehen soll, in einem Watcher. Die Fallbeispiel-App setzt auf diese Weise das Formular zurück, wenn ein anderes Ding erscheint. Die Dokumentation nennt ausserdem onBeforeRouteUpdate, das vor dem Wechsel läuft und ihn auch abbrechen kann.

Was nach dem Fragezeichen steht, ist die Query. Sie passt zu Werten, die eine Seite verändern, ohne eine andere Seite zu sein, etwa zu Suche, Filter und Sortierung. route.query.q liest den Suchtext, und ein Link mit query setzt ihn.

App.vue

App.vue
<script setup>
import { RouterLink, RouterView, useRoute } from 'vue-router'
const route = useRoute()
</script>
<template>
<nav>
<RouterLink :to="{ name: 'catalog' }">Katalog</RouterLink>
</nav>
<p>Adresse: {{ route.fullPath }}</p>
<RouterView />
</template>
<style scoped>
nav {
display: flex;
flex-wrap: wrap;
gap: 1rem;
}
</style>

CatalogView.vue

CatalogView.vue
<script setup>
import { computed } from 'vue'
import { RouterLink, useRoute, useRouter } from 'vue-router'
import { things } from './things.js'
const route = useRoute()
const router = useRouter()
// The search text lives in the address, the field reads it from there and writes it back
const query = computed({
get: () => route.query.q ?? '',
// replace instead of push, so the back button skips the single keystrokes
set: (text) => router.replace({ query: { ...route.query, q: text || undefined } }),
})
const visibleThings = computed(() => {
const text = query.value.trim().toLowerCase()
return things.filter((thing) => thing.name.toLowerCase().includes(text))
})
</script>
<template>
<h1>Katalog</h1>
<label for="search">Suchen</label>
<input id="search" v-model="query" type="search">
<ul>
<li v-for="thing in visibleThings" :key="thing.id">{{ thing.name }}</li>
</ul>
<RouterLink :to="{ name: 'catalog', query: { q: 'säge' } }">Alle Sägen</RouterLink>
</template>

main.js

main.js
import { createApp } from 'vue'
import App from './App.vue'
import router from './router.js'
createApp(App).use(router).mount('#app')

router.js

router.js
import { createMemoryHistory, createRouter } from 'vue-router'
import CatalogView from './CatalogView.vue'
export default createRouter({
history: createMemoryHistory(),
routes: [{ path: '/', name: 'catalog', component: CatalogView }],
})

things.js

things.js
// A few things of the catalog
export const things = [
{ id: 1, name: 'Bohrmaschine', deposit: 20 },
{ id: 2, name: 'Zelt für vier Personen', deposit: 50 },
{ id: 5, name: 'Stichsäge', deposit: 20 },
{ id: 6, name: 'Beamer', deposit: 80 },
{ id: 15, name: 'Kreissäge', deposit: 40 },
]

Das Suchfeld bindet einen berechneten Wert mit get und set. get liest den Text aus der Adresse, set schreibt jede Änderung mit router.replace zurück. replace ersetzt den aktuellen Eintrag der History, sonst legte jeder Tastendruck einen neuen an, und der Zurück-Knopf ginge Buchstabe für Buchstabe zurück. Ein Wert undefined nimmt den Schlüssel ganz aus der Adresse.

In der Fallbeispiel-App wandern Suchtext, Kategorie und Sortierung in die Query. Das Suchfeld schreibt erst nach der Pause von 300 Millisekunden aus Kapitel 5.2 in die Adresse, Kategorie und Sortierung sofort. Den Speicher des Browsers aus Teil 5 brauchen die drei Werte nicht mehr. Ein Neuladen behält die Adresse, der Zurück-Knopf stellt sie wieder her, und ein Link gibt sie weiter.

Manchmal führt keine Verlinkung, sondern Code zur nächsten Seite, etwa nach dem Senden eines Formulars. useRouter liefert den Router, und router.push nimmt dieselben Ziele wie RouterLink.

App.vue

App.vue
<script setup>
import { RouterLink, RouterView, useRoute } from 'vue-router'
const route = useRoute()
</script>
<template>
<nav>
<RouterLink :to="{ name: 'catalog' }">Katalog</RouterLink>
</nav>
<p>Adresse: {{ route.fullPath }}</p>
<RouterView />
</template>
<style scoped>
nav {
display: flex;
flex-wrap: wrap;
gap: 1rem;
}
</style>

CatalogView.vue

CatalogView.vue
<script setup>
import { ref } from 'vue'
import { useRouter } from 'vue-router'
const router = useRouter()
const number = ref(6)
// push adds an entry to the history, the back button leads here again
function open() {
router.push({ name: 'item', params: { id: number.value } })
}
</script>
<template>
<h1>Katalog</h1>
<form @submit.prevent="open">
<label for="number">Nummer des Dings</label>
<input id="number" v-model.number="number" type="number" min="1">
<button type="submit">Öffnen</button>
</form>
</template>

ItemView.vue

ItemView.vue
<script setup>
import { computed } from 'vue'
import { useRouter } from 'vue-router'
import { things } from './things.js'
const props = defineProps({ id: { type: Number, required: true } })
const router = useRouter()
const thing = computed(() => things.find((entry) => entry.id === props.id))
</script>
<template>
<h1 v-if="thing">{{ thing.name }}</h1>
<p v-else>Dieses Ding gibt es nicht.</p>
<!-- back goes one step back in the history, like the button of the browser -->
<button type="button" @click="router.back()">Zurück</button>
</template>

main.js

main.js
import { createApp } from 'vue'
import App from './App.vue'
import router from './router.js'
createApp(App).use(router).mount('#app')

router.js

router.js
import { createMemoryHistory, createRouter } from 'vue-router'
import CatalogView from './CatalogView.vue'
import ItemView from './ItemView.vue'
export default createRouter({
history: createMemoryHistory(),
routes: [
{ path: '/', name: 'catalog', component: CatalogView },
{ path: '/items/:id', name: 'item', component: ItemView, props: (route) => ({ id: Number(route.params.id) }) },
],
})

things.js

things.js
// A few things of the catalog
export const things = [
{ id: 1, name: 'Bohrmaschine', deposit: 20 },
{ id: 2, name: 'Zelt für vier Personen', deposit: 50 },
{ id: 5, name: 'Stichsäge', deposit: 20 },
{ id: 6, name: 'Beamer', deposit: 80 },
{ id: 15, name: 'Kreissäge', deposit: 40 },
]

push legt einen Eintrag in der History an, replace ersetzt den aktuellen, und back geht wie der Knopf des Browsers einen Schritt zurück. Laut Dokumentation ignoriert der Router params, wenn ein Ziel einen path hat, darum steht im Beispiel der Name der Route. push und replace geben ein Promise zurück, das sich erfüllt, wenn die Navigation vorbei ist.

Die Seite eines Dings liegt unter /items/6. Bisher leitete der Entwicklungsserver der Fallbeispiel-App alles unter /items an den kleinen Server weiter, und damit auch diese Seite. Ab diesem Teil holt die App den Katalog unter /api/items. Vite leitet nur noch Adressen unter /api weiter und entfernt das Präfix, alle anderen Adressen gehören der App.

Arbeite mit den Beispielen dieses Kapitels. Lies den Code und schreib auf, was du erwartest, bevor du etwas änderst.

  1. Nimm im Beispiel mit useRoute den Aufruf Number() weg. Was zeigt die Seite für die Bohrmaschine, und warum?
  2. Ersetze im Beispiel mit den Props die Funktion durch props: true. Was meldet Vue in der Konsole?
  3. Klick im Beispiel mit der wiederverwendeten Komponente mehrmals auf «Nächstes Ding». Wie oft erscheint «ItemView entsteht»?
  4. Ersetze im Beispiel mit der Query router.replace durch router.push und ergänze einen Knopf, der router.back() aufruft. Tipp ein Wort und klick mehrmals auf den Knopf. Was passiert mit dem Suchfeld?

Die Aufgaben bauen die Links aus dem Auftrag. Du findest das Projekt im Kursrepo im Ordner exercises/06-routing/params-query/de. Kopiere den Ordner, installiere die Pakete und starte die Tests.

Terminal-Fenster
npm install
npm test

Aufgabe Einstieg

ItemView zeigt ein Ding und erwartet seine Nummer als Prop id vom Typ Number. Ergänze in src/router/index.js die Route /items/:id mit dem Namen item und gib ItemView die Nummer als Zahl. Der Name jedes Dings im Katalog führt mit RouterLink zu seiner Seite.

Lösung
index.js
routes: [
{ path: '/', name: 'catalog', component: CatalogView },
{
path: '/items/:id',
name: 'item',
component: ItemView,
// Params arrive as text, the route turns the id into a number for the prop
props: (route) => ({ id: Number(route.params.id) }),
},
],
})
CatalogView.vue
<ul class="catalog">
<li v-for="item in visibleItems" :key="item.id">
<RouterLink :to="{ name: 'item', params: { id: item.id } }" class="name">{{ item.name }}</RouterLink>
<span class="category">{{ item.category }}</span>
</li>
</ul>

Mit props: true käme die Nummer als Text an. ItemView fände dann kein Ding, weil '6' nicht gleich 6 ist, und Vue warnte vor dem falschen Typ der Prop.

Aufgabe Vertiefung

Die Suche gehört in die Adresse. Das Suchfeld liest den Text aus route.query.q und schreibt jede Änderung mit router.replace als q in die Query. Ein Link wie /?q=zelt zeigt sofort die drei Zelte, und ein leeres Feld nimmt q aus der Adresse.

Lösung
CatalogView.vue
const route = useRoute()
const router = useRouter()
// Search text and sort live in the query, so a link shows the same list.
// replace instead of push, the back button should not step through every keystroke.
const query = computed({
get: () => route.query.q ?? '',
set: (text) => router.replace({ query: { ...route.query, q: text || undefined } }),
})

...route.query übernimmt die übrigen Werte der Query, damit eine spätere Sortierung erhalten bleibt. Das Feld bindet query mit v-model wie ein gewöhnliches Ref, und visibleItems liest query.value wie vorher.

Aufgabe Knobelei

Ergänze eine Auswahl <select id="sort"> mit «Name» und dem Wert '' und «Pfand» mit dem Wert deposit. Sie schreibt sort=deposit in die Query. Ohne Angabe sortiert der Katalog nach dem Namen, mit deposit nach dem Pfand und bei gleichem Pfand nach dem Namen. Die Suche bleibt beim Sortieren erhalten.

Lösung
CatalogView.vue
<script setup>
import { computed } from 'vue'
import { RouterLink, useRoute, useRouter } from 'vue-router'
import items from '../../data/items.json'
const route = useRoute()
const router = useRouter()
// Search text and sort live in the query, so a link shows the same list.
// replace instead of push, the back button should not step through every keystroke.
const query = computed({
get: () => route.query.q ?? '',
set: (text) => router.replace({ query: { ...route.query, q: text || undefined } }),
})
const sort = computed({
get: () => route.query.sort ?? '',
set: (value) => router.replace({ query: { ...route.query, sort: value || undefined } }),
})
const byName = (a, b) => a.name.localeCompare(b.name, 'de-CH')
const byDeposit = (a, b) => a.deposit - b.deposit || byName(a, b)
const visibleItems = computed(() => {
const text = query.value.trim().toLowerCase()
const found = items.filter((item) => item.name.toLowerCase().includes(text))
return found.toSorted(sort.value === 'deposit' ? byDeposit : byName)
})
</script>
<template>
<h1>Katalog</h1>
<div class="filters">
<div class="search">
<label for="search">Suchen</label>
<input id="search" v-model="query" type="search">
</div>
<div class="filter">
<label for="sort">Sortieren nach</label>
<select id="sort" v-model="sort">
<option value="">Name</option>
<option value="deposit">Pfand</option>
</select>
</div>
</div>
<ul class="catalog">
<li v-for="item in visibleItems" :key="item.id">
<RouterLink :to="{ name: 'item', params: { id: item.id } }" class="name">{{ item.name }}</RouterLink>
<span class="category">{{ item.category }}</span>
</li>
</ul>
</template>

toSorted liefert ein sortiertes neues Array und lässt die Daten unverändert. a.deposit - b.deposit || byName(a, b) sortiert nach dem Namen weiter, wenn zwei Dinge dasselbe Pfand haben, denn die Differenz ist dann 0.

FehlerWas hilft
route.params.id === 6 ist nie wahr.Parameter sind Text. Mit Number() umwandeln, am besten in der Funktion props der Route.
router.push({ path: '/items', params: { id: 6 } }) landet auf /items.Mit path ignoriert der Router params. Den Namen der Route verwenden.
Nach einem Wechsel von /items/1 zu /items/2 zeigt die Seite noch das alte Ding.Die Komponente bleibt, nur der Parameter ändert sich. Den Wert mit computed aus dem Parameter berechnen statt ihn einmal zu kopieren.
Jeder Tastendruck im Suchfeld legt einen Eintrag in der History an.router.replace statt router.push verwenden.
Von zwei Änderungen der Query direkt hintereinander geht die erste verloren.route ändert sich erst nach der Navigation. Beide Werte in einem Aufruf ändern oder mit await warten, bis die erste Navigation vorbei ist.
Ein KI-Assistent liest this.$route.params in <script setup>.Dort gibt es kein this. useRoute() liefert die aktuelle Route.

Selbstcheck

  1. Wie kommt der Wert von :id in eine Komponente?
  2. Warum wandelt die Route den Parameter in eine Zahl um?
  3. Wann passt router.replace, wann router.push?
  4. Was passiert mit dem Code in <script setup>, wenn nur der Parameter wechselt?
Antworten
  1. Über route.params.id aus useRoute, oder die Route gibt ihn mit props als Prop weiter.
  2. Parameter kommen als Text an, die Nummern der Dinge sind Zahlen. Ohne Umwandlung fände der Vergleich kein Ding.
  3. replace für Änderungen, die keinen eigenen Schritt zurück verdienen, etwa jeden Buchstaben der Suche. push für einen Wechsel der Seite, zu dem der Zurück-Knopf führen soll.
  4. Er läuft nicht noch einmal, denn der Router behält die Komponente. Nur berechnete Werte und Watcher reagieren auf den neuen Parameter.

Auftrag

Mitglieder können sich jetzt Links schicken. Am Tresen fragt das Team nach einem eigenen Bereich. «Wir brauchen eine Übersicht, was ausgeliehen ist, und einen Ort, an dem wir Rückgaben erfassen.» Für mehrere Seiten unter einer gemeinsamen Adresse gibt es verschachtelte Routen, das nächste Kapitel.

Texte und Abbildungen stehen unter CC BY-SA 4.0, der Code unter der MIT-Lizenz und die Daten des Kurses unter CC0 1.0. Die Schrift der Abbildungen ist Kalam unter der SIL Open Font License 1.1. Die Quellen liegen auf GitHub.