diff --git a/tests/ref/html/link-html-here.html b/tests/ref/html/link-html-here.html
new file mode 100644
index 000000000..783c050bd
--- /dev/null
+++ b/tests/ref/html/link-html-here.html
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+ Go
+
+
diff --git a/tests/ref/html/link-html-id-attach.html b/tests/ref/html/link-html-id-attach.html
new file mode 100644
index 000000000..d1e7cbf9b
--- /dev/null
+++ b/tests/ref/html/link-html-id-attach.html
@@ -0,0 +1,29 @@
+
+
+
+
+
+
+
+
+ Hi
+ Hi there
+ See it
+ See it here
+ See a b
+ See a b
+ See
+
+ See
+
+
+
diff --git a/tests/ref/html/link-html-id-existing.html b/tests/ref/html/link-html-id-existing.html
new file mode 100644
index 000000000..7686fc9ac
--- /dev/null
+++ b/tests/ref/html/link-html-id-existing.html
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
+ This
+ Go
+
+
diff --git a/tests/ref/html/link-html-label-disambiguation.html b/tests/ref/html/link-html-label-disambiguation.html
new file mode 100644
index 000000000..41f1d33a9
--- /dev/null
+++ b/tests/ref/html/link-html-label-disambiguation.html
@@ -0,0 +1,31 @@
+
+
+
+
+
+
+
+ A
+ B
+ C
+ D
+ E
+ F
+ G
+ H
+ I
+ J
+
+
+
diff --git a/tests/ref/html/link-html-nested-empty.html b/tests/ref/html/link-html-nested-empty.html
new file mode 100644
index 000000000..e197d54e1
--- /dev/null
+++ b/tests/ref/html/link-html-nested-empty.html
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+
+ Hi
+ A B C
+
+
diff --git a/tests/suite/model/link.typ b/tests/suite/model/link.typ
index bd6c8a307..43d72a888 100644
--- a/tests/suite/model/link.typ
+++ b/tests/suite/model/link.typ
@@ -66,6 +66,74 @@ My cool #box(move(dx: 0.7cm, dy: 0.7cm, rotate(10deg, scale(200%, mylink))))
Text
#link()[Go to text.]
+--- link-html-id-attach html ---
+// Tests how IDs and, if necessary, spans, are added to the DOM to support
+// links.
+
+#for i in range(1, 9) {
+ list.item(link(label("t" + str(i)), [Go]))
+}
+
+// Text at start of paragraph
+Hi
+
+// Text at start of paragraph + more text
+Hi there
+
+// Text in the middle of paragraph
+See #[it ]
+
+// Text in the middle of paragraph + more text
+See #[it ] here
+
+// Text + more elements
+See #[a *b*]
+
+// Element
+See *a _b_*
+
+// Nothing
+See #[]
+
+// Nothing 2
+See #metadata(none)
+
+--- link-html-label-disambiguation html ---
+// Tests automatic ID generation for labelled elements.
+
+#[= A] #label("%") // not reusable => loc-1
+= B <1> // not reusable => loc-3 (loc-2 exists)
+= C // reusable, unique => loc
+= D // reusable, unique => loc-2
+= E // reusable, not unique => lib-1
+= F // reusable, not unique => lib-3 (lib-2 exists)
+= G // reusable, unique => lib-2
+= H // reusable, unique => hi
+= I // reusable, not unique => hi-2-1
+= J // reusable, not unique => hi-2-2
+
+#context for it in query(heading) {
+ list.item(link(it.location(), it.body))
+}
+
+--- link-html-id-existing html ---
+// Test that linking reuses the existing ID, if any.
+#html.div[
+ #html.span(id: "this")[This]
+]
+
+#link()[Go]
+
+--- link-html-here html ---
+#context link(here())[Go]
+
+--- link-html-nested-empty html ---
+#[#metadata(none) #metadata(none) Hi]
+
+#link()[A] // creates empty span
+#link()[B] // creates second empty span
+#link()[C] // links to #a because the generated span is contained in it
+
--- link-to-label-missing ---
// Error: 2-20 label `` does not exist in the document
#link()[Nope.]