diff --git a/muppet/breadcrumbs.py b/muppet/breadcrumbs.py
new file mode 100644
index 0000000000000000000000000000000000000000..ae88d3c172eb9eb13d2d0920813c462075296017
--- /dev/null
+++ b/muppet/breadcrumbs.py
@@ -0,0 +1,59 @@
+"""
+Page breadcrumbs.
+
+Page breadcrumbs are the "how did we get here" at the top of web pages.
+
+For example, the page
+``/modules/example-module/example-class`` would have a breadcrumb list like
+
+- ``/modules`` - Modules
+- ``/modules/example-module`` - Example Module
+- ``/modules/example-module/example-class`` Example Class
+"""
+
+import re
+from dataclasses import dataclass
+from typing import (
+    Tuple,
+)
+
+
+@dataclass
+class Breadcrumb:
+    """
+    A breadcrumb entry.
+
+    :param ref:
+        A url path. Meaning that it should contain all parents.
+    :param text:
+        The displayed text for this entry.
+    """
+
+    ref: str
+    text: str
+
+
+def breadcrumbs(*items: str | Tuple[str, str]) -> list[Breadcrumb]:
+    """
+    Generate a breadcrumb trail.
+
+    :param items:
+        The parts of the trace.
+
+        Each item should either be a single string, which is then used
+        as both the name, and the url component, or a tuple, in which
+        case the left value will be the displaed string, and the right
+        value the url component.
+    """
+    url = '/'
+    result = []
+    for item in items:
+        if isinstance(item, str):
+            url += item + '/'
+            text = item
+        else:
+            url += item[1] + '/'
+            text = item[0]
+        url = re.sub('/+', '/', url)
+        result.append(Breadcrumb(ref=url, text=text))
+    return result
diff --git a/muppet/output.py b/muppet/output.py
index 0aae7ed110656127769f9792d5c8dbf1268a5694..676bbb464f7e58a61c2b027ad6a42464bead527c 100644
--- a/muppet/output.py
+++ b/muppet/output.py
@@ -29,6 +29,7 @@ from collections.abc import (
 )
 from .util import group_by
 from .puppet.strings import isprivate
+from .breadcrumbs import breadcrumbs
 
 
 # TODO replace 'output' with base, or put this somewhere else
@@ -301,8 +302,15 @@ def setup_module(base: str, module: ModuleEntry, *, path_base: str) -> None:
 
         with open(os.path.join(dir, 'index.html'), 'w') as f:
             template = jinja.get_template('code_page.html')
+            crumbs = breadcrumbs(
+                    ('Environment', ''),
+                    module.name,
+                    (puppet_class['name'],
+                     'manifests/' + '/'.join(puppet_class['name'].split('::')[1:])),
+                    )
             f.write(template.render(content=format_class(puppet_class),
-                                    path_base=path_base))
+                                    path_base=path_base,
+                                    breadcrumbs=crumbs))
 
         # puppet_class['file']
         # puppet_class['line']
diff --git a/static/style.css b/static/style.css
index fb19678a0ed02564100d9635f7bfad42d1827b0e..7ab8ae0de173b84c0b060e2ec9ae7bec47c64d75 100644
--- a/static/style.css
+++ b/static/style.css
@@ -80,3 +80,17 @@ code.json {
 .comment p:last-child {
 	margin-bottom: 0;
 }
+
+.breadcrumb {
+	padding: 0;
+}
+
+.breadcrumb li {
+	display: inline-block;
+	padding: 0;
+}
+
+.breadcrumb li:not(:first-child)::before {
+	content: "ยป";
+	padding: 1ex;
+}
diff --git a/templates/base.html b/templates/base.html
index c4351755afe6cbce3b23a25cf744b945892446f4..abc40f1975a8116e9791beda27cdd20cc2728a25 100644
--- a/templates/base.html
+++ b/templates/base.html
@@ -20,6 +20,15 @@
 		</noscript>
 	</head>
 	<body>
+		<header>
+			{% if breadcrumbs %}
+				<ul class="breadcrumb">
+					{%- for item in breadcrumbs -%}
+						<li><a href="{{ path_base }}{{ item.ref }}">{{ item.text }}</a></li>
+					{%- endfor -%}
+				</ul>
+			{% endif %}
+		</header>
 		{% block content %}
 		{% endblock %}
 	</body>