diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index 6e62cd43e7ab02e65aa4842396c554d629fc0aae..483c7fb62c21be3feb9f333a0b559d4d4fffbc65 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -6,9 +6,22 @@ All notable changes to this project will be documented in this file.
 The format is based on `Keep a Changelog`_,
 and this project adheres to `Semantic Versioning`_.
 
+Breaking changes
+----------------
+
+Removed
+~~~~~~~
+
+* Remove legacy menu entries.
+
 Unreleased
 ----------
 
+Added
+~~~~~
+
+* Add SPA support.
+
 Changed
 ~~~~~~~
 
diff --git a/aleksis/apps/alsijil/frontend/index.js b/aleksis/apps/alsijil/frontend/index.js
new file mode 100644
index 0000000000000000000000000000000000000000..14bb71e6f8c2e7078c509ee8bf8d03a45ae75802
--- /dev/null
+++ b/aleksis/apps/alsijil/frontend/index.js
@@ -0,0 +1,393 @@
+import { notLoggedInValidator, hasPersonValidator } from "aleksis.core/routeValidators";
+
+export default
+  {
+    meta: {
+      inMenu: true,
+      titleKey: "alsijil.menu_title",
+      icon: "mdi-account-group-outline",
+      validators: [
+        hasPersonValidator
+      ]
+    },
+    props: {
+      byTheGreatnessOfTheAlmightyAleksolotlISwearIAmWorthyOfUsingTheLegacyBaseTemplate: true,
+    },
+    children: [
+      {
+        path: "lesson",
+        component: () => import("aleksis.core/components/LegacyBaseTemplate.vue"),
+        name: "alsijil.lessonPeriod",
+        meta: {
+          inMenu: true,
+          titleKey: "alsijil.lesson.menu_title",
+          icon: "mdi-alarm",
+          permission: "alsijil.view_lesson_menu_rule",
+        },
+        props: {
+          byTheGreatnessOfTheAlmightyAleksolotlISwearIAmWorthyOfUsingTheLegacyBaseTemplate: true,
+        },
+      },
+      {
+        path: "lesson/:year(\\d+)/:week(\\d+)/:id_(\\d+)",
+        component: () => import("aleksis.core/components/LegacyBaseTemplate.vue"),
+        name: "alsijil.lessonPeriodByCWAndID",
+        props: {
+          byTheGreatnessOfTheAlmightyAleksolotlISwearIAmWorthyOfUsingTheLegacyBaseTemplate: true,
+        },
+      },
+      {
+        path: "extra_lesson/:id_(\\d+)/",
+        component: () => import("aleksis.core/components/LegacyBaseTemplate.vue"),
+        name: "alsijil.extraLessonByID",
+        props: {
+          byTheGreatnessOfTheAlmightyAleksolotlISwearIAmWorthyOfUsingTheLegacyBaseTemplate: true,
+        },
+      },
+      {
+        path: "event/:id_(\\d+)/",
+        component: () => import("aleksis.core/components/LegacyBaseTemplate.vue"),
+        name: "alsijil.eventByID",
+        props: {
+          byTheGreatnessOfTheAlmightyAleksolotlISwearIAmWorthyOfUsingTheLegacyBaseTemplate: true,
+        },
+      },
+      {
+        path: "week/",
+        component: () => import("aleksis.core/components/LegacyBaseTemplate.vue"),
+        name: "alsijil.weekView",
+        meta: {
+          inMenu: true,
+          titleKey: "alsijil.week.menu_title",
+          icon: "mdi-view-week-outline",
+          permission: "alsijil.view_week_menu_rule",
+        },
+        props: {
+          byTheGreatnessOfTheAlmightyAleksolotlISwearIAmWorthyOfUsingTheLegacyBaseTemplate: true,
+        },
+      },
+      {
+        path: "week/:year(\\d+)/:week(\\d+)/",
+        component: () => import("aleksis.core/components/LegacyBaseTemplate.vue"),
+        name: "alsijil.weekViewByWeek",
+        props: {
+          byTheGreatnessOfTheAlmightyAleksolotlISwearIAmWorthyOfUsingTheLegacyBaseTemplate: true,
+        },
+      },
+      {
+        path: "week/year/cw/",
+        component: () => import("aleksis.core/components/LegacyBaseTemplate.vue"),
+        name: "alsijil.weekViewPlaceholders",
+        props: {
+          byTheGreatnessOfTheAlmightyAleksolotlISwearIAmWorthyOfUsingTheLegacyBaseTemplate: true,
+        },
+      },
+      {
+        path: "week/:type_/:id_(\\d+)/",
+        component: () => import("aleksis.core/components/LegacyBaseTemplate.vue"),
+        name: "alsijil.weekViewByTypeAndID",
+        props: {
+          byTheGreatnessOfTheAlmightyAleksolotlISwearIAmWorthyOfUsingTheLegacyBaseTemplate: true,
+        },
+      },
+      {
+        path: "week/year/cw/:type_/:id_(\\d+)/",
+        component: () => import("aleksis.core/components/LegacyBaseTemplate.vue"),
+        name: "alsijil.weekViewPlaceholdersByTypeAndID",
+        props: {
+          byTheGreatnessOfTheAlmightyAleksolotlISwearIAmWorthyOfUsingTheLegacyBaseTemplate: true,
+        },
+      },
+      {
+        path: "week/:year(\\d+)/:week(\\d+)/:type_/:id_(\\d+)/",
+        component: () => import("aleksis.core/components/LegacyBaseTemplate.vue"),
+        name: "alsijil.weekViewByWeekTypeAndID",
+        props: {
+          byTheGreatnessOfTheAlmightyAleksolotlISwearIAmWorthyOfUsingTheLegacyBaseTemplate: true,
+        },
+      },
+      {
+        path: "print/group/:id_(\\d+)",
+        component: () => import("aleksis.core/components/LegacyBaseTemplate.vue"),
+        name: "alsijil.fullRegisterGroup",
+        props: {
+          byTheGreatnessOfTheAlmightyAleksolotlISwearIAmWorthyOfUsingTheLegacyBaseTemplate: true,
+        },
+      },
+      {
+        path: "groups/",
+        component: () => import("aleksis.core/components/LegacyBaseTemplate.vue"),
+        name: "alsijil.myGroups",
+        meta: {
+          inMenu: true,
+          titleKey: "alsijil.groups.menu_title",
+          icon: "mdi-account-multiple-outline",
+          permission: "alsijil.view_my_groups_rule",
+        },
+        props: {
+          byTheGreatnessOfTheAlmightyAleksolotlISwearIAmWorthyOfUsingTheLegacyBaseTemplate: true,
+        },
+      },
+      {
+        path: "groups/:pk(\\d+)/",
+        component: () => import("aleksis.core/components/LegacyBaseTemplate.vue"),
+        name: "alsijil.studentsList",
+        props: {
+          byTheGreatnessOfTheAlmightyAleksolotlISwearIAmWorthyOfUsingTheLegacyBaseTemplate: true,
+        },
+      },
+      {
+        path: "persons/",
+        component: () => import("aleksis.core/components/LegacyBaseTemplate.vue"),
+        name: "alsijil.myStudents",
+        meta: {
+          inMenu: true,
+          titleKey: "alsijil.persons.menu_title",
+          icon: "mdi-account-school-outline",
+          permission: "alsijil.view_my_students_rule",
+        },
+        props: {
+          byTheGreatnessOfTheAlmightyAleksolotlISwearIAmWorthyOfUsingTheLegacyBaseTemplate: true,
+        },
+      },
+      {
+        path: "persons/:id_(\\d+)/",
+        component: () => import("aleksis.core/components/LegacyBaseTemplate.vue"),
+        name: "alsijil.overviewPerson",
+        props: {
+          byTheGreatnessOfTheAlmightyAleksolotlISwearIAmWorthyOfUsingTheLegacyBaseTemplate: true,
+        },
+      },
+      {
+        path: "me/",
+        component: () => import("aleksis.core/components/LegacyBaseTemplate.vue"),
+        name: "alsijil.overviewMe",
+        meta: {
+          inMenu: true,
+          titleKey: "alsijil.my_overview.menu_title",
+          icon: "mdi-chart-box-outline",
+          permission: "alsijil.view_person_overview_menu_rule",
+        },
+        props: {
+          byTheGreatnessOfTheAlmightyAleksolotlISwearIAmWorthyOfUsingTheLegacyBaseTemplate: true,
+        },
+      },
+      {
+        path: "notes/:pk(\\d+)/delete/",
+        component: () => import("aleksis.core/components/LegacyBaseTemplate.vue"),
+        name: "alsijil.deletePersonalNote",
+        props: {
+          byTheGreatnessOfTheAlmightyAleksolotlISwearIAmWorthyOfUsingTheLegacyBaseTemplate: true,
+        },
+      },
+      {
+        path: "absence/new/:id_(\\d+)/",
+        component: () => import("aleksis.core/components/LegacyBaseTemplate.vue"),
+        name: "alsijil.registerAbsenceWithID",
+        props: {
+          byTheGreatnessOfTheAlmightyAleksolotlISwearIAmWorthyOfUsingTheLegacyBaseTemplate: true,
+        },
+      },
+      {
+        path: "absence/new/",
+        component: () => import("aleksis.core/components/LegacyBaseTemplate.vue"),
+        name: "alsijil.registerAbsence",
+        meta: {
+          inMenu: true,
+          titleKey: "alsijil.absence.menu_title",
+          icon: "mdi-message-alert-outline",
+          permission: "alsijil.view_register_absence_rule",
+        },
+        props: {
+          byTheGreatnessOfTheAlmightyAleksolotlISwearIAmWorthyOfUsingTheLegacyBaseTemplate: true,
+        },
+      },
+      {
+        path: "extra_marks/",
+        component: () => import("aleksis.core/components/LegacyBaseTemplate.vue"),
+        name: "alsijil.extraMarks",
+        meta: {
+          inMenu: true,
+          titleKey: "alsijil.extra_marks.menu_title",
+          icon: "mdi-label-variant-outline",
+          permission: "alsijil.view_extramarks_rule",
+        },
+        props: {
+          byTheGreatnessOfTheAlmightyAleksolotlISwearIAmWorthyOfUsingTheLegacyBaseTemplate: true,
+        },
+      },
+      {
+        path: "extra_marks/create/",
+        component: () => import("aleksis.core/components/LegacyBaseTemplate.vue"),
+        name: "alsijil.createExtraMark",
+        props: {
+          byTheGreatnessOfTheAlmightyAleksolotlISwearIAmWorthyOfUsingTheLegacyBaseTemplate: true,
+        },
+      },
+      {
+        path: "extra_marks/:pk(\\d+)/edit/",
+        component: () => import("aleksis.core/components/LegacyBaseTemplate.vue"),
+        name: "alsijil.editExtraMark",
+        props: {
+          byTheGreatnessOfTheAlmightyAleksolotlISwearIAmWorthyOfUsingTheLegacyBaseTemplate: true,
+        },
+      },
+      {
+        path: "extra_marks/:pk(\\d+)/delete/",
+        component: () => import("aleksis.core/components/LegacyBaseTemplate.vue"),
+        name: "alsijil.deleteExtraMark",
+        props: {
+          byTheGreatnessOfTheAlmightyAleksolotlISwearIAmWorthyOfUsingTheLegacyBaseTemplate: true,
+        },
+      },
+      {
+        path: "excuse_types/",
+        component: () => import("aleksis.core/components/LegacyBaseTemplate.vue"),
+        name: "alsijil.excuseTypes",
+        meta: {
+          inMenu: true,
+          titleKey: "alsijil.excuse_types.menu_title",
+          icon: "mdi-label-outline",
+          permission: "alsijil.view_excusetypes_rule",
+        },
+        props: {
+          byTheGreatnessOfTheAlmightyAleksolotlISwearIAmWorthyOfUsingTheLegacyBaseTemplate: true,
+        },
+      },
+      {
+        path: "excuse_types/create/",
+        component: () => import("aleksis.core/components/LegacyBaseTemplate.vue"),
+        name: "alsijil.createExcuseType",
+        props: {
+          byTheGreatnessOfTheAlmightyAleksolotlISwearIAmWorthyOfUsingTheLegacyBaseTemplate: true,
+        },
+      },
+      {
+        path: "excuse_types/:pk(\\d+)/edit/",
+        component: () => import("aleksis.core/components/LegacyBaseTemplate.vue"),
+        name: "alsijil.editExcuseType",
+        props: {
+          byTheGreatnessOfTheAlmightyAleksolotlISwearIAmWorthyOfUsingTheLegacyBaseTemplate: true,
+        },
+      },
+      {
+        path: "excuse_types/:pk(\\d+)/delete/",
+        component: () => import("aleksis.core/components/LegacyBaseTemplate.vue"),
+        name: "alsijil.deleteExcuseType",
+        props: {
+          byTheGreatnessOfTheAlmightyAleksolotlISwearIAmWorthyOfUsingTheLegacyBaseTemplate: true,
+        },
+      },
+      {
+        path: "group_roles/",
+        component: () => import("aleksis.core/components/LegacyBaseTemplate.vue"),
+        name: "alsijil.groupRoles",
+        meta: {
+          inMenu: true,
+          titleKey: "alsijil.group_roles.menu_title_manage",
+          icon: "mdi-clipboard-plus-outline",
+          permission: "alsijil.view_grouproles_rule",
+        },
+        props: {
+          byTheGreatnessOfTheAlmightyAleksolotlISwearIAmWorthyOfUsingTheLegacyBaseTemplate: true,
+        },
+      },
+      {
+        path: "group_roles/create/",
+        component: () => import("aleksis.core/components/LegacyBaseTemplate.vue"),
+        name: "alsijil.createGroupRole",
+        props: {
+          byTheGreatnessOfTheAlmightyAleksolotlISwearIAmWorthyOfUsingTheLegacyBaseTemplate: true,
+        },
+      },
+      {
+        path: "group_roles/:pk(\\d+)/edit/",
+        component: () => import("aleksis.core/components/LegacyBaseTemplate.vue"),
+        name: "alsijil.editGroupRole",
+        props: {
+          byTheGreatnessOfTheAlmightyAleksolotlISwearIAmWorthyOfUsingTheLegacyBaseTemplate: true,
+        },
+      },
+      {
+        path: "group_roles/:pk(\\d+)/delete/",
+        component: () => import("aleksis.core/components/LegacyBaseTemplate.vue"),
+        name: "alsijil.deleteGroupRole",
+        props: {
+          byTheGreatnessOfTheAlmightyAleksolotlISwearIAmWorthyOfUsingTheLegacyBaseTemplate: true,
+        },
+      },
+      {
+        path: "groups/:pk(\\d+)/group_roles/",
+        component: () => import("aleksis.core/components/LegacyBaseTemplate.vue"),
+        name: "alsijil.assignedGroupRoles",
+        props: {
+          byTheGreatnessOfTheAlmightyAleksolotlISwearIAmWorthyOfUsingTheLegacyBaseTemplate: true,
+        },
+      },
+      {
+        path: "groups/:pk(\\d+)/group_roles/assign/",
+        component: () => import("aleksis.core/components/LegacyBaseTemplate.vue"),
+        name: "alsijil.assignGroupRole",
+        props: {
+          byTheGreatnessOfTheAlmightyAleksolotlISwearIAmWorthyOfUsingTheLegacyBaseTemplate: true,
+        },
+      },
+      {
+        path: "groups/:pk(\\d+)/group_roles/:role_pk(\\d+)/assign/",
+        component: () => import("aleksis.core/components/LegacyBaseTemplate.vue"),
+        name: "alsijil.assignGroupRoleByRolePK",
+        props: {
+          byTheGreatnessOfTheAlmightyAleksolotlISwearIAmWorthyOfUsingTheLegacyBaseTemplate: true,
+        },
+      },
+      {
+        path: "group_roles/assignments/:pk(\\d+)/edit/",
+        component: () => import("aleksis.core/components/LegacyBaseTemplate.vue"),
+        name: "alsijil.editGroupRoleAssignment",
+        props: {
+          byTheGreatnessOfTheAlmightyAleksolotlISwearIAmWorthyOfUsingTheLegacyBaseTemplate: true,
+        },
+      },
+      {
+        path: "group_roles/assignments/:pk(\\d+)/stop/",
+        component: () => import("aleksis.core/components/LegacyBaseTemplate.vue"),
+        name: "alsijil.stopGroupRoleAssignment",
+        props: {
+          byTheGreatnessOfTheAlmightyAleksolotlISwearIAmWorthyOfUsingTheLegacyBaseTemplate: true,
+        },
+      },
+      {
+        path: "group_roles/assignments/:pk(\\d+)/delete/",
+        component: () => import("aleksis.core/components/LegacyBaseTemplate.vue"),
+        name: "alsijil.deleteGroupRoleAssignment",
+        props: {
+          byTheGreatnessOfTheAlmightyAleksolotlISwearIAmWorthyOfUsingTheLegacyBaseTemplate: true,
+        },
+      },
+      {
+        path: "group_roles/assignments/assign/",
+        component: () => import("aleksis.core/components/LegacyBaseTemplate.vue"),
+        name: "alsijil.assignGroupRoleMultiple",
+        meta: {
+          inMenu: true,
+          titleKey: "alsijil.group_roles.menu_title_assign",
+          icon: "mdi-clipboard-account-outline",
+          permission: "alsijil.assign_grouprole_for_multiple_rule",
+        },
+        props: {
+          byTheGreatnessOfTheAlmightyAleksolotlISwearIAmWorthyOfUsingTheLegacyBaseTemplate: true,
+        },
+      },
+      {
+        path: "all/",
+        component: () => import("aleksis.core/components/LegacyBaseTemplate.vue"),
+        name: "alsijil.allRegisterObjects",
+        meta: {
+          inMenu: true,
+          titleKey: "alsijil.all_lessons.menu_title",
+          icon: "mdi-format-list-text",
+          permission: "alsijil.view_register_objects_list_rule",
+        },
+      },
+      ],
+  }
diff --git a/aleksis/apps/alsijil/frontend/messages/de.json b/aleksis/apps/alsijil/frontend/messages/de.json
new file mode 100644
index 0000000000000000000000000000000000000000..527bebf46ef9ec7b38ba08b65cb0fa00a822c76d
--- /dev/null
+++ b/aleksis/apps/alsijil/frontend/messages/de.json
@@ -0,0 +1,5 @@
+{
+  "alsijil": {
+    "menu_title": "Klassenbuch"
+  }
+}
diff --git a/aleksis/apps/alsijil/frontend/messages/en.json b/aleksis/apps/alsijil/frontend/messages/en.json
new file mode 100644
index 0000000000000000000000000000000000000000..cd9798229b0d867611da8dc6b89dfe02eae91f85
--- /dev/null
+++ b/aleksis/apps/alsijil/frontend/messages/en.json
@@ -0,0 +1,36 @@
+{
+  "alsijil": {
+    "lesson": {
+      "menu_title": "Current lesson"
+    },
+    "week": {
+      "menu_title": "Current week"
+    },
+    "groups": {
+      "menu_title": "My groups"
+    },
+    "persons": {
+      "menu_title": "My students"
+    },
+    "absence": {
+      "menu_title": "Register absence"
+    },
+    "my_overview": {
+      "menu_title": "My overview"
+    },
+    "extra_marks": {
+      "menu_title": "Extra marks"
+    },
+    "excuse_types": {
+      "menu_title": "Excuse types"
+    },
+    "group_roles": {
+      "menu_title_manage": "Manage group roles",
+      "menu_title_assign": "Assign group roles"
+    },
+    "all_lessons": {
+      "menu_title": "All lessons"
+    },
+    "menu_title": "Class register"
+  }
+}
diff --git a/aleksis/apps/alsijil/menus.py b/aleksis/apps/alsijil/menus.py
deleted file mode 100644
index fcf14e7cc8f8ea51eabee00abb5a75c61de42af8..0000000000000000000000000000000000000000
--- a/aleksis/apps/alsijil/menus.py
+++ /dev/null
@@ -1,139 +0,0 @@
-from django.utils.translation import gettext_lazy as _
-
-MENUS = {
-    "NAV_MENU_CORE": [
-        {
-            "name": _("Class register"),
-            "url": "#",
-            "svg_icon": "mdi:book-open-outline",
-            "root": True,
-            "validators": [
-                "menu_generator.validators.is_authenticated",
-                "aleksis.core.util.core_helpers.has_person",
-            ],
-            "submenu": [
-                {
-                    "name": _("Current lesson"),
-                    "url": "lesson_period",
-                    "svg_icon": "mdi:alarm",
-                    "validators": [
-                        (
-                            "aleksis.core.util.predicates.permission_validator",
-                            "alsijil.view_lesson_menu_rule",
-                        ),
-                    ],
-                },
-                {
-                    "name": _("Current week"),
-                    "url": "week_view",
-                    "svg_icon": "mdi:view-week-outline",
-                    "validators": [
-                        (
-                            "aleksis.core.util.predicates.permission_validator",
-                            "alsijil.view_week_menu_rule",
-                        ),
-                    ],
-                },
-                {
-                    "name": _("My groups"),
-                    "url": "my_groups",
-                    "svg_icon": "mdi:account-multiple-outline",
-                    "validators": [
-                        (
-                            "aleksis.core.util.predicates.permission_validator",
-                            "alsijil.view_my_groups_rule",
-                        ),
-                    ],
-                },
-                {
-                    "name": _("My overview"),
-                    "url": "overview_me",
-                    "svg_icon": "mdi:chart-box-outline",
-                    "validators": [
-                        (
-                            "aleksis.core.util.predicates.permission_validator",
-                            "alsijil.view_person_overview_menu_rule",
-                        ),
-                    ],
-                },
-                {
-                    "name": _("My students"),
-                    "url": "my_students",
-                    "svg_icon": "mdi:account-school-outline",
-                    "validators": [
-                        (
-                            "aleksis.core.util.predicates.permission_validator",
-                            "alsijil.view_my_students_rule",
-                        ),
-                    ],
-                },
-                {
-                    "name": _("Assign group role"),
-                    "url": "assign_group_role_multiple",
-                    "svg_icon": "mdi:clipboard-account-outline",
-                    "validators": [
-                        (
-                            "aleksis.core.util.predicates.permission_validator",
-                            "alsijil.assign_grouprole_for_multiple_rule",
-                        ),
-                    ],
-                },
-                {
-                    "name": _("All lessons"),
-                    "url": "all_register_objects",
-                    "svg_icon": "mdi:format-list-text",
-                    "validators": [
-                        (
-                            "aleksis.core.util.predicates.permission_validator",
-                            "alsijil.view_register_objects_list_rule",
-                        ),
-                    ],
-                },
-                {
-                    "name": _("Register absence"),
-                    "url": "register_absence",
-                    "icon": "rate_review",
-                    "validators": [
-                        (
-                            "aleksis.core.util.predicates.permission_validator",
-                            "alsijil.view_register_absence_rule",
-                        ),
-                    ],
-                },
-                {
-                    "name": _("Excuse types"),
-                    "url": "excuse_types",
-                    "svg_icon": "mdi:label-outline",
-                    "validators": [
-                        (
-                            "aleksis.core.util.predicates.permission_validator",
-                            "alsijil.view_excusetypes_rule",
-                        ),
-                    ],
-                },
-                {
-                    "name": _("Extra marks"),
-                    "url": "extra_marks",
-                    "svg_icon": "mdi:label-variant-outline",
-                    "validators": [
-                        (
-                            "aleksis.core.util.predicates.permission_validator",
-                            "alsijil.view_extramarks_rule",
-                        ),
-                    ],
-                },
-                {
-                    "name": _("Manage group roles"),
-                    "url": "group_roles",
-                    "svg_icon": "mdi:clipboard-plus-outline",
-                    "validators": [
-                        (
-                            "aleksis.core.util.predicates.permission_validator",
-                            "alsijil.view_grouproles_rule",
-                        ),
-                    ],
-                },
-            ],
-        }
-    ]
-}
diff --git a/aleksis/apps/alsijil/static/css/alsijil/lesson.css b/aleksis/apps/alsijil/static/css/alsijil/lesson.css
index cb2d9399c9d865c7116c2f8137274edc1ec501d0..fbfa4d8d683d42b8fe003edc54bf10411fbbc7eb 100644
--- a/aleksis/apps/alsijil/static/css/alsijil/lesson.css
+++ b/aleksis/apps/alsijil/static/css/alsijil/lesson.css
@@ -130,4 +130,8 @@
     width: calc(100% + 40px);
     padding: 10px 20px;
     margin: -10px -20px 0;
-}
\ No newline at end of file
+}
+
+.tabs-icons .tab svg.iconify {
+    display: block;
+}
diff --git a/aleksis/apps/alsijil/templates/alsijil/class_register/lesson.html b/aleksis/apps/alsijil/templates/alsijil/class_register/lesson.html
index fd376568067136e72107ec5f9363b4dc68ab4040..056bea93ca036f54869ad037f84b7575f3a2123d 100644
--- a/aleksis/apps/alsijil/templates/alsijil/class_register/lesson.html
+++ b/aleksis/apps/alsijil/templates/alsijil/class_register/lesson.html
@@ -3,7 +3,6 @@
 {% load week_helpers material_form_internal material_form i18n static rules time_helpers %}
 
 {% block browser_title %}{% blocktrans %}Lesson{% endblocktrans %}{% endblock %}
-{% block no_page_title %}{% endblock %}
 {% block extra_head %}
   {{ block.super }}
   <link rel="stylesheet" href="{% static 'css/alsijil/lesson.css' %}"/>
@@ -13,48 +12,8 @@
   {% endif %}
 {% endblock %}
 
-{% block nav_content %}
-  <ul class="tabs tabs-transparent tabs-icons tabs-fixed-width">
-    <li class="tab">
-      <a href="#lesson-documentation">
-        <i class="material-icons iconify" data-icon="mdi:message-bulleted"></i>
-        {% trans "Period" %}
-      </a>
-    </li>
-    {% if register_object.label_ != "lesson_period" or not register_object.get_substitution.cancelled or not request.site.preferences.alsijil__block_personal_notes_for_cancelled %}
-      <li class="tab">
-        <a href="#personal-notes">
-          <i class="material-icons iconify" data-icon="mdi:account-multiple-outline"></i>
-          {% trans "Persons" %}
-        </a>
-      </li>
-    {% endif %}
-    {% if with_seating_plan %}
-      <li class="tab">
-        <a href="#seating-plan">
-          <i class="material-icons iconify" data-icon="mdi:seat-outline"></i>
-          {% trans "Seating plan" %}
-        </a>
-      </li>
-    {% endif %}
-    {% if prev_lesson %}
-      {% has_perm "alsijil.view_lessondocumentation_rule" user prev_lesson as can_view_prev_lesson_documentation %}
-      {% if prev_lesson.get_lesson_documentation and can_view_prev_lesson_documentation %}
-        <li class="tab">
-          <a href="#previous-lesson">
-            <i class="material-icons iconify" data-icon="mdi:history"></i>
-            {% trans "Previous" %}
-          </a>
-        </li>
-      {% endif %}
-    {% endif %}
-    <li class="tab">
-      <a href="#more">
-        <i class="material-icons iconify" data-icon="mdi:dots-horizontal"></i>
-        {% trans "More" %}
-      </a>
-    </li>
-  </ul>
+{% block page_title %}
+  {% include "alsijil/partials/lesson/heading.html" %}
 {% endblock %}
 
 {% block content %}
@@ -62,54 +21,50 @@
   {% has_perm "alsijil.edit_lessondocumentation_rule" user register_object as can_edit_lesson_documentation %}
   {% has_perm "alsijil.edit_register_object_personalnote_rule" user register_object as can_edit_register_object_personalnote %}
 
-  {% if next_lesson_person or prev_lesson_person or back_to_week_url %}
-    <div class="row margin-bottom z-depth-1 alsijil-nav-header">
-      <div class="col s12 no-padding">
-        {# Back to week view #}
-        {% if back_to_week_url %}
-          <a href="{{ back_to_week_url }}"
-             class="btn secondary-color waves-light waves-effect margin-bottom {% if prev_lesson_person or next_lesson_person %}hide-on-extra-large-only{% endif %}">
-            <i class="material-icons iconify left" data-icon="mdi:chevron-left"></i> {% trans "Week view" %}
+  <!-- Tab Buttons -->
+  <div class="col s12 margin-bottom">
+    <ul class="tabs tabs-icons tabs-fixed-width">
+      <li class="tab col">
+        <a href="#lesson-documentation">
+          <i class="material-icons iconify" data-icon="mdi:message-bulleted"></i>
+          {% trans "Period" %}
+        </a>
+      </li>
+      {% if register_object.label_ != "lesson_period" or not register_object.get_substitution.cancelled or not request.site.preferences.alsijil__block_personal_notes_for_cancelled %}
+        <li class="tab col">
+          <a href="#personal-notes">
+            <i class="material-icons iconify" data-icon="mdi:account-multiple-outline"></i>
+            {% trans "Persons" %}
           </a>
-        {% endif %}
-
-        {% if prev_lesson_person or next_lesson_person %}
-          <div class="col s12 no-padding center alsijil-nav">
-            {% if back_to_week_url %}
-              <a href="{{ back_to_week_url }}"
-                 class="btn-flat secondary-color-text waves-light waves-effect left hide-on-med-and-down hide-on-large-only show-on-extra-large">
-                <i class="material-icons iconify left" data-icon="mdi:chevron-left"></i> {% trans "Week view" %}
-              </a>
-            {% endif %}
-
-            {# Previous lesson #}
-            <a class="btn-flat waves-effect waves-light left primary-color-text {% if not prev_lesson_person %}disabled{% endif %}"
-               title="{% trans "My previous lesson" %}"
-                {% if prev_lesson_person %}
-               href="{% url "lesson_period" prev_lesson_person.week.year prev_lesson_person.week.week prev_lesson_person.id %}"
-                {% endif %}
-            >
-              <i class="material-icons iconify left" data-icon="mdi:chevron-left"></i>
-              <span class="hide-on-small-only">{% trans "My previous lesson" %}</span>
-              <span class="hide-on-med-and-up">{% trans "Previous" %}</span>
-            </a>
-            {# Next lesson #}
-            <a class="btn-flat waves-effect waves-light right primary-color-text {% if not next_lesson_person %}disabled{% endif %}"
-               title="{% trans "My next lesson" %}"
-                {% if next_lesson_person %}
-               href="{% url "lesson_period" next_lesson_person.week.year next_lesson_person.week.week next_lesson_person.id %}"
-                {% endif %}
-            >
-              <i class="material-icons iconify right" data-icon="mdi:chevron-right"></i>
-              <span class="hide-on-small-only">{% trans "My next lesson" %}</span>
-              <span class="hide-on-med-and-up">{% trans "Next" %}</span>
+        </li>
+      {% endif %}
+      {% if with_seating_plan %}
+        <li class="tab col">
+          <a href="#seating-plan">
+            <i class="material-icons iconify" data-icon="mdi:seat-outline"></i>
+            {% trans "Seating plan" %}
+          </a>
+        </li>
+      {% endif %}
+      {% if prev_lesson %}
+        {% has_perm "alsijil.view_lessondocumentation_rule" user prev_lesson as can_view_prev_lesson_documentation %}
+        {% if prev_lesson.get_lesson_documentation and can_view_prev_lesson_documentation %}
+          <li class="tab col">
+            <a href="#previous-lesson">
+              <i class="material-icons iconify" data-icon="mdi:history"></i>
+              {% trans "Previous" %}
             </a>
-            <span class="truncate">{{ request.user.person }}</span>
-          </div>
+          </li>
         {% endif %}
-      </div>
-    </div>
-  {% endif %}
+      {% endif %}
+      <li class="tab col">
+        <a href="#more">
+          <i class="material-icons iconify" data-icon="mdi:dots-horizontal"></i>
+          {% trans "More" %}
+        </a>
+      </li>
+    </ul>
+  </div>
 
   <form method="post" class="row">
     {% csrf_token %}
@@ -148,8 +103,6 @@
         </div>
       </div>
     {% else %}
-      {% include "alsijil/partials/lesson/heading.html" %}
-
       <div class="row no-margin">
         <div class="container">
           <div class="card">
diff --git a/aleksis/apps/alsijil/templates/alsijil/partials/lesson/heading.html b/aleksis/apps/alsijil/templates/alsijil/partials/lesson/heading.html
index 4b768265a510df03c6fab59b40cb4e356f90290a..132e97f05acd0216d59f89a12cab08b96e123cc5 100644
--- a/aleksis/apps/alsijil/templates/alsijil/partials/lesson/heading.html
+++ b/aleksis/apps/alsijil/templates/alsijil/partials/lesson/heading.html
@@ -1,5 +1,54 @@
 {% load i18n %}
 
+{% if next_lesson_person or prev_lesson_person or back_to_week_url %}
+  <div class="row margin-bottom alsijil-nav-header">
+    <div class="col s12 no-padding">
+      {# Back to week view #}
+      {% if back_to_week_url %}
+        <a href="{{ back_to_week_url }}"
+           class="btn secondary-color waves-light waves-effect margin-bottom {% if prev_lesson_person or next_lesson_person %}hide-on-extra-large-only{% endif %}">
+          <i class="material-icons iconify left" data-icon="mdi:chevron-left"></i> {% trans "Week view" %}
+        </a>
+      {% endif %}
+
+      {% if prev_lesson_person or next_lesson_person %}
+        <div class="col s12 no-padding center alsijil-nav">
+          {% if back_to_week_url %}
+            <a href="{{ back_to_week_url }}"
+               class="btn-flat secondary-color-text waves-light waves-effect left hide-on-med-and-down hide-on-large-only show-on-extra-large">
+              <i class="material-icons iconify left" data-icon="mdi:chevron-left"></i> {% trans "Week view" %}
+            </a>
+          {% endif %}
+
+          {# Previous lesson #}
+          <a class="btn-flat waves-effect waves-light left primary-color-text {% if not prev_lesson_person %}disabled{% endif %}"
+             title="{% trans "My previous lesson" %}"
+              {% if prev_lesson_person %}
+             href="{% url "lesson_period" prev_lesson_person.week.year prev_lesson_person.week.week prev_lesson_person.id %}"
+              {% endif %}
+          >
+            <i class="material-icons iconify left" data-icon="mdi:chevron-left"></i>
+            <span class="hide-on-small-only">{% trans "My previous lesson" %}</span>
+            <span class="hide-on-med-and-up">{% trans "Previous" %}</span>
+          </a>
+          {# Next lesson #}
+          <a class="btn-flat waves-effect waves-light right primary-color-text {% if not next_lesson_person %}disabled{% endif %}"
+             title="{% trans "My next lesson" %}"
+              {% if next_lesson_person %}
+             href="{% url "lesson_period" next_lesson_person.week.year next_lesson_person.week.week next_lesson_person.id %}"
+              {% endif %}
+          >
+            <i class="material-icons iconify right" data-icon="mdi:chevron-right"></i>
+            <span class="hide-on-small-only">{% trans "My next lesson" %}</span>
+            <span class="hide-on-med-and-up">{% trans "Next" %}</span>
+          </a>
+          <span class="truncate">{{ request.user.person }}</span>
+        </div>
+      {% endif %}
+    </div>
+  </div>
+{% endif %}
+
 <h1>
   <span class="right hide-on-small-only">
     {% include "alsijil/partials/lesson_status.html" with register_object=register_object css_class="medium" %}
diff --git a/aleksis/apps/alsijil/templates/alsijil/partials/lesson/tabs/documentation.html b/aleksis/apps/alsijil/templates/alsijil/partials/lesson/tabs/documentation.html
index ef0f42048bf1df524924ec1f380f8d88dd7fab3e..22b396f457a13d4d04824836745fd49fdf7eac72 100644
--- a/aleksis/apps/alsijil/templates/alsijil/partials/lesson/tabs/documentation.html
+++ b/aleksis/apps/alsijil/templates/alsijil/partials/lesson/tabs/documentation.html
@@ -1,6 +1,5 @@
 {% load i18n material_form_internal material_form %}
 
-{% include "alsijil/partials/lesson/heading.html" %}
 {% include "alsijil/partials/lesson/prev_next.html" with with_save=0 %}
 
 <div class="hide-on-med-and-up margin-bottom">
diff --git a/aleksis/apps/alsijil/templates/alsijil/partials/lesson/tabs/more.html b/aleksis/apps/alsijil/templates/alsijil/partials/lesson/tabs/more.html
index b7e010125077dd003e529bb85432dfc64adad7ab..ffc7706488757871e43de4e71da9e73802273a3f 100644
--- a/aleksis/apps/alsijil/templates/alsijil/partials/lesson/tabs/more.html
+++ b/aleksis/apps/alsijil/templates/alsijil/partials/lesson/tabs/more.html
@@ -1,7 +1,5 @@
 {% load i18n %}
 
-{% include "alsijil/partials/lesson/heading.html" %}
-
 {% if group_roles %}
   {% include "alsijil/group_role/partials/assigned_roles.html" with roles=group_roles group=register_object.get_groups.first back_url=back_url %}
 {% endif %}
diff --git a/aleksis/apps/alsijil/templates/alsijil/partials/lesson/tabs/notes.html b/aleksis/apps/alsijil/templates/alsijil/partials/lesson/tabs/notes.html
index e94fb66d7632272eacbdbc0b09ba868127fc9f84..1b013252a8f316979cdf5bdcc87ef1c7463b6e36 100644
--- a/aleksis/apps/alsijil/templates/alsijil/partials/lesson/tabs/notes.html
+++ b/aleksis/apps/alsijil/templates/alsijil/partials/lesson/tabs/notes.html
@@ -1,6 +1,5 @@
 {% load i18n material_form_internal material_form time_helpers %}
 
-{% include "alsijil/partials/lesson/heading.html" %}
 {% include "alsijil/partials/lesson/prev_next.html" with with_save=1 %}
 
 {% if not blocked_because_holidays %}
diff --git a/aleksis/apps/alsijil/views.py b/aleksis/apps/alsijil/views.py
index 99809b7643f972c79ed0469adee90aa2ee7aac67..7cee4ac8d7b389cd4e838d992ef68750e31386d5 100644
--- a/aleksis/apps/alsijil/views.py
+++ b/aleksis/apps/alsijil/views.py
@@ -31,6 +31,7 @@ from aleksis.apps.chronos.managers import TimetableType
 from aleksis.apps.chronos.models import Event, ExtraLesson, Holiday, LessonPeriod, TimePeriod
 from aleksis.apps.chronos.util.build import build_weekdays
 from aleksis.apps.chronos.util.date import get_weeks_for_year, week_weekday_to_date
+from aleksis.core.decorators import pwa_cache
 from aleksis.core.mixins import (
     AdvancedCreateView,
     AdvancedDeleteView,
@@ -77,6 +78,7 @@ from .util.alsijil_helpers import (
 )
 
 
+@pwa_cache
 @permission_required("alsijil.view_register_object_rule", fn=get_register_object_by_pk)  # FIXME
 def register_object(
     request: HttpRequest,
@@ -326,6 +328,7 @@ def register_object(
     return render(request, "alsijil/class_register/lesson.html", context)
 
 
+@pwa_cache
 @permission_required("alsijil.view_week_rule", fn=get_timetable_instance_by_pk)
 def week_view(
     request: HttpRequest,
@@ -631,6 +634,7 @@ def week_view(
     return render(request, "alsijil/class_register/week_view.html", context)
 
 
+@pwa_cache
 @permission_required(
     "alsijil.view_full_register_rule", fn=objectgetter_optional(Group, None, False)
 )
@@ -667,6 +671,7 @@ def full_register_group(request: HttpRequest, id_: int) -> HttpResponse:
     )
 
 
+@pwa_cache
 @permission_required("alsijil.view_my_students_rule")
 def my_students(request: HttpRequest) -> HttpResponse:
     context = {}
@@ -707,6 +712,7 @@ def my_students(request: HttpRequest) -> HttpResponse:
     return render(request, "alsijil/class_register/persons.html", context)
 
 
+@pwa_cache
 @permission_required(
     "alsijil.view_my_groups_rule",
 )
@@ -718,6 +724,7 @@ def my_groups(request: HttpRequest) -> HttpResponse:
     return render(request, "alsijil/class_register/groups.html", context)
 
 
+@method_decorator(pwa_cache, "dispatch")
 class StudentsList(PermissionRequiredMixin, DetailView):
     model = Group
     template_name = "alsijil/class_register/students_list.html"
@@ -737,6 +744,7 @@ class StudentsList(PermissionRequiredMixin, DetailView):
         return context
 
 
+@pwa_cache
 @permission_required(
     "alsijil.view_person_overview_rule",
     fn=objectgetter_optional(
@@ -1048,6 +1056,7 @@ class DeletePersonalNoteView(PermissionRequiredMixin, DetailView):
         return redirect("overview_person", note.person.pk)
 
 
+@method_decorator(pwa_cache, "dispatch")
 class ExtraMarkListView(PermissionRequiredMixin, SingleTableView):
     """Table of all extra marks."""
 
@@ -1092,6 +1101,7 @@ class ExtraMarkDeleteView(PermissionRequiredMixin, RevisionMixin, AdvancedDelete
     success_message = _("The extra mark has been deleted.")
 
 
+@method_decorator(pwa_cache, "dispatch")
 class ExcuseTypeListView(PermissionRequiredMixin, SingleTableView):
     """Table of all excuse types."""
 
@@ -1136,6 +1146,7 @@ class ExcuseTypeDeleteView(PermissionRequiredMixin, RevisionMixin, AdvancedDelet
     success_message = _("The excuse type has been deleted.")
 
 
+@method_decorator(pwa_cache, "dispatch")
 class GroupRoleListView(PermissionRequiredMixin, SingleTableView):
     """Table of all group roles."""
 
@@ -1180,6 +1191,7 @@ class GroupRoleDeleteView(PermissionRequiredMixin, RevisionMixin, AdvancedDelete
     success_message = _("The group role has been deleted.")
 
 
+@method_decorator(pwa_cache, "dispatch")
 class AssignedGroupRolesView(PermissionRequiredMixin, DetailView):
     permission_required = "alsijil.view_assigned_grouproles_rule"
     model = Group
@@ -1302,6 +1314,7 @@ class GroupRoleAssignmentDeleteView(
         return reverse("assigned_group_roles", args=[pk])
 
 
+@method_decorator(pwa_cache, "dispatch")
 class AllRegisterObjectsView(PermissionRequiredMixin, View):
     """Provide overview of all register objects for coordinators."""
 
diff --git a/pyproject.toml b/pyproject.toml
index a3a4aea8d9b8f3cfd286cab366900bcce9ee5b9c..c857f98eef3eace85829b5de01f3cb7d0867f161 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
 [tool.poetry]
 name = "AlekSIS-App-Alsijil"
-version = "2.2.dev0"
+version = "3.0.dev0"
 packages = [
     { include = "aleksis" }
 ]
@@ -48,8 +48,8 @@ secondary = true
 
 [tool.poetry.dependencies]
 python = "^3.9"
-aleksis-core = "^2.12"
-aleksis-app-chronos = "^2.2"
+aleksis-core = "^3.0.dev3"
+aleksis-app-chronos = "^3.0.dev1"
 aleksis-app-stoelindeling = { version = "^1.0", optional = true }
 
 [tool.poetry.dev-dependencies]