diff --git a/bskyweb/cmd/bskyweb/filters.go b/bskyweb/cmd/bskyweb/filters.go
new file mode 100644
index 0000000000..a92cc606bf
--- /dev/null
+++ b/bskyweb/cmd/bskyweb/filters.go
@@ -0,0 +1,28 @@
+package main
+
+import (
+ "net/url"
+
+ "github.com/flosch/pongo2/v6"
+)
+
+func init() {
+ pongo2.RegisterFilter("canonicalize_url", filterCanonicalizeURL)
+}
+
+func filterCanonicalizeURL(in *pongo2.Value, param *pongo2.Value) (*pongo2.Value, *pongo2.Error) {
+ urlStr := in.String()
+
+ parsedURL, err := url.Parse(urlStr)
+ if err != nil {
+ // If parsing fails, return the original URL
+ return in, nil
+ }
+
+ // Remove query parameters and fragment
+ parsedURL.RawQuery = ""
+ parsedURL.Fragment = ""
+
+ // Return the cleaned URL
+ return pongo2.AsValue(parsedURL.String()), nil
+}
diff --git a/bskyweb/cmd/bskyweb/filters_test.go b/bskyweb/cmd/bskyweb/filters_test.go
new file mode 100644
index 0000000000..a63ad03175
--- /dev/null
+++ b/bskyweb/cmd/bskyweb/filters_test.go
@@ -0,0 +1,61 @@
+package main
+
+import (
+ "testing"
+
+ "github.com/flosch/pongo2/v6"
+)
+
+func TestCanonicalizeURLFilter(t *testing.T) {
+ tests := []struct {
+ name string
+ input string
+ expected string
+ }{
+ {
+ name: "clean URL",
+ input: "https://bsky.app/profile/user",
+ expected: "https://bsky.app/profile/user",
+ },
+ {
+ name: "URL with query params",
+ input: "https://bsky.app/profile/user?utm_source=test",
+ expected: "https://bsky.app/profile/user",
+ },
+ {
+ name: "URL with multiple params",
+ input: "https://bsky.app/profile/user?utm_source=twitter&utm_campaign=test",
+ expected: "https://bsky.app/profile/user",
+ },
+ {
+ name: "URL with fragment",
+ input: "https://bsky.app/profile/user#section",
+ expected: "https://bsky.app/profile/user",
+ },
+ {
+ name: "URL with both params and fragment",
+ input: "https://bsky.app/profile/user?param=1#section",
+ expected: "https://bsky.app/profile/user",
+ },
+ {
+ name: "malformed URL",
+ input: "not-a-url",
+ expected: "not-a-url", // Should return original on error
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ inputValue := pongo2.AsValue(tt.input)
+ result, err := filterCanonicalizeURL(inputValue, nil)
+ if err != nil {
+ t.Errorf("filterCanonicalizeURL() error = %v", err)
+ return
+ }
+
+ if result.String() != tt.expected {
+ t.Errorf("filterCanonicalizeURL() = %v, want %v", result.String(), tt.expected)
+ }
+ })
+ }
+}
diff --git a/bskyweb/templates/post.html b/bskyweb/templates/post.html
index 963af20652..1f3f6da4e6 100644
--- a/bskyweb/templates/post.html
+++ b/bskyweb/templates/post.html
@@ -14,7 +14,7 @@
{%- if requestURI %}
-
+
{% endif -%}
{%- if postView.Author.DisplayName %}
diff --git a/bskyweb/templates/profile.html b/bskyweb/templates/profile.html
index af4180dc1f..8506a9cff7 100644
--- a/bskyweb/templates/profile.html
+++ b/bskyweb/templates/profile.html
@@ -15,7 +15,7 @@
{%- if requestURI %}
-
+
{% endif -%}
{%- if profileView.DisplayName %}