diff --git a/aleksis/apps/kort/frontend/index.js b/aleksis/apps/kort/frontend/index.js
index 997acdeab6833d6aff2ac9f36fde7ac8e8032d0c..12baed9d53d5cf85a4d34f771c026a773f7c008e 100644
--- a/aleksis/apps/kort/frontend/index.js
+++ b/aleksis/apps/kort/frontend/index.js
@@ -52,6 +52,15 @@ export default {
         byTheGreatnessOfTheAlmightyAleksolotlISwearIAmWorthyOfUsingTheLegacyBaseTemplate: true,
       },
     },
+    {
+      path: "cards/:pk/preview/",
+      component: () => import("aleksis.core/components/LegacyBaseTemplate.vue"),
+      name: "kort.previewCard",
+
+      props: {
+        byTheGreatnessOfTheAlmightyAleksolotlISwearIAmWorthyOfUsingTheLegacyBaseTemplate: true,
+      },
+    },
     {
       path: "cards/:pk/print/",
       component: () => import("aleksis.core/components/LegacyBaseTemplate.vue"),
diff --git a/aleksis/apps/kort/migrations/0017_alter_card_managers_alter_cardlayout_managers_and_more.py b/aleksis/apps/kort/migrations/0017_alter_card_managers_alter_cardlayout_managers_and_more.py
new file mode 100644
index 0000000000000000000000000000000000000000..efed94299fd25b35130f69c68fc1400ab3d228a7
--- /dev/null
+++ b/aleksis/apps/kort/migrations/0017_alter_card_managers_alter_cardlayout_managers_and_more.py
@@ -0,0 +1,161 @@
+# Generated by Django 4.2.5 on 2023-09-05 14:43
+
+import aleksis.core.managers
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ("sites", "0002_alter_domain_unique"),
+        ("kort", "0016_cardprinter_oauth2_client_secret"),
+    ]
+
+    operations = [
+        migrations.AlterModelManagers(
+            name="card",
+            managers=[
+                ("objects", aleksis.core.managers.AlekSISBaseManager()),
+            ],
+        ),
+        migrations.AlterModelManagers(
+            name="cardlayout",
+            managers=[
+                ("objects", aleksis.core.managers.AlekSISBaseManager()),
+            ],
+        ),
+        migrations.AlterModelManagers(
+            name="cardlayoutmediafile",
+            managers=[
+                ("objects", aleksis.core.managers.AlekSISBaseManager()),
+            ],
+        ),
+        migrations.AlterModelManagers(
+            name="cardprinter",
+            managers=[
+                ("objects", aleksis.core.managers.AlekSISBaseManager()),
+            ],
+        ),
+        migrations.AlterModelManagers(
+            name="cardprintjob",
+            managers=[
+                ("objects", aleksis.core.managers.AlekSISBaseManager()),
+            ],
+        ),
+        migrations.AddField(
+            model_name="card",
+            name="managed_by_app_label",
+            field=models.CharField(
+                blank=True,
+                editable=False,
+                max_length=255,
+                verbose_name="App label of app responsible for managing this instance",
+            ),
+        ),
+        migrations.AddField(
+            model_name="cardlayout",
+            name="managed_by_app_label",
+            field=models.CharField(
+                blank=True,
+                editable=False,
+                max_length=255,
+                verbose_name="App label of app responsible for managing this instance",
+            ),
+        ),
+        migrations.AddField(
+            model_name="cardlayoutmediafile",
+            name="managed_by_app_label",
+            field=models.CharField(
+                blank=True,
+                editable=False,
+                max_length=255,
+                verbose_name="App label of app responsible for managing this instance",
+            ),
+        ),
+        migrations.AddField(
+            model_name="cardprinter",
+            name="managed_by_app_label",
+            field=models.CharField(
+                blank=True,
+                editable=False,
+                max_length=255,
+                verbose_name="App label of app responsible for managing this instance",
+            ),
+        ),
+        migrations.AddField(
+            model_name="cardprintjob",
+            name="managed_by_app_label",
+            field=models.CharField(
+                blank=True,
+                editable=False,
+                max_length=255,
+                verbose_name="App label of app responsible for managing this instance",
+            ),
+        ),
+        migrations.AlterField(
+            model_name="card",
+            name="site",
+            field=models.ForeignKey(
+                default=1,
+                editable=False,
+                on_delete=django.db.models.deletion.CASCADE,
+                related_name="+",
+                to="sites.site",
+            ),
+        ),
+        migrations.AlterField(
+            model_name="cardlayout",
+            name="site",
+            field=models.ForeignKey(
+                default=1,
+                editable=False,
+                on_delete=django.db.models.deletion.CASCADE,
+                related_name="+",
+                to="sites.site",
+            ),
+        ),
+        migrations.AlterField(
+            model_name="cardlayoutmediafile",
+            name="site",
+            field=models.ForeignKey(
+                default=1,
+                editable=False,
+                on_delete=django.db.models.deletion.CASCADE,
+                related_name="+",
+                to="sites.site",
+            ),
+        ),
+        migrations.AlterField(
+            model_name="cardprinter",
+            name="cups_printer",
+            field=models.CharField(
+                blank=True,
+                help_text="Leave blank to deactivate CUPS printing",
+                max_length=255,
+                verbose_name="CUPS printer",
+            ),
+        ),
+        migrations.AlterField(
+            model_name="cardprinter",
+            name="site",
+            field=models.ForeignKey(
+                default=1,
+                editable=False,
+                on_delete=django.db.models.deletion.CASCADE,
+                related_name="+",
+                to="sites.site",
+            ),
+        ),
+        migrations.AlterField(
+            model_name="cardprintjob",
+            name="site",
+            field=models.ForeignKey(
+                default=1,
+                editable=False,
+                on_delete=django.db.models.deletion.CASCADE,
+                related_name="+",
+                to="sites.site",
+            ),
+        ),
+    ]
diff --git a/aleksis/apps/kort/tables.py b/aleksis/apps/kort/tables.py
index 6a67ac39b302ba6837096592328f04a31ad917ec..9cc9b6cc42a8e8a65bdba6b39357874e6c8de5e3 100644
--- a/aleksis/apps/kort/tables.py
+++ b/aleksis/apps/kort/tables.py
@@ -3,13 +3,7 @@ from django.template.loader import render_to_string
 from django.utils.safestring import SafeString, mark_safe
 from django.utils.translation import gettext as _
 
-from django_tables2 import (
-    BooleanColumn,
-    Column,
-    DateTimeColumn,
-    LinkColumn,
-    Table,
-)
+from django_tables2 import BooleanColumn, Column, DateTimeColumn, LinkColumn, Table
 from django_tables2.utils import A, AttributeDict, computed_values
 
 from aleksis.apps.kort.forms import PrinterSelectForm
diff --git a/aleksis/apps/kort/templates/kort/card/detail_content.html b/aleksis/apps/kort/templates/kort/card/detail_content.html
index 07e102e5b030da2974930fbabd85d97de3df6ca3..dc1830e8a0ddf6c753806e36ff463583229fa5c8 100644
--- a/aleksis/apps/kort/templates/kort/card/detail_content.html
+++ b/aleksis/apps/kort/templates/kort/card/detail_content.html
@@ -63,5 +63,9 @@
         </div>
       {% endif %}
     {% endif %}
+    <a href="{% url 'preview_card' card.pk %}" class="btn waves-effect waves-light">
+      <i class="material-icons left iconify" data-icon="mdi:file-pdf-box"></i>
+      {% trans "Preview card layout" %}
+    </a>
   </div>
 </div>
\ No newline at end of file
diff --git a/aleksis/apps/kort/urls.py b/aleksis/apps/kort/urls.py
index 424fd51ca8b788cc738c9ff755377173a7e4fb54..06e9f0168d5223a76b85e9f8a4da72d4a66a30e3 100644
--- a/aleksis/apps/kort/urls.py
+++ b/aleksis/apps/kort/urls.py
@@ -12,6 +12,7 @@ urlpatterns = [
         name="generate_card_pdf",
     ),
     path("cards/<int:pk>/deactivate/", views.CardDeactivateView.as_view(), name="deactivate_card"),
+    path("cards/<int:pk>/preview/", views.CardPreviewView.as_view(), name="preview_card"),
     path("cards/<int:pk>/print/", views.CardPrintView.as_view(), name="print_card"),
     path("cards/<int:pk>/delete/", views.CardDeleteView.as_view(), name="delete_card"),
     path("printers/", views.CardPrinterListView.as_view(), name="card_printers"),
diff --git a/aleksis/apps/kort/views.py b/aleksis/apps/kort/views.py
index 53177c497b87fdbe2509f69968873bc65bc65254..7e510339be0b846ed37668ba52e277319b630474 100644
--- a/aleksis/apps/kort/views.py
+++ b/aleksis/apps/kort/views.py
@@ -209,6 +209,16 @@ class CardDetailView(PermissionRequiredMixin, RevisionMixin, PrinterSelectMixin,
     template_name = "kort/card/detail.html"
 
 
+class CardPreviewView(PermissionRequiredMixin, DetailView):
+    permission_required = "kort.view_card_rule"
+    model = Card
+
+    def get(self, *args, **kwargs):
+        self.object = self.get_object()
+        html = self.object.layout.render(self.object)
+        return HttpResponse(html)
+
+
 class CardGeneratePDFView(PermissionRequiredMixin, RevisionMixin, SingleObjectMixin, View):
     permission_required = "views.edit_card_rule"
     model = Card