Repository: sissbruecker/linkding
Pull request #1036 (2025-04-03 19:18:24)

Bump vite from 5.4.14 to 5.4.17 in /docs
Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 5.4.14 to 5.4.17.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a href="https://github.com/vitejs/vite/releases">vite's releases</a>.</em></p>
<blockquote>
<h2>v5.4.17</h2>
<p>Please refer to <a href="https://github.com/vitejs/vite/blob/v5.4.17/packages/vite/CHANGELOG.md">CHANGELOG.md</a> for details.</p>
<h2>v5.4.16</h2>
<p>Please refer to <a href="https://github.com/vitejs/vite/blob/v5.4.16/packages/vite/CHANGELOG.md">CHANGELOG.md</a> for details.</p>
<h2>v5.4.15</h2>
<p>Please refer to <a href="https://github.com/vitejs/vite/blob/v5.4.15/packages/vite/CHANGELOG.md">CHANGELOG.md</a> for details.</p>
</blockquote>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a href="https://github.com/vitejs/vite/blob/v5.4.17/packages/vite/CHANGELOG.md">vite's changelog</a>.</em></p>
<blockquote>
<h2><!-- raw HTML omitted -->5.4.17 (2025-04-03)<!-- raw HTML omitted --></h2>
<ul>
<li>fix: backport <a href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/19782">#19782</a>, fs check with svg and relative paths (<a href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/19784">#19784</a>) (<a href="https://github.com/vitejs/vite/commit/84b2b46ed129be8215108e789a90adbb33a9c42c">84b2b46</a>), closes <a href="https://redirect.github.com/vitejs/vite/issues/19782">#19782</a> <a href="https://redirect.github.com/vitejs/vite/issues/19784">#19784</a></li>
</ul>
<h2><!-- raw HTML omitted -->5.4.16 (2025-03-31)<!-- raw HTML omitted --></h2>
<ul>
<li>fix: backport <a href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/19761">#19761</a>, fs check in transform middleware (<a href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/19762">#19762</a>) (<a href="https://github.com/vitejs/vite/commit/b627c50d359f3bd9b602408fbbf462cf4a2f019c">b627c50</a>), closes <a href="https://redirect.github.com/vitejs/vite/issues/19761">#19761</a> <a href="https://redirect.github.com/vitejs/vite/issues/19762">#19762</a></li>
</ul>
<h2><!-- raw HTML omitted -->5.4.15 (2025-03-24)<!-- raw HTML omitted --></h2>
<ul>
<li>fix: backport <a href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/19702">#19702</a>, fs raw query with query separators (<a href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/19703">#19703</a>) (<a href="https://github.com/vitejs/vite/commit/807d7f06d33ab49c48a2a3501da3eea1906c0d41">807d7f0</a>), closes <a href="https://redirect.github.com/vitejs/vite/issues/19702">#19702</a> <a href="https://redirect.github.com/vitejs/vite/issues/19703">#19703</a></li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a href="https://github.com/vitejs/vite/commit/0a2518a98d2354c61ee8ef51f7d00fa92aebb511"><code>0a2518a</code></a> release: v5.4.17</li>
<li><a href="https://github.com/vitejs/vite/commit/84b2b46ed129be8215108e789a90adbb33a9c42c"><code>84b2b46</code></a> fix: backport <a href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/19782">#19782</a>, fs check with svg and relative paths (<a href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/19784">#19784</a>)</li>
<li><a href="https://github.com/vitejs/vite/commit/712cb71aa0e2a03dbf49db92043fb4ecbfc826b1"><code>712cb71</code></a> release: v5.4.16</li>
<li><a href="https://github.com/vitejs/vite/commit/b627c50d359f3bd9b602408fbbf462cf4a2f019c"><code>b627c50</code></a> fix: backport <a href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/19761">#19761</a>, fs check in transform middleware (<a href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/19762">#19762</a>)</li>
<li><a href="https://github.com/vitejs/vite/commit/9b0f4c80eea8b136d262c705234353e96abfbe75"><code>9b0f4c8</code></a> release: v5.4.15</li>
<li><a href="https://github.com/vitejs/vite/commit/807d7f06d33ab49c48a2a3501da3eea1906c0d41"><code>807d7f0</code></a> fix: backport <a href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/19702">#19702</a>, fs raw query with query separators (<a href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/19703">#19703</a>)</li>
<li>See full diff in <a href="https://github.com/vitejs/vite/commits/v5.4.17/packages/vite">compare view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=vite&package-manager=npm_and_yarn&previous-version=5.4.14&new-version=5.4.17)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

You can trigger a rebase of this PR by commenting `@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
You can disable automated security fix PRs for this repo from the [Security Alerts page](https://github.com/sissbruecker/linkding/network/alerts).

</details>

> **Note**
> Automatic rebases have been disabled on this pull request as it has been open for over 30 days.



Patch:
diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml
index a8a1247..da1b3dc 100644
--- a/.github/workflows/build.yaml
+++ b/.github/workflows/build.yaml
@@ -19,6 +19,13 @@ jobs:
           username: ${{ vars.DOCKERHUB_USERNAME }}
           password: ${{ secrets.DOCKERHUB_TOKEN }}
 
+      - name: Login to GitHub Container Registry
+        uses: docker/login-action@v3
+        with:
+          registry: ghcr.io
+          username: ${{ github.actor }}
+          password: ${{ github.token }}
+
       - name: Set up QEMU
         uses: docker/setup-qemu-action@v3
 
@@ -34,6 +41,8 @@ jobs:
           tags: |
             sissbruecker/linkding:latest
             sissbruecker/linkding:${{ env.VERSION }}
+            ghcr.io/sissbruecker/linkding:latest
+            ghcr.io/sissbruecker/linkding:${{ env.VERSION }}
           target: linkding
           push: true
 
@@ -46,6 +55,8 @@ jobs:
           tags: |
             sissbruecker/linkding:latest-alpine
             sissbruecker/linkding:${{ env.VERSION }}-alpine
+            ghcr.io/sissbruecker/linkding:latest-alpine
+            ghcr.io/sissbruecker/linkding:${{ env.VERSION }}-alpine
           target: linkding
           push: true
 
@@ -58,6 +69,8 @@ jobs:
           tags: |
             sissbruecker/linkding:latest-plus
             sissbruecker/linkding:${{ env.VERSION }}-plus
+            ghcr.io/sissbruecker/linkding:latest-plus
+            ghcr.io/sissbruecker/linkding:${{ env.VERSION }}-plus
           target: linkding-plus
           push: true
 
@@ -70,5 +83,7 @@ jobs:
           tags: |
             sissbruecker/linkding:latest-plus-alpine
             sissbruecker/linkding:${{ env.VERSION }}-plus-alpine
+            ghcr.io/sissbruecker/linkding:latest-plus-alpine
+            ghcr.io/sissbruecker/linkding:${{ env.VERSION }}-plus-alpine
           target: linkding-plus
           push: true
\ No newline at end of file
diff --git a/bookmarks/services/auto_tagging.py b/bookmarks/services/auto_tagging.py
index 840745a..cdabab6 100644
--- a/bookmarks/services/auto_tagging.py
+++ b/bookmarks/services/auto_tagging.py
@@ -11,10 +11,18 @@ def get_tags(script: str, url: str):
         return result
 
     for line in script.lower().split("\n"):
-        if "#" in line:
-            i = line.index("#")
-            line = line[:i]
+        line = line.strip()
 
+        # Skip empty lines or lines that start with a comment
+        if not line or line.startswith("#"):
+            continue
+
+        # Remove trailing comment - only if # is preceded by whitespace
+        comment_match = re.search(r"\s+#", line)
+        if comment_match:
+            line = line[: comment_match.start()]
+
+        # Ignore lines that don't contain a URL and a tag
         parts = line.split()
         if len(parts) < 2:
             continue
@@ -36,6 +44,11 @@ def get_tags(script: str, url: str):
         ):
             continue
 
+        if parsed_pattern.fragment and not _fragment_matches(
+            parsed_pattern.fragment, parsed_url.fragment
+        ):
+            continue
+
         for tag in parts[1:]:
             result.add(tag)
 
@@ -65,3 +78,7 @@ def _qs_matches(expected_qs: str, actual_qs: str) -> bool:
                 return False
 
     return True
+
+
+def _fragment_matches(expected_fragment: str, actual_fragment: str) -> bool:
+    return actual_fragment.startswith(expected_fragment)
diff --git a/docs/package-lock.json b/docs/package-lock.json
index 7554093..85debce 100644
--- a/docs/package-lock.json
+++ b/docs/package-lock.json
@@ -408,25 +408,25 @@
       }
     },
     "node_modules/@babel/helpers": {
-      "version": "7.26.7",
-      "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.26.7.tgz",
-      "integrity": "sha512-8NHiL98vsi0mbPQmYAGWwfcFaOy4j2HY49fXJCfuDcdE7fMIsH9a7GdaeXpIBsbT7307WU8KCMp5pUVDNL4f9A==",
+      "version": "7.27.0",
+      "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.27.0.tgz",
+      "integrity": "sha512-U5eyP/CTFPuNE3qk+WZMxFkp/4zUzdceQlfzf7DdGdhp+Fezd7HD+i8Y24ZuTMKX3wQBld449jijbGq6OdGNQg==",
       "license": "MIT",
       "dependencies": {
-        "@babel/template": "^7.25.9",
-        "@babel/types": "^7.26.7"
+        "@babel/template": "^7.27.0",
+        "@babel/types": "^7.27.0"
       },
       "engines": {
         "node": ">=6.9.0"
       }
     },
     "node_modules/@babel/parser": {
-      "version": "7.26.7",
-      "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.7.tgz",
-      "integrity": "sha512-kEvgGGgEjRUutvdVvZhbn/BxVt+5VSpwXz1j3WYXQbXDo8KzFOPNG2GQbdAiNq8g6wn1yKk7C/qrke03a84V+w==",
+      "version": "7.27.0",
+      "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.0.tgz",
+      "integrity": "sha512-iaepho73/2Pz7w2eMS0Q5f83+0RKI7i4xmiYeBmDzfRVbQtTOG7Ts0S4HzJVsTMGI9keU8rNfuZr8DKfSt7Yyg==",
       "license": "MIT",
       "dependencies": {
-        "@babel/types": "^7.26.7"
+        "@babel/types": "^7.27.0"
       },
       "bin": {
         "parser": "bin/babel-parser.js"
@@ -470,14 +470,14 @@
       }
     },
     "node_modules/@babel/template": {
-      "version": "7.25.9",
-      "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.25.9.tgz",
-      "integrity": "sha512-9DGttpmPvIxBb/2uwpVo3dqJ+O6RooAFOS+lB+xDqoE2PVCE8nfoHMdZLpfCQRLwvohzXISPZcgxt80xLfsuwg==",
+      "version": "7.27.0",
+      "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.0.tgz",
+      "integrity": "sha512-2ncevenBqXI6qRMukPlXwHKHchC7RyMuu4xv5JBXRfOGVcTy1mXCD12qrp7Jsoxll1EV3+9sE4GugBVRjT2jFA==",
       "license": "MIT",
       "dependencies": {
-        "@babel/code-frame": "^7.25.9",
-        "@babel/parser": "^7.25.9",
-        "@babel/types": "^7.25.9"
+        "@babel/code-frame": "^7.26.2",
+        "@babel/parser": "^7.27.0",
+        "@babel/types": "^7.27.0"
       },
       "engines": {
         "node": ">=6.9.0"
@@ -502,9 +502,9 @@
       }
     },
     "node_modules/@babel/types": {
-      "version": "7.26.7",
-      "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.7.tgz",
-      "integrity": "sha512-t8kDRGrKXyp6+tjUh7hw2RLyclsW4TRoRvRHtSyAX9Bb5ldlFh+90YAYY6awRXrlB4G5G2izNeGySpATlFzmOg==",
+      "version": "7.27.0",
+      "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.27.0.tgz",
+      "integrity": "sha512-H45s8fVLYjbhFH62dIJ3WtmJ6RSPt/3DRO0ZcT2SUiYiQyz3BLVb9ADEnLl91m74aQPS3AzzeajZHYOalWe3bg==",
       "license": "MIT",
       "dependencies": {
         "@babel/helper-string-parser": "^7.25.9",
@@ -5946,9 +5946,10 @@
       }
     },
     "node_modules/prismjs": {
-      "version": "1.29.0",
-      "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.29.0.tgz",
-      "integrity": "sha512-Kx/1w86q/epKcmte75LNrEoT+lX8pBpavuAbvJWRXar7Hz8jrtF+e3vY751p0R8H9HdArwaCTNDDzHg/ScJK1Q==",
+      "version": "1.30.0",
+      "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.30.0.tgz",
+      "integrity": "sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw==",
+      "license": "MIT",
       "engines": {
         "node": ">=6"
       }
@@ -7123,9 +7124,9 @@
       }
     },
     "node_modules/vite": {
-      "version": "5.4.14",
-      "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.14.tgz",
-      "integrity": "sha512-EK5cY7Q1D8JNhSaPKVK4pwBFvaTmZxEnoKXLG/U9gmdDcihQGNzFlgIvaxezFR4glP1LsuiedwMBqCXH3wZccA==",
+      "version": "5.4.17",
+      "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.17.tgz",
+      "integrity": "sha512-5+VqZryDj4wgCs55o9Lp+p8GE78TLVg0lasCH5xFZ4jacZjtqZa6JUw9/p0WeAojaOfncSM6v77InkFPGnvPvg==",
       "license": "MIT",
       "dependencies": {
         "esbuild": "^0.21.3",
diff --git a/docs/src/content/docs/auto-tagging.md b/docs/src/content/docs/auto-tagging.md
index ae8edc1..cec9cdc 100644
--- a/docs/src/content/docs/auto-tagging.md
+++ b/docs/src/content/docs/auto-tagging.md
@@ -14,8 +14,8 @@ youtube.com video
 reddit.com/r/Music music reddit
 ```
 
-When a bookmark is created or updated, the URL of the bookmark is parsed to extract the hostname, path, and query
-string. These components are then compared against the patterns defined in the auto tagging rules. If all components
+When a bookmark is created or updated, the URL of the bookmark is parsed to extract the hostname, path, query
+string and fragment. These components are then compared against the patterns defined in the auto tagging rules. If all components
 match, the tags associated with the rule are added to the bookmark. Both the bookmark form in the web interface and the
 browser extension will show a preview of the tags that will be added based on the auto tagging rules.
 
@@ -33,6 +33,9 @@ The URL matching works like this:
   rule does not specify any query string parameters, it matches all query strings. If the rule specifies a query string
   it will only match if the bookmark URL contains all the specified query string parameters with their respective
   values.
+- **Fragment Matching**: The URL fragment (part after the # symbol) is also compared when present in a rule. If the rule
+  specifies a fragment, it will match any URL whose fragment starts with the specified fragment. For example, a rule with
+  `example.com/#/projects` will match URLs like `example.com/#/projects/123` or `example.com/#/projects/456`.
 
 Note that URL matching currently does not support any kind of wildcards. Rule matching only works based on the URL, not
 on the content of the website or any other aspect of the bookmark.
diff --git a/docs/src/content/docs/community.md b/docs/src/content/docs/community.md
index 8d3a86d..7e68aa6 100644
--- a/docs/src/content/docs/community.md
+++ b/docs/src/content/docs/community.md
@@ -25,6 +25,7 @@ This section lists community projects around using linkding, in alphabetical ord
 - [Linkdy](https://github.com/JGeek00/linkdy): An open-source Android and iOS client created with Flutter. Available on the [Google Play Store](https://play.google.com/store/apps/details?id=com.jgeek00.linkdy) and [App Store](https://apps.apple.com/us/app/linkdy/id6479930976). By [JGeek00](https://github.com/JGeek00).
 - [Linklater](https://github.com/danielyrovas/linklater) An open-source Android client written in Kotlin.
 - [LinkThing](https://apps.apple.com/us/app/linkthing/id1666031776) An iOS client for linkding. By [amoscardino](https://github.com/amoscardino)
+- [linktiles](https://github.com/haondt/linktiles) A web app that displays your links as tiles in a configurable mosaic. By [haondt](https://github.com/haondt)
 - [Open all links bookmarklet](https://gist.github.com/ukcuddlyguy/336dd7339e6d35fc64a75ccfc9323c66) A browser bookmarklet to open all links on the current Linkding page in new tabs. By [ukcuddlyguy](https://github.com/ukcuddlyguy)
 - [Pinkt](https://github.com/fibelatti/pinboard-kotlin) An Android client for linkding. By [fibelatti](https://github.com/fibelatti)
 - [Postman collection](https://gist.github.com/gingerbeardman/f0b42502f3bc9344e92ce63afd4360d3) a group of saved request templates for API testing. By [gingerbeardman](https://github.com/gingerbeardman)


Test patch:
diff --git a/bookmarks/tests/test_auto_tagging.py b/bookmarks/tests/test_auto_tagging.py
index b4213a1..1273264 100644
--- a/bookmarks/tests/test_auto_tagging.py
+++ b/bookmarks/tests/test_auto_tagging.py
@@ -202,3 +202,44 @@ class AutoTaggingTestCase(TestCase):
         tags = auto_tagging.get_tags(script, url)
 
         self.assertEqual(tags, {"tag1", "tag2"})
+
+    def test_auto_tag_with_url_fragment(self):
+        script = """
+            example.com/#/section/1 section1
+            example.com/#/section/2 section2
+        """
+        url = "https://example.com/#/section/1"
+
+        tags = auto_tagging.get_tags(script, url)
+
+        self.assertEqual(tags, {"section1"})
+
+    def test_auto_tag_with_url_fragment_partial_match(self):
+        script = """
+            example.com/#/section section
+        """
+        url = "https://example.com/#/section/1"
+
+        tags = auto_tagging.get_tags(script, url)
+
+        self.assertEqual(tags, {"section"})
+
+    def test_auto_tag_with_url_fragment_ignores_case(self):
+        script = """
+            example.com/#SECTION section
+        """
+        url = "https://example.com/#section"
+
+        tags = auto_tagging.get_tags(script, url)
+
+        self.assertEqual(tags, {"section"})
+
+    def test_auto_tag_with_url_fragment_and_comment(self):
+        script = """
+            example.com/#section1 section1 #This is a comment
+        """
+        url = "https://example.com/#section1"
+
+        tags = auto_tagging.get_tags(script, url)
+
+        self.assertEqual(tags, {"section1"})