Index: /trunk/src/org/openstreetmap/josm/tools/PatternUtils.java
===================================================================
--- /trunk/src/org/openstreetmap/josm/tools/PatternUtils.java	(revision 18475)
+++ /trunk/src/org/openstreetmap/josm/tools/PatternUtils.java	(revision 18475)
@@ -0,0 +1,57 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.tools;
+
+import java.util.concurrent.TimeUnit;
+import java.util.regex.Pattern;
+
+import org.apache.commons.jcs3.access.CacheAccess;
+import org.apache.commons.jcs3.engine.behavior.IElementAttributes;
+import org.openstreetmap.josm.data.cache.JCSCacheManager;
+import org.openstreetmap.josm.spi.preferences.Config;
+
+/**
+ * A class that caches compiled patterns.
+ * @author Taylor Smock
+ * @since 18475
+ */
+public final class PatternUtils {
+    /** A string that is highly unlikely to appear in regexes to split a regex from its flags */
+    private static final String MAGIC_STRING = "========";
+    /** A cache for Java Patterns (no flags) */
+    private static final CacheAccess<String, Pattern> cache = JCSCacheManager.getCache("java:pattern",
+            Config.getPref().getInt("java.pattern.cache", 1024), 0, null);
+
+    static {
+        // We don't want to keep these around forever, so set a reasonablish max idle life.
+        final IElementAttributes defaultAttributes = cache.getDefaultElementAttributes();
+        defaultAttributes.setIdleTime(TimeUnit.HOURS.toSeconds(1));
+        cache.setDefaultElementAttributes(defaultAttributes);
+    }
+
+    private PatternUtils() {
+        // Hide the constructor
+    }
+
+    /**
+     * Compile a regex into a pattern. This may return a {@link Pattern} used elsewhere. This is safe.
+     * @param regex The regex to compile
+     * @return The immutable {@link Pattern}.
+     * @see Pattern#compile(String)
+     */
+    public static Pattern compile(final String regex) {
+        return compile(regex, 0);
+    }
+
+    /**
+     * Compile a regex into a pattern. This may return a {@link Pattern} used elsewhere. This is safe.
+     * @param regex The regex to compile
+     * @param flags The flags from {@link Pattern} to apply
+     * @return The immutable {@link Pattern}.
+     * @see Pattern#compile(String, int)
+     */
+    public static Pattern compile(String regex, int flags) {
+        // Right now, the maximum value of flags is 511 (3 characters). This should avoid unnecessary array copying.
+        final StringBuilder sb = new StringBuilder(3 + MAGIC_STRING.length() + regex.length());
+        return cache.get(sb.append(flags).append(MAGIC_STRING).append(regex).toString(), () -> Pattern.compile(regex, flags));
+    }
+}
Index: /trunk/src/org/openstreetmap/josm/tools/Tag2Link.java
===================================================================
--- /trunk/src/org/openstreetmap/josm/tools/Tag2Link.java	(revision 18474)
+++ /trunk/src/org/openstreetmap/josm/tools/Tag2Link.java	(revision 18475)
@@ -11,9 +11,10 @@
 import java.util.Arrays;
 import java.util.Collections;
-import java.util.HashMap;
+import java.util.EnumMap;
 import java.util.List;
 import java.util.Map;
 import java.util.Objects;
 import java.util.Optional;
+import java.util.Set;
 import java.util.function.Supplier;
 import java.util.function.UnaryOperator;
@@ -68,4 +69,5 @@
     static final CachingProperty<List<String>> PREF_SEARCH_ENGINES = new ListProperty("tag2link.search",
             Arrays.asList("https://duckduckgo.com/?q=$1", "https://www.google.com/search?q=$1")).cached();
+    private static final Pattern PATTERN_DOLLAR_ONE = Pattern.compile("$1", Pattern.LITERAL);
 
     private Tag2Link() {
@@ -148,5 +150,5 @@
         }
 
-        final HashMap<OsmPrimitiveType, Optional<ImageResource>> memoize = new HashMap<>();
+        final Map<OsmPrimitiveType, Optional<ImageResource>> memoize = new EnumMap<>(OsmPrimitiveType.class);
         final Supplier<ImageResource> imageResource = () -> memoize
                 .computeIfAbsent(OsmPrimitiveType.NODE, type -> OsmPrimitiveImageProvider.getResource(key, value, type))
@@ -209,9 +211,20 @@
         }
 
-        wikidataRules.getValues(key).forEach(urlFormatter -> {
+        final Set<String> formatterUrls = wikidataRules.getValues(key);
+        if (!formatterUrls.isEmpty()) {
             final String formattedValue = valueFormatter.getOrDefault(key, x -> x).apply(value);
-            final String url = urlFormatter.replace("$1", formattedValue);
-            linkConsumer.acceptLink(getLinkName(url, key), url, imageResource.get());
-        });
+
+            final String urlKey = formatterUrls.stream().map(urlFormatter -> PATTERN_DOLLAR_ONE.matcher(urlFormatter)
+                            .replaceAll(Matcher.quoteReplacement("(.*)"))).map(PatternUtils::compile)
+                            .map(pattern -> pattern.matcher(value)).filter(Matcher::matches)
+                            .map(matcher -> matcher.group(1)).findFirst().orElse(formattedValue);
+
+            formatterUrls.forEach(urlFormatter -> {
+                // Check if the current value matches the formatter pattern -- some keys can take a full url or a key for
+                // the formatter. Example: https://wiki.openstreetmap.org/wiki/Key:contact:facebook
+                final String url = PATTERN_DOLLAR_ONE.matcher(urlFormatter).replaceAll(urlKey);
+                linkConsumer.acceptLink(getLinkName(url, key), url, imageResource.get());
+            });
+        }
     }
 
Index: /trunk/test/unit/org/openstreetmap/josm/tools/Tag2LinkTest.java
===================================================================
--- /trunk/test/unit/org/openstreetmap/josm/tools/Tag2LinkTest.java	(revision 18474)
+++ /trunk/test/unit/org/openstreetmap/josm/tools/Tag2LinkTest.java	(revision 18475)
@@ -1,4 +1,7 @@
 // License: GPL. For details, see LICENSE file.
 package org.openstreetmap.josm.tools;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
 
 import java.util.ArrayList;
@@ -6,6 +9,7 @@
 import java.util.List;
 
-import org.junit.Assert;
 import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
 import org.openstreetmap.josm.testutils.annotations.BasicPreferences;
 
@@ -23,5 +27,5 @@
 
     void checkLinks(String... expected) {
-        Assert.assertEquals(Arrays.asList(expected), links);
+        assertEquals(Arrays.asList(expected), this.links);
     }
 
@@ -32,5 +36,25 @@
     void testInitialize() {
         Tag2Link.initialize();
-        Assert.assertTrue("obtains at least 40 rules", Tag2Link.wikidataRules.size() > 40);
+        assertTrue(Tag2Link.wikidataRules.size() > 40, "obtains at least 40 rules");
+    }
+
+    /**
+     * Unit test for links that may come in multiple forms.
+     * Example: <a href="https://wiki.osm.org/wiki/Key:contact:facebook">https://wiki.openstreetmap.org/wiki/Key:contact:facebook</a>
+     *
+     * See also JOSM #21794
+     * @param value The tag value for "contact:facebook"
+     */
+    @ParameterizedTest
+    @ValueSource(strings = {"https://www.facebook.com/FacebookUserName", "FacebookUserName"})
+    void testUrlKeyMultipleForms(final String value) {
+        // We need the wikidata rules Since testInitialize tests initialization, reuse it.
+        if (!Tag2Link.wikidataRules.containsKey("contact:facebook")) {
+            this.testInitialize();
+        }
+        Tag2Link.getLinksForTag("contact:facebook", value, this::addLink);
+        this.checkLinks("Open unavatar.now.sh // https://unavatar.now.sh/facebook/FacebookUserName",
+                "Open facebook.com // https://www.facebook.com/FacebookUserName",
+                "Open messenger.com // https://www.messenger.com/t/FacebookUserName");
     }
 
