diff --git a/Makefile b/Makefile
index 36a67c193cf39daa27af48e522ea672faeb09c88..cf85308e4ebcf5fe2fb883977baa445ca7ed3169 100644
--- a/Makefile
+++ b/Makefile
@@ -3,7 +3,7 @@
 all: output
 
 output:
-	python -m muppet --env ~/puppet/generated-environments/production/modules/
+	python -m muppet --env ~/puppet/generated-environments/test/modules/
 
 check:
 	flake8 muppet
diff --git a/muppet/format.py b/muppet/format.py
index 4065136284aafc1fb896f3174e74cdfbdb873942..b0e7e2a54f6fb26d2fe78b05811930cb1270aeaf 100644
--- a/muppet/format.py
+++ b/muppet/format.py
@@ -47,7 +47,7 @@ class Tag:
         inner: str
         if isinstance(self.item, str):
             inner = self.item
-        elif isinstance(self.item, Tag):
+        elif isinstance(self.item, Markup):
             inner = str(self.item)
         else:
             inner = ''.join(str(i) for i in self.item)
@@ -56,16 +56,71 @@ class Tag:
         return f'<span class="{tags}">{inner}</span>'
 
 
+@dataclass
+class Link:
+    """An item which should link somewhere."""
+
+    item: Any
+    target: str
+
+    def __str__(self) -> str:
+        return f'<a href="{self.target}">{self.item}</a>'
+
+
+@dataclass
+class ID:
+    """Item with an ID attached."""
+
+    item: Any
+    id: str
+
+    def __str__(self) -> str:
+        return f'<span id="{self.id}">{self.item}</span>'
+
+
+@dataclass
+class Documentation:
+    """Attach documentation to a given item."""
+
+    item: Any
+    documentation: str
+
+    def __str__(self) -> str:
+        s = '<span class="documentation-anchor">'
+        s += str(self.item)
+        s += f'<div class="documentation">{self.documentation}</div>'
+        s += '</span>'
+        return s
+
+
+Markup: TypeAlias = Tag | Link | ID | Documentation
+
+
 all_tags: set[str] = set()
 
 
-def tag(item: str | Tag | Sequence[str | Tag], *tags: str) -> Tag:
+def tag(item: str | Markup | Sequence[str | Markup], *tags: str) -> Tag:
     """Tag item with tags."""
     global all_tags
     all_tags |= set(tags)
     return Tag(item, tags=tags)
 
 
+def link(item: str | Markup, target: str) -> Link:
+    """Create a new link element."""
+    return Link(item, target)
+
+
+def id(item: str | Markup, id: str) -> ID:
+    """Attach an id to an item."""
+    return ID(item, id)
+
+
+def doc(item: str | Markup, documentation: str) -> Documentation:
+    """Attach documentation to an item."""
+    return Documentation(item, documentation)
+
+
 def ind(level: int) -> str:
     """Returnu a string for indentation."""
     return ' '*level*2
@@ -128,11 +183,17 @@ def print_var(x: str, dollar: bool = True) -> Tag:
         If there should be a dollar prefix.
     """
     dol = '$' if dollar else ''
-    if doc := param_doc.get(x):
-        s = f'{dol}{x}<div class="documentation">{doc}</div>'
-        return tag(s, 'var')
+    if docs := param_doc.get(x):
+        s = f'{dol}{x}'
+        return link(doc(tag(s, 'var'), docs), f'#{x}')
     else:
-        return tag(f'{dol}{x}', 'var')
+        return link(tag(f'{dol}{x}', 'var'), f'#{x}')
+
+
+def declare_var(x: str) -> Tag:
+    """Returna a tag declaring that this variable exists."""
+    return tag(id(f"${x}", x), 'var')
+
 
 # TODO strip leading colons when looking up documentation
 
@@ -356,7 +417,7 @@ def parse(form: Any, indent: int, context: list[str]) -> Tag:
                                       'type'),
                                   ' ']
                     # print(f'<span class="var">${name}</span>', end='')
-                    items += [print_var(name)]
+                    items += [declare_var(name)]
                     if 'value' in data:
                         items += [
                             ' = ',
@@ -775,7 +836,10 @@ def parse(form: Any, indent: int, context: list[str]) -> Tag:
         case ['var', x]:
             # TODO how does this work with deeply nested expressions
             # in strings?
-            return print_var(x, context[0] != 'str')
+            if context[0] == 'declaration':
+                return declare_var(x)
+            else:
+                return print_var(x, context[0] != 'str')
 
         case ['virtual-query', q]:
             return tag([
@@ -914,10 +978,10 @@ def parse(form: Any, indent: int, context: list[str]) -> Tag:
 
         case ['=', field, value]:
             return tag([
-                parse(field, indent, context),
+                parse(field, indent, ['declaration'] + context),
                 ' ', '=', ' ',
                 parse(value, indent, context),
-            ])
+            ], 'declaration')
 
         case ['==', a, b]:
             return tag([
@@ -931,7 +995,7 @@ def parse(form: Any, indent: int, context: list[str]) -> Tag:
                 parse(a, indent, context),
                 ' ', '=~', ' ',
                 parse(b, indent, context),
-            ])
+            ], 'declaration')
 
         case ['!~', a, b]:
             return tag([
diff --git a/static/style.css b/static/style.css
index a62934bd1cebd76b0cf731a9a28b55792a8df1ca..01ca608250a9a9f47fa82be10d393cac9a671b4d 100644
--- a/static/style.css
+++ b/static/style.css
@@ -49,3 +49,7 @@ h2 {
 code.json {
 	font-size: 80%;
 }
+
+:target {
+	background-color: yellow;
+}