diff --git a/mdbook-spec/src/lib.rs b/mdbook-spec/src/lib.rs
index 9eac0e4ed..7b06704f4 100644
--- a/mdbook-spec/src/lib.rs
+++ b/mdbook-spec/src/lib.rs
@@ -24,6 +24,10 @@ static ADMONITION_RE: Lazy<Regex> = Lazy::new(|| {
     Regex::new(r"(?m)^ *> \[!(?<admon>[^]]+)\]\n(?<blockquote>(?: *>.*\n)+)").unwrap()
 });
 
+/// A primitive regex to find link reference definitions.
+static MD_LINK_REFERENCE_DEFINITION: Lazy<Regex> =
+    Lazy::new(|| Regex::new(r"(?m)^\[(?<label>[^]]+)]: (?<dest>.*)").unwrap());
+
 pub fn handle_preprocessing() -> Result<(), Error> {
     let pre = Spec::new(None)?;
     let (ctx, book) = CmdPreprocessor::parse_input(io::stdin())?;
@@ -114,6 +118,34 @@ impl Spec {
         Ok(Spec { rust_root })
     }
 
+    /// Converts link reference definitions that point to a rule to the correct link.
+    ///
+    /// For example:
+    /// ```markdown
+    /// See [this rule].
+    ///
+    /// [this rule]: expr.array
+    /// ```
+    ///
+    /// This will convert the `[this rule]` definition to point to the actual link.
+    fn rule_link_references(&self, chapter: &Chapter, rules: &Rules) -> String {
+        let current_path = chapter.path.as_ref().unwrap().parent().unwrap();
+        MD_LINK_REFERENCE_DEFINITION
+            .replace_all(&chapter.content, |caps: &Captures<'_>| {
+                let dest = &caps["dest"];
+                if let Some((_source_path, path)) = rules.def_paths.get(dest) {
+                    let label = &caps["label"];
+                    let relative = pathdiff::diff_paths(path, current_path).unwrap();
+                    // Adjust paths for Windows.
+                    let relative = relative.display().to_string().replace('\\', "/");
+                    format!("[{label}]: {relative}#r-{dest}")
+                } else {
+                    caps.get(0).unwrap().as_str().to_string()
+                }
+            })
+            .to_string()
+    }
+
     /// Generates link references to all rules on all pages, so you can easily
     /// refer to rules anywhere in the book.
     fn auto_link_references(&self, chapter: &Chapter, rules: &Rules) -> String {
@@ -255,6 +287,7 @@ impl Preprocessor for Spec {
                 return;
             }
             ch.content = self.admonitions(&ch, &mut diag);
+            ch.content = self.rule_link_references(&ch, &rules);
             ch.content = self.auto_link_references(&ch, &rules);
             ch.content = self.render_rule_definitions(&ch.content, &tests, &git_ref);
             if ch.name == "Test summary" {
diff --git a/src/introduction.md b/src/introduction.md
index 88666a236..d80b8e9f5 100644
--- a/src/introduction.md
+++ b/src/introduction.md
@@ -116,7 +116,7 @@ These conventions are documented here.
   See [Notation] for more detail.
 
 r[example.rule.label]
-* Rule identifiers appear before each language rule enclosed in square brackets. These identifiers provide a way to refer to a specific rule in the language. The rule identifier uses periods to separate sections from most general to most specific ([destructors.scope.nesting.function-body] for example). On narrow screens, the rule name will collapse to display `[*]`.
+* Rule identifiers appear before each language rule enclosed in square brackets. These identifiers provide a way to refer to and link to a specific rule in the language ([e.g.][example rule]). The rule identifier uses periods to separate sections from most general to most specific ([destructors.scope.nesting.function-body] for example). On narrow screens, the rule name will collapse to display `[*]`.
 
   The rule name can be clicked to link to that rule.
 
@@ -144,6 +144,7 @@ We also want the reference to be as normative as possible, so if you see anythin
 [_Expression_]: expressions.md
 [cargo book]: ../cargo/index.html
 [cargo reference]: ../cargo/reference/index.html
+[example rule]: example.rule.label
 [expressions chapter]: expressions.html
 [file an issue]: https://github.com/rust-lang/reference/issues
 [lifetime of temporaries]: expressions.html#temporaries