Repository: sissbruecker/linkding (2025-02-22 ~ 2025-03-15)

problem_statement: Accept Authorization: Bearer <Token> as well as Authorization: Token <Token>
Accepting `Authorization: Bearer <Token>` as well as `Authorization: Token <Token>` would make using API testing tools easier, as they generally have presets for Auth type Bearer. See #470 and https://learning.postman.com/docs/sending-requests/authorization/#bearer-token

> I'm not an expert, I believe using Bearer is a standard related to OAuth, and maybe JSON Web Token (JWT).

It would also allow easier use of tools and frameworks setup for that type of Auth. Example: I'm currently working with a modified version of Shiori bookmark app replacing Delicious support with Linkding support.

I guess this is somewhat related to Delicious API standard #207 

## Screenshot of Postman Auth choices
![image](https://github.com/sissbruecker/linkding/assets/49612/8a18664b-ba42-4b59-9457-b1d1db6274ea)

![image](https://github.com/sissbruecker/linkding/assets/49612/374a8784-b531-4804-85a6-33d6942d4cb0)

![image](https://github.com/sissbruecker/linkding/assets/49612/1c6ee6be-3c5d-48ab-b847-7f26d00bb668)


date: 2025-02-22
patch: diff --git a/data/.gitkeep b/data/.gitkeep
new file mode 100644
--- /dev/null
+++ b/data/.gitkeep
@@ -0,0 +1 @@
+


----

problem_statement: [Feature request] Button to refresh title/description of a bookmark
Hi Sascha,

Sometimes I want to fetch the latest title/description of bookmarks. It would be amazing if there is a button on every bookmark page which re-fetches the data. :)

_Temporary workaround: Right now I cut and then paste the url once again to trigger re-fetching._

date: 2025-03-07
patch: diff --git a/bookmarks/api/routes.py b/bookmarks/api/routes.py
index 78d9c582..aa49f4fd 100644
--- a/bookmarks/api/routes.py
+++ b/bookmarks/api/routes.py
@@ -98,12 +98,13 @@ def unarchive(self, request: HttpRequest, pk):
     @action(methods=["get"], detail=False)
     def check(self, request: HttpRequest):
         url = request.GET.get("url")
+        ignore_cache = request.GET.get("ignore_cache", False) in ["true"]
         bookmark = Bookmark.objects.filter(owner=request.user, url=url).first()
         existing_bookmark_data = (
             self.get_serializer(bookmark).data if bookmark else None
         )
 
-        metadata = website_loader.load_website_metadata(url)
+        metadata = website_loader.load_website_metadata(url, ignore_cache=ignore_cache)
 
         # Return tags that would be automatically applied to the bookmark
         profile = request.user.profile
diff --git a/bookmarks/services/bookmarks.py b/bookmarks/services/bookmarks.py
index c3e2f50b..f2b8a817 100644
--- a/bookmarks/services/bookmarks.py
+++ b/bookmarks/services/bookmarks.py
@@ -197,6 +197,17 @@ def unshare_bookmarks(bookmark_ids: [Union[int, str]], current_user: User):
     )
 
 
+def refresh_bookmarks_metadata(bookmark_ids: [Union[int, str]], current_user: User):
+    sanitized_bookmark_ids = _sanitize_id_list(bookmark_ids)
+    owned_bookmarks = Bookmark.objects.filter(
+        owner=current_user, id__in=sanitized_bookmark_ids
+    )
+
+    for bookmark in owned_bookmarks:
+        tasks.refresh_metadata(bookmark)
+        tasks.load_preview_image(current_user, bookmark)
+
+
 def _merge_bookmark_data(from_bookmark: Bookmark, to_bookmark: Bookmark):
     to_bookmark.title = from_bookmark.title
     to_bookmark.description = from_bookmark.description
diff --git a/bookmarks/services/tasks.py b/bookmarks/services/tasks.py
index 3af0014a..9c5ce615 100644
--- a/bookmarks/services/tasks.py
+++ b/bookmarks/services/tasks.py
@@ -6,6 +6,7 @@
 from django.conf import settings
 from django.contrib.auth.models import User
 from django.db.models import Q
+from django.utils import timezone
 from huey import crontab
 from huey.contrib.djhuey import HUEY as huey
 from huey.exceptions import TaskLockedException
@@ -13,7 +14,7 @@
 
 from bookmarks.models import Bookmark, BookmarkAsset, UserProfile
 from bookmarks.services import assets, favicon_loader, preview_image_loader
-from bookmarks.services.website_loader import DEFAULT_USER_AGENT
+from bookmarks.services.website_loader import DEFAULT_USER_AGENT, load_website_metadata
 
 logger = logging.getLogger(__name__)
 
@@ -225,6 +226,31 @@ def _schedule_bookmarks_without_previews_task(user_id: int):
             logging.exception(exc)
 
 
+def refresh_metadata(bookmark: Bookmark):
+    if not settings.LD_DISABLE_BACKGROUND_TASKS:
+        _refresh_metadata_task(bookmark.id)
+
+
+@task()
+def _refresh_metadata_task(bookmark_id: int):
+    try:
+        bookmark = Bookmark.objects.get(id=bookmark_id)
+    except Bookmark.DoesNotExist:
+        return
+
+    logger.info(f"Refresh metadata for bookmark. url={bookmark.url}")
+
+    metadata = load_website_metadata(bookmark.url)
+    if metadata.title:
+        bookmark.title = metadata.title
+    if metadata.description:
+        bookmark.description = metadata.description
+    bookmark.date_modified = timezone.now()
+
+    bookmark.save()
+    logger.info(f"Successfully refreshed metadata for bookmark. url={bookmark.url}")
+
+
 def is_html_snapshot_feature_active() -> bool:
     return settings.LD_ENABLE_SNAPSHOTS and not settings.LD_DISABLE_BACKGROUND_TASKS
 
diff --git a/bookmarks/services/website_loader.py b/bookmarks/services/website_loader.py
index 2134659b..e396f57f 100644
--- a/bookmarks/services/website_loader.py
+++ b/bookmarks/services/website_loader.py
@@ -27,10 +27,20 @@ def to_dict(self):
         }
 
 
+def load_website_metadata(url: str, ignore_cache: bool = False):
+    if ignore_cache:
+        return _load_website_metadata(url)
+    return _load_website_metadata_cached(url)
+
+
 # Caching metadata avoids scraping again when saving bookmarks, in case the
 # metadata was already scraped to show preview values in the bookmark form
 @lru_cache(maxsize=10)
-def load_website_metadata(url: str):
+def _load_website_metadata_cached(url: str):
+    return _load_website_metadata(url)
+
+
+def _load_website_metadata(url: str):
     title = None
     description = None
     preview_image = None
diff --git a/bookmarks/styles/bookmark-form.css b/bookmarks/styles/bookmark-form.css
index 820fab8f..6b52aa4a 100644
--- a/bookmarks/styles/bookmark-form.css
+++ b/bookmarks/styles/bookmark-form.css
@@ -15,14 +15,23 @@
     visibility: hidden;
   }
 
-  & .form-group .clear-button {
-    display: none;
+  & .form-group .suffix-button {
     padding: 0;
     border: none;
     height: auto;
     font-size: var(--font-size-sm);
   }
 
+  & .form-group .clear-button,
+  & .form-group #refresh-button {
+    display: none;
+  }
+
+  & .form-group input.modified,
+  & .form-group textarea.modified {
+    background: var(--primary-color-shade);
+  }
+
   & .form-input-hint.bookmark-exists {
     display: none;
     color: var(--warning-color);
diff --git a/bookmarks/templates/bookmarks/bulk_edit/bar.html b/bookmarks/templates/bookmarks/bulk_edit/bar.html
index 9f5d8a9c..36484839 100644
--- a/bookmarks/templates/bookmarks/bulk_edit/bar.html
+++ b/bookmarks/templates/bookmarks/bulk_edit/bar.html
@@ -22,6 +22,7 @@
           <option value="bulk_share">Share</option>
           <option value="bulk_unshare">Unshare</option>
         {% endif %}
+        <option value="bulk_refresh">Refresh from website</option>
       </select>
       <div class="tag-autocomplete d-none" ld-tag-autocomplete>
         <input name="bulk_tag_string" class="form-input input-sm" placeholder="Tag names..." variant="small">
diff --git a/bookmarks/templates/bookmarks/form.html b/bookmarks/templates/bookmarks/form.html
index 62457546..309dfc57 100644
--- a/bookmarks/templates/bookmarks/form.html
+++ b/bookmarks/templates/bookmarks/form.html
@@ -33,9 +33,12 @@
   <div class="form-group">
     <div class="d-flex justify-between align-baseline">
       <label for="{{ form.title.id_for_label }}" class="form-label">Title</label>
-      <button ld-clear-button data-for="{{ form.title.id_for_label }}" class="btn btn-link clear-button"
-              type="button">Clear
-      </button>
+      <div class="flex">
+        <button id="refresh-button" class="btn btn-link suffix-button" type="button">Refresh from website</button>
+        <button ld-clear-button data-for="{{ form.title.id_for_label }}" class="ml-2 btn btn-link suffix-button clear-button"
+                type="button">Clear
+        </button>
+      </div>
     </div>
     {{ form.title|add_class:"form-input"|attr:"autocomplete:off" }}
     {{ form.title.errors }}
@@ -43,7 +46,8 @@
   <div class="form-group">
     <div class="d-flex justify-between align-baseline">
       <label for="{{ form.description.id_for_label }}" class="form-label">Description</label>
-      <button ld-clear-button data-for="{{ form.description.id_for_label }}" class="btn btn-link clear-button"
+      <button ld-clear-button data-for="{{ form.description.id_for_label }}"
+              class="btn btn-link suffix-button clear-button"
               type="button">Clear
       </button>
     </div>
@@ -111,6 +115,7 @@
       const notesInput = document.getElementById('{{ form.notes.id_for_label }}');
       const unreadCheckbox = document.getElementById('{{ form.unread.id_for_label }}');
       const sharedCheckbox = document.getElementById('{{ form.shared.id_for_label }}');
+      const refreshButton = document.getElementById('refresh-button');
       const bookmarkExistsHint = document.querySelector('.form-input-hint.bookmark-exists');
       const editedBookmarkId = {{ form.instance.id|default:0 }};
       let isTitleModified = !!titleInput.value;
@@ -154,6 +159,7 @@
             // Display hint if URL is already bookmarked
             const existingBookmark = data.bookmark;
             bookmarkExistsHint.style['display'] = existingBookmark ? 'block' : 'none';
+            refreshButton.style['display'] = existingBookmark ? 'inline-block' : 'none';
 
             // Prefill form with existing bookmark data
             if (existingBookmark) {
@@ -193,6 +199,36 @@
           });
       }
 
+      function refreshMetadata() {
+        if (!urlInput.value) {
+          return;
+        }
+
+        toggleLoadingIcon(urlInput, true);
+
+        const websiteUrl = encodeURIComponent(urlInput.value);
+        const requestUrl = `{% url 'linkding:api-root' %}bookmarks/check?url=${websiteUrl}&ignore_cache=true`;
+
+        fetch(requestUrl)
+          .then(response => response.json())
+          .then(data => {
+            const metadata = data.metadata;
+            const existingBookmark = data.bookmark;
+            toggleLoadingIcon(urlInput, false);
+
+            if (metadata.title && metadata.title !== existingBookmark?.title) {
+              titleInput.value = metadata.title;
+              titleInput.classList.add("modified");
+            }
+
+            if (metadata.description && metadata.description !== existingBookmark?.description) {
+              descriptionInput.value = metadata.description;
+              descriptionInput.classList.add("modified");
+            }
+          });
+      }
+      refreshButton.addEventListener('click', refreshMetadata);
+
       // Fetch website metadata when page loads and when URL changes, unless we are editing an existing bookmark
       if (!editedBookmarkId) {
         checkUrl();
@@ -203,6 +239,8 @@
         descriptionInput.addEventListener('input', () => {
           isDescriptionModified = true;
         });
+      } else {
+        refreshButton.style['display'] = 'inline-block';
       }
     })();
   </script>
diff --git a/bookmarks/views/bookmarks.py b/bookmarks/views/bookmarks.py
index f6bdebb2..7d27ac51 100644
--- a/bookmarks/views/bookmarks.py
+++ b/bookmarks/views/bookmarks.py
@@ -30,6 +30,7 @@
     mark_bookmarks_as_unread,
     share_bookmarks,
     unshare_bookmarks,
+    refresh_bookmarks_metadata,
 )
 from bookmarks.type_defs import HttpRequest
 from bookmarks.utils import get_safe_return_url
@@ -348,6 +349,8 @@ def handle_action(request: HttpRequest, query: QuerySet[Bookmark] = None):
             return share_bookmarks(bookmark_ids, request.user)
         if "bulk_unshare" == bulk_action:
             return unshare_bookmarks(bookmark_ids, request.user)
+        if "bulk_refresh" == bulk_action:
+            return refresh_bookmarks_metadata(bookmark_ids, request.user)
 
 
 @login_required

test_patch: diff --git a/bookmarks/tests/test_bookmark_archived_view.py b/bookmarks/tests/test_bookmark_archived_view.py
index 2d12c992..c871d8bb 100644
--- a/bookmarks/tests/test_bookmark_archived_view.py
+++ b/bookmarks/tests/test_bookmark_archived_view.py
@@ -278,6 +278,7 @@ def test_allowed_bulk_actions(self):
             <option value="bulk_untag">Remove tags</option>
             <option value="bulk_read">Mark as read</option>
             <option value="bulk_unread">Mark as unread</option>
+            <option value="bulk_refresh">Refresh from website</option>
           </select>
         """,
             html,
@@ -303,6 +304,7 @@ def test_allowed_bulk_actions_with_sharing_enabled(self):
             <option value="bulk_unread">Mark as unread</option>
             <option value="bulk_share">Share</option>
             <option value="bulk_unshare">Unshare</option>
+            <option value="bulk_refresh">Refresh from website</option>
           </select>
         """,
             html,
diff --git a/bookmarks/tests/test_bookmark_index_view.py b/bookmarks/tests/test_bookmark_index_view.py
index deabf777..8884e03d 100644
--- a/bookmarks/tests/test_bookmark_index_view.py
+++ b/bookmarks/tests/test_bookmark_index_view.py
@@ -259,6 +259,7 @@ def test_allowed_bulk_actions(self):
             <option value="bulk_untag">Remove tags</option>
             <option value="bulk_read">Mark as read</option>
             <option value="bulk_unread">Mark as unread</option>
+            <option value="bulk_refresh">Refresh from website</option>
           </select>
         """,
             html,
@@ -284,6 +285,7 @@ def test_allowed_bulk_actions_with_sharing_enabled(self):
             <option value="bulk_unread">Mark as unread</option>
             <option value="bulk_share">Share</option>
             <option value="bulk_unshare">Unshare</option>
+            <option value="bulk_refresh">Refresh from website</option>
           </select>
         """,
             html,
diff --git a/bookmarks/tests/test_bookmarks_api.py b/bookmarks/tests/test_bookmarks_api.py
index 515e96ef..68a45107 100644
--- a/bookmarks/tests/test_bookmarks_api.py
+++ b/bookmarks/tests/test_bookmarks_api.py
@@ -1047,6 +1047,43 @@ def test_check_returns_matching_auto_tags(self):
 
         self.assertCountEqual(auto_tags, ["tag1", "tag2"])
 
+    def test_check_ignore_cache(self):
+        self.authenticate()
+
+        with patch.object(
+            website_loader, "load_website_metadata"
+        ) as mock_load_website_metadata:
+            expected_metadata = WebsiteMetadata(
+                "https://example.com",
+                "Scraped metadata",
+                "Scraped description",
+                "https://example.com/preview.png",
+            )
+            mock_load_website_metadata.return_value = expected_metadata
+
+            # Does not ignore cache by default
+            url = reverse("linkding:bookmark-check")
+            check_url = urllib.parse.quote_plus("https://example.com")
+            self.get(
+                f"{url}?url={check_url}",
+                expected_status_code=status.HTTP_200_OK,
+            )
+
+            mock_load_website_metadata.assert_called_once_with(
+                "https://example.com", ignore_cache=False
+            )
+            mock_load_website_metadata.reset_mock()
+
+            # Ignores cache based on query param
+            self.get(
+                f"{url}?url={check_url}&ignore_cache=true",
+                expected_status_code=status.HTTP_200_OK,
+            )
+
+            mock_load_website_metadata.assert_called_once_with(
+                "https://example.com", ignore_cache=True
+            )
+
     def test_can_only_access_own_bookmarks(self):
         self.authenticate()
         self.setup_bookmark()
diff --git a/bookmarks/tests/test_bookmarks_service.py b/bookmarks/tests/test_bookmarks_service.py
index 1715b888..db2c34a9 100644
--- a/bookmarks/tests/test_bookmarks_service.py
+++ b/bookmarks/tests/test_bookmarks_service.py
@@ -21,6 +21,7 @@
     share_bookmarks,
     unshare_bookmarks,
     enhance_with_website_metadata,
+    refresh_bookmarks_metadata,
 )
 from bookmarks.tests.helpers import BookmarkFactoryMixin
 
@@ -30,6 +31,21 @@ class BookmarkServiceTestCase(TestCase, BookmarkFactoryMixin):
     def setUp(self) -> None:
         self.get_or_create_test_user()
 
+        self.mock_schedule_refresh_metadata_patcher = patch(
+            "bookmarks.services.bookmarks.tasks.refresh_metadata"
+        )
+        self.mock_schedule_refresh_metadata = (
+            self.mock_schedule_refresh_metadata_patcher.start()
+        )
+        self.mock_load_preview_image_patcher = patch(
+            "bookmarks.services.bookmarks.tasks.load_preview_image"
+        )
+        self.mock_load_preview_image = self.mock_load_preview_image_patcher.start()
+
+    def tearDown(self):
+        self.mock_schedule_refresh_metadata_patcher.stop()
+        self.mock_load_preview_image_patcher.stop()
+
     def test_create_should_not_update_website_metadata(self):
         with patch.object(
             website_loader, "load_website_metadata"
@@ -891,3 +907,70 @@ def test_enhance_with_website_metadata(self):
 
             self.assertEqual("", bookmark.title)
             self.assertEqual("", bookmark.description)
+
+    def test_refresh_bookmarks_metadata(self):
+        bookmark1 = self.setup_bookmark()
+        bookmark2 = self.setup_bookmark()
+        bookmark3 = self.setup_bookmark()
+
+        refresh_bookmarks_metadata(
+            [bookmark1.id, bookmark2.id, bookmark3.id], self.get_or_create_test_user()
+        )
+
+        self.assertEqual(self.mock_schedule_refresh_metadata.call_count, 3)
+        self.assertEqual(self.mock_load_preview_image.call_count, 3)
+
+    def test_refresh_bookmarks_metadata_should_only_refresh_specified_bookmarks(self):
+        bookmark1 = self.setup_bookmark()
+        bookmark2 = self.setup_bookmark()
+        bookmark3 = self.setup_bookmark()
+
+        refresh_bookmarks_metadata(
+            [bookmark1.id, bookmark3.id], self.get_or_create_test_user()
+        )
+
+        self.assertEqual(self.mock_schedule_refresh_metadata.call_count, 2)
+        self.assertEqual(self.mock_load_preview_image.call_count, 2)
+
+        for call_args in self.mock_schedule_refresh_metadata.call_args_list:
+            args, kwargs = call_args
+            self.assertNotIn(bookmark2.id, args)
+
+        for call_args in self.mock_load_preview_image.call_args_list:
+            args, kwargs = call_args
+            self.assertNotIn(bookmark2.id, args)
+
+    def test_refresh_bookmarks_metadata_should_only_refresh_user_owned_bookmarks(self):
+        other_user = self.setup_user()
+        bookmark1 = self.setup_bookmark()
+        bookmark2 = self.setup_bookmark()
+        inaccessible_bookmark = self.setup_bookmark(user=other_user)
+
+        refresh_bookmarks_metadata(
+            [bookmark1.id, bookmark2.id, inaccessible_bookmark.id],
+            self.get_or_create_test_user(),
+        )
+
+        self.assertEqual(self.mock_schedule_refresh_metadata.call_count, 2)
+        self.assertEqual(self.mock_load_preview_image.call_count, 2)
+
+        for call_args in self.mock_schedule_refresh_metadata.call_args_list:
+            args, kwargs = call_args
+            self.assertNotIn(inaccessible_bookmark.id, args)
+
+        for call_args in self.mock_load_preview_image.call_args_list:
+            args, kwargs = call_args
+            self.assertNotIn(inaccessible_bookmark.id, args)
+
+    def test_refresh_bookmarks_metadata_should_accept_mix_of_int_and_string_ids(self):
+        bookmark1 = self.setup_bookmark()
+        bookmark2 = self.setup_bookmark()
+        bookmark3 = self.setup_bookmark()
+
+        refresh_bookmarks_metadata(
+            [str(bookmark1.id), str(bookmark2.id), bookmark3.id],
+            self.get_or_create_test_user(),
+        )
+
+        self.assertEqual(self.mock_schedule_refresh_metadata.call_count, 3)
+        self.assertEqual(self.mock_load_preview_image.call_count, 3)
diff --git a/bookmarks/tests/test_bookmarks_tasks.py b/bookmarks/tests/test_bookmarks_tasks.py
index de29f7a4..b5f36e36 100644
--- a/bookmarks/tests/test_bookmarks_tasks.py
+++ b/bookmarks/tests/test_bookmarks_tasks.py
@@ -8,6 +8,7 @@
 
 from bookmarks.models import BookmarkAsset, UserProfile
 from bookmarks.services import tasks
+from bookmarks.services.website_loader import WebsiteMetadata
 from bookmarks.tests.helpers import BookmarkFactoryMixin
 
 
@@ -615,3 +616,52 @@ def test_create_missing_html_snapshots_respects_current_user(self):
 
         self.assertEqual(count, 3)
         self.assertEqual(BookmarkAsset.objects.count(), count)
+
+    @override_settings(LD_DISABLE_BACKGROUND_TASKS=True)
+    def test_refresh_metadata_task_not_called_when_background_tasks_disabled(self):
+        bookmark = self.setup_bookmark()
+        with mock.patch(
+            "bookmarks.services.tasks._refresh_metadata_task"
+        ) as mock_refresh_metadata_task:
+            tasks.refresh_metadata(bookmark)
+            mock_refresh_metadata_task.assert_not_called()
+
+    @override_settings(LD_DISABLE_BACKGROUND_TASKS=False)
+    def test_refresh_metadata_task_called_when_background_tasks_enabled(self):
+        bookmark = self.setup_bookmark()
+        with mock.patch(
+            "bookmarks.services.tasks._refresh_metadata_task"
+        ) as mock_refresh_metadata_task:
+            tasks.refresh_metadata(bookmark)
+            mock_refresh_metadata_task.assert_called_once()
+
+    def test_refresh_metadata_task_should_handle_missing_bookmark(self):
+        with mock.patch(
+            "bookmarks.services.website_loader.load_website_metadata"
+        ) as mock_load_website_metadata:
+            tasks._refresh_metadata_task(123)
+
+            mock_load_website_metadata.assert_not_called()
+
+    def test_refresh_metadata_updates_title_description(self):
+        bookmark = self.setup_bookmark(
+            title="Initial title",
+            description="Initial description",
+        )
+        mock_website_metadata = WebsiteMetadata(
+            url=bookmark.url,
+            title="New title",
+            description="New description",
+            preview_image=None,
+        )
+
+        with mock.patch(
+            "bookmarks.services.tasks.load_website_metadata"
+        ) as mock_load_website_metadata:
+            mock_load_website_metadata.return_value = mock_website_metadata
+
+            tasks.refresh_metadata(bookmark)
+
+            bookmark.refresh_from_db()
+            self.assertEqual(bookmark.title, "New title")
+            self.assertEqual(bookmark.description, "New description")
diff --git a/bookmarks/tests/test_website_loader.py b/bookmarks/tests/test_website_loader.py
index bfb39c8a..ce2ef158 100644
--- a/bookmarks/tests/test_website_loader.py
+++ b/bookmarks/tests/test_website_loader.py
@@ -27,7 +27,7 @@ def __exit__(self, exc_type, exc_value, traceback):
 class WebsiteLoaderTestCase(TestCase):
     def setUp(self):
         # clear cached metadata before test run
-        website_loader.load_website_metadata.cache_clear()
+        website_loader._load_website_metadata_cached.cache_clear()
 
     def render_html_document(
         self, title, description="", og_description="", og_image=""
@@ -183,3 +183,20 @@ def test_load_website_metadata_prefers_description_over_og_description(self):
             metadata = website_loader.load_website_metadata("https://example.com")
             self.assertEqual("test title", metadata.title)
             self.assertEqual("test description", metadata.description)
+
+    def test_website_metadata_ignore_cache(self):
+        expected_html = '<html><head><title>Test Title</title><meta name="description" content="Test Description"><meta property="og:image" content="/images/test.jpg"></head></html>'
+
+        with mock.patch.object(
+            website_loader, "load_page", return_value=expected_html
+        ) as mock_load_page:
+            website_loader.load_website_metadata("https://example.com")
+            mock_load_page.assert_called_once()
+
+            website_loader.load_website_metadata("https://example.com")
+            mock_load_page.assert_called_once()
+
+            website_loader.load_website_metadata(
+                "https://example.com", ignore_cache=True
+            )
+            self.assertEqual(mock_load_page.call_count, 2)


----

problem_statement: [feature request] More information in the title (apart from "linkding")
Guess this could be a quick win. Right now, we have only "linkding" in the title, this is probably a static string somewhere. Nothing bad with it, but a little bit more information would be nice.

Ideas:

- "linkding #<current_tag>"
- "linkding <search term>"
- "linkding/archived", "linkding/shared", ...

Yes, I'm bookmarking linkding bookmarks. They have a nice spot in my browser's bookmark bar.

date: 2025-03-15
patch: diff --git a/bookmarks/frontend/behaviors/filter-drawer.js b/bookmarks/frontend/behaviors/filter-drawer.js
index 8645b737..e5c340e4 100644
--- a/bookmarks/frontend/behaviors/filter-drawer.js
+++ b/bookmarks/frontend/behaviors/filter-drawer.js
@@ -34,7 +34,7 @@ class FilterDrawerTriggerBehavior extends Behavior {
           </button>
         </div>
         <div class="modal-body">
-          <section class="content content-area"></div>
+          <div class="content"></div>
         </div>
       </div>    
     `;
@@ -70,13 +70,13 @@ class FilterDrawerBehavior extends ModalBehavior {
 
   teleport() {
     const content = this.element.querySelector(".content");
-    const sidePanel = document.querySelector("section.side-panel");
+    const sidePanel = document.querySelector(".side-panel");
     content.append(...sidePanel.children);
     this.mapHeading(content, "h2", "h3");
   }
 
   teleportBack() {
-    const sidePanel = document.querySelector("section.side-panel");
+    const sidePanel = document.querySelector(".side-panel");
     const content = this.element.querySelector(".content");
     sidePanel.append(...content.children);
     this.mapHeading(sidePanel, "h3", "h2");
diff --git a/bookmarks/styles/bookmark-form.css b/bookmarks/styles/bookmark-form.css
index f84842e8..820fab8f 100644
--- a/bookmarks/styles/bookmark-form.css
+++ b/bookmarks/styles/bookmark-form.css
@@ -1,5 +1,5 @@
 .bookmarks-form-page {
-  section {
+  main {
     max-width: 550px;
     margin: 0 auto;
   }
diff --git a/bookmarks/styles/bookmark-page.css b/bookmarks/styles/bookmark-page.css
index 223ec492..f3f5d7d1 100644
--- a/bookmarks/styles/bookmark-page.css
+++ b/bookmarks/styles/bookmark-page.css
@@ -30,11 +30,11 @@
   }
 
   &.collapse-side-panel {
-    section.main {
+    main {
       grid-column: span var(--grid-columns);
     }
 
-    section.side-panel {
+    .side-panel {
       display: none;
     }
 
@@ -459,7 +459,7 @@ ul.bookmark-list {
 
   /* Hide section border when bulk edit bar is opened, otherwise borders overlap in dark mode due to using contrast colors */
 
-  &.active section:first-of-type .content-area-header {
+  &.active .main .section-header {
     border-bottom-color: transparent;
   }
 
diff --git a/bookmarks/styles/components.css b/bookmarks/styles/components.css
index b3c6f878..9f4c0217 100644
--- a/bookmarks/styles/components.css
+++ b/bookmarks/styles/components.css
@@ -1,36 +1,31 @@
 /* Shared components */
 
-/* Content area component */
-section.content-area {
+/* Section header component */
+.section-header {
+  border-bottom: solid 1px var(--secondary-border-color);
+  display: flex;
+  flex-wrap: wrap;
+  column-gap: var(--unit-5);
+  padding-bottom: var(--unit-2);
+  margin-bottom: var(--unit-4);
+
+  h1,
   h2,
   h3 {
     font-size: var(--font-size-lg);
+    flex: 0 0 auto;
+    line-height: var(--unit-9);
+    margin: 0;
   }
 
-  .content-area-header {
-    border-bottom: solid 1px var(--secondary-border-color);
+  .header-controls {
+    flex: 1 1 0;
     display: flex;
-    flex-wrap: wrap;
-    column-gap: var(--unit-5);
-    padding-bottom: var(--unit-2);
-    margin-bottom: var(--unit-4);
-
-    h2,
-    h3 {
-      flex: 0 0 auto;
-      line-height: var(--unit-9);
-      margin: 0;
-    }
-
-    .header-controls {
-      flex: 1 1 0;
-      display: flex;
-    }
   }
 }
 
 @media (max-width: 600px) {
-  section.content-area .content-area-header {
+  .section-header {
     flex-direction: column;
   }
 }
diff --git a/bookmarks/styles/layout.css b/bookmarks/styles/layout.css
index 698a8dd2..bc951d16 100644
--- a/bookmarks/styles/layout.css
+++ b/bookmarks/styles/layout.css
@@ -11,18 +11,20 @@ body {
 header {
   margin-bottom: var(--unit-9);
 
-  .logo {
-    width: 28px;
-    height: 28px;
+  a.app-link:hover {
+    text-decoration: none;
   }
 
-  a:hover {
-    text-decoration: none;
+  .app-logo {
+    width: 28px;
+    height: 28px;
   }
 
-  h1 {
-    margin: 0 0 0 var(--unit-3);
+  .app-name {
+    margin-left: var(--unit-3);
     font-size: var(--font-size-lg);
+    font-weight: 500;
+    line-height: 1.2;
   }
 }
 
diff --git a/bookmarks/styles/settings.css b/bookmarks/styles/settings.css
index 0b24838c..1398538b 100644
--- a/bookmarks/styles/settings.css
+++ b/bookmarks/styles/settings.css
@@ -1,8 +1,14 @@
 .settings-page {
-  section.content-area {
+  h1 {
+    font-size: var(--font-size-xl);
+    margin-bottom: var(--unit-6);
+  }
+
+  section {
     margin-bottom: var(--unit-10);
 
     h2 {
+      font-size: var(--font-size-lg);
       margin-bottom: var(--unit-3);
     }
   }
diff --git a/bookmarks/styles/theme/variables.css b/bookmarks/styles/theme/variables.css
index 2fecf737..9eb3751d 100644
--- a/bookmarks/styles/theme/variables.css
+++ b/bookmarks/styles/theme/variables.css
@@ -87,6 +87,7 @@
   --font-size: 0.7rem;
   --font-size-sm: 0.65rem;
   --font-size-lg: 0.8rem;
+  --font-size-xl: 1rem;
   --line-height: 1rem;
 
   /* Sizes */
diff --git a/bookmarks/templates/bookmarks/archive.html b/bookmarks/templates/bookmarks/archive.html
index d7a6a222..cdf7afdb 100644
--- a/bookmarks/templates/bookmarks/archive.html
+++ b/bookmarks/templates/bookmarks/archive.html
@@ -8,9 +8,9 @@
        class="bookmarks-page grid columns-md-1 {% if bookmark_list.collapse_side_panel %}collapse-side-panel{% endif %}">
 
     {# Bookmark list #}
-    <section class="main content-area col-2">
-      <div class="content-area-header mb-0">
-        <h2>Archived bookmarks</h2>
+    <main class="main col-2" aria-labelledby="main-heading">
+      <div class="section-header mb-0">
+        <h1 id="main-heading">Archived bookmarks</h1>
         <div class="header-controls">
           {% bookmark_search bookmark_list.search mode='archived' %}
           {% include 'bookmarks/bulk_edit/toggle.html' %}
@@ -28,17 +28,19 @@ <h2>Archived bookmarks</h2>
           {% include 'bookmarks/bookmark_list.html' %}
         </div>
       </form>
-    </section>
+    </main>
 
     {# Tag cloud #}
-    <section class="side-panel content-area col-1">
-      <div class="content-area-header">
-        <h2>Tags</h2>
-      </div>
-      <div id="tag-cloud-container">
-        {% include 'bookmarks/tag_cloud.html' %}
-      </div>
-    </section>
+    <div class="side-panel col-1">
+      <section aria-labelledby="tags-heading">
+        <div class="section-header">
+          <h2 id="tags-heading">Tags</h2>
+        </div>
+        <div id="tag-cloud-container">
+          {% include 'bookmarks/tag_cloud.html' %}
+        </div>
+      </section>
+    </div>
   </div>
 {% endblock %}
 
diff --git a/bookmarks/templates/bookmarks/bookmark_list.html b/bookmarks/templates/bookmarks/bookmark_list.html
index e63fb722..39a2cbbc 100644
--- a/bookmarks/templates/bookmarks/bookmark_list.html
+++ b/bookmarks/templates/bookmarks/bookmark_list.html
@@ -5,160 +5,162 @@
 {% if bookmark_list.is_empty %}
   {% include 'bookmarks/empty_bookmarks.html' %}
 {% else %}
-  <ul class="bookmark-list{% if bookmark_list.show_notes %} show-notes{% endif %}"
-      role="list" tabindex="-1"
-      style="--ld-bookmark-description-max-lines:{{ bookmark_list.description_max_lines }};"
-      data-bookmarks-total="{{ bookmark_list.bookmarks_total }}">
-    {% for bookmark_item in bookmark_list.items %}
-      <li ld-bookmark-item data-bookmark-id="{{ bookmark_item.id }}" role="listitem"
-          {% if bookmark_item.css_classes %}class="{{ bookmark_item.css_classes }}"{% endif %}>
-        <div class="content">
-          <div class="title">
-            <label class="form-checkbox bulk-edit-checkbox">
-              <input type="checkbox" name="bookmark_id" value="{{ bookmark_item.id }}">
-              <i class="form-icon"></i>
-            </label>
-            {% if bookmark_item.favicon_file and bookmark_list.show_favicons %}
-              <img class="favicon" src="{% static bookmark_item.favicon_file %}" alt="">
-            {% endif %}
-            <a href="{{ bookmark_item.url }}" target="{{ bookmark_list.link_target }}" rel="noopener">
-              <span>{{ bookmark_item.title }}</span>
-            </a>
-          </div>
-          {% if bookmark_list.show_url %}
-            <div class="url-path truncate">
-              <a href="{{ bookmark_item.url }}" target="{{ bookmark_list.link_target }}" rel="noopener"
-                 class="url-display">
-                {{ bookmark_item.url }}
+  <section aria-label="Bookmark list">
+    <ul class="bookmark-list{% if bookmark_list.show_notes %} show-notes{% endif %}"
+        role="list" tabindex="-1"
+        style="--ld-bookmark-description-max-lines:{{ bookmark_list.description_max_lines }};"
+        data-bookmarks-total="{{ bookmark_list.bookmarks_total }}">
+      {% for bookmark_item in bookmark_list.items %}
+        <li ld-bookmark-item data-bookmark-id="{{ bookmark_item.id }}" role="listitem"
+            {% if bookmark_item.css_classes %}class="{{ bookmark_item.css_classes }}"{% endif %}>
+          <div class="content">
+            <div class="title">
+              <label class="form-checkbox bulk-edit-checkbox">
+                <input type="checkbox" name="bookmark_id" value="{{ bookmark_item.id }}">
+                <i class="form-icon"></i>
+              </label>
+              {% if bookmark_item.favicon_file and bookmark_list.show_favicons %}
+                <img class="favicon" src="{% static bookmark_item.favicon_file %}" alt="">
+              {% endif %}
+              <a href="{{ bookmark_item.url }}" target="{{ bookmark_list.link_target }}" rel="noopener">
+                <span>{{ bookmark_item.title }}</span>
               </a>
             </div>
-          {% endif %}
-          {% if bookmark_list.description_display == 'inline' %}
-            <div class="description inline truncate">
-              {% if bookmark_item.tag_names %}
-                <span class="tags">
+            {% if bookmark_list.show_url %}
+              <div class="url-path truncate">
+                <a href="{{ bookmark_item.url }}" target="{{ bookmark_list.link_target }}" rel="noopener"
+                   class="url-display">
+                  {{ bookmark_item.url }}
+                </a>
+              </div>
+            {% endif %}
+            {% if bookmark_list.description_display == 'inline' %}
+              <div class="description inline truncate">
+                {% if bookmark_item.tag_names %}
+                  <span class="tags">
                   {% for tag_name in bookmark_item.tag_names %}
                     <a href="?{% add_tag_to_query tag_name %}">{{ tag_name|hash_tag }}</a>
                   {% endfor %}
                 </span>
-              {% endif %}
-              {% if bookmark_item.tag_names and bookmark_item.description %} | {% endif %}
+                {% endif %}
+                {% if bookmark_item.tag_names and bookmark_item.description %} | {% endif %}
+                {% if bookmark_item.description %}
+                  <span>{{ bookmark_item.description }}</span>
+                {% endif %}
+              </div>
+            {% else %}
               {% if bookmark_item.description %}
-                <span>{{ bookmark_item.description }}</span>
+                <div class="description separate">{{ bookmark_item.description }}</div>
               {% endif %}
-            </div>
-          {% else %}
-            {% if bookmark_item.description %}
-              <div class="description separate">{{ bookmark_item.description }}</div>
-            {% endif %}
-            {% if bookmark_item.tag_names %}
-              <div class="tags">
-                {% for tag_name in bookmark_item.tag_names %}
-                  <a href="?{% add_tag_to_query tag_name %}">{{ tag_name|hash_tag }}</a>
-                {% endfor %}
-              </div>
-            {% endif %}
-          {% endif %}
-          {% if bookmark_item.notes %}
-            <div class="notes">
-              <div class="markdown">{% markdown bookmark_item.notes %}</div>
-            </div>
-          {% endif %}
-          <div class="actions">
-            {% if bookmark_item.display_date %}
-              {% if bookmark_item.web_archive_snapshot_url %}
-                <a href="{{ bookmark_item.web_archive_snapshot_url }}"
-                   title="Show snapshot on the Internet Archive Wayback Machine"
-                   target="{{ bookmark_list.link_target }}"
-                   rel="noopener">
-                  {{ bookmark_item.display_date }}
-                </a>
-              {% else %}
-                <span>{{ bookmark_item.display_date }}</span>
+              {% if bookmark_item.tag_names %}
+                <div class="tags">
+                  {% for tag_name in bookmark_item.tag_names %}
+                    <a href="?{% add_tag_to_query tag_name %}">{{ tag_name|hash_tag }}</a>
+                  {% endfor %}
+                </div>
               {% endif %}
-              <span>|</span>
             {% endif %}
-            {# View link is visible for both owned and shared bookmarks #}
-            {% if bookmark_list.show_view_action %}
-              <a href="{{ bookmark_item.details_url }}" class="view-action"
-                 data-turbo-action="replace" data-turbo-frame="details-modal">View</a>
+            {% if bookmark_item.notes %}
+              <div class="notes">
+                <div class="markdown">{% markdown bookmark_item.notes %}</div>
+              </div>
             {% endif %}
-            {% if bookmark_item.is_editable %}
-              {# Bookmark owner actions #}
-              {% if bookmark_list.show_edit_action %}
-                <a href="{% url 'linkding:bookmarks.edit' bookmark_item.id %}?return_url={{ bookmark_list.return_url|urlencode }}">Edit</a>
-              {% endif %}
-              {% if bookmark_list.show_archive_action %}
-                {% if bookmark_item.is_archived %}
-                  <button type="submit" name="unarchive" value="{{ bookmark_item.id }}"
-                          class="btn btn-link btn-sm">Unarchive
-                  </button>
+            <div class="actions">
+              {% if bookmark_item.display_date %}
+                {% if bookmark_item.web_archive_snapshot_url %}
+                  <a href="{{ bookmark_item.web_archive_snapshot_url }}"
+                     title="Show snapshot on the Internet Archive Wayback Machine"
+                     target="{{ bookmark_list.link_target }}"
+                     rel="noopener">
+                    {{ bookmark_item.display_date }}
+                  </a>
                 {% else %}
-                  <button type="submit" name="archive" value="{{ bookmark_item.id }}"
-                          class="btn btn-link btn-sm">Archive
-                  </button>
+                  <span>{{ bookmark_item.display_date }}</span>
                 {% endif %}
+                <span>|</span>
               {% endif %}
-              {% if bookmark_list.show_remove_action %}
-                <button ld-confirm-button type="submit" name="remove" value="{{ bookmark_item.id }}"
-                        class="btn btn-link btn-sm">Remove
-                </button>
+              {# View link is visible for both owned and shared bookmarks #}
+              {% if bookmark_list.show_view_action %}
+                <a href="{{ bookmark_item.details_url }}" class="view-action"
+                   data-turbo-action="replace" data-turbo-frame="details-modal">View</a>
               {% endif %}
-            {% else %}
-              {# Shared bookmark actions #}
-              <span>Shared by
-                <a href="?{% replace_query_param user=bookmark_item.owner.username %}">{{ bookmark_item.owner.username }}</a>
-              </span>
-            {% endif %}
-            {% if bookmark_item.has_extra_actions %}
-              <div class="extra-actions">
-                <span class="hide-sm">|</span>
-                {% if bookmark_item.show_mark_as_read %}
-                  <button type="submit" name="mark_as_read" value="{{ bookmark_item.id }}"
-                          class="btn btn-link btn-sm btn-icon"
-                          ld-confirm-button ld-confirm-icon="ld-icon-read" ld-confirm-question="Mark as read?">
-                    <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16">
-                      <use xlink:href="#ld-icon-unread"></use>
-                    </svg>
-                    Unread
-                  </button>
+              {% if bookmark_item.is_editable %}
+                {# Bookmark owner actions #}
+                {% if bookmark_list.show_edit_action %}
+                  <a href="{% url 'linkding:bookmarks.edit' bookmark_item.id %}?return_url={{ bookmark_list.return_url|urlencode }}">Edit</a>
                 {% endif %}
-                {% if bookmark_item.show_unshare %}
-                  <button type="submit" name="unshare" value="{{ bookmark_item.id }}"
-                          class="btn btn-link btn-sm btn-icon"
-                          ld-confirm-button ld-confirm-icon="ld-icon-unshare" ld-confirm-question="Unshare?">
-                    <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16">
-                      <use xlink:href="#ld-icon-share"></use>
-                    </svg>
-                    Shared
-                  </button>
+                {% if bookmark_list.show_archive_action %}
+                  {% if bookmark_item.is_archived %}
+                    <button type="submit" name="unarchive" value="{{ bookmark_item.id }}"
+                            class="btn btn-link btn-sm">Unarchive
+                    </button>
+                  {% else %}
+                    <button type="submit" name="archive" value="{{ bookmark_item.id }}"
+                            class="btn btn-link btn-sm">Archive
+                    </button>
+                  {% endif %}
                 {% endif %}
-                {% if bookmark_item.show_notes_button %}
-                  <button type="button" class="btn btn-link btn-sm btn-icon toggle-notes">
-                    <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16">
-                      <use xlink:href="#ld-icon-note"></use>
-                    </svg>
-                    Notes
+                {% if bookmark_list.show_remove_action %}
+                  <button ld-confirm-button type="submit" name="remove" value="{{ bookmark_item.id }}"
+                          class="btn btn-link btn-sm">Remove
                   </button>
                 {% endif %}
+              {% else %}
+                {# Shared bookmark actions #}
+                <span>Shared by
+                <a href="?{% replace_query_param user=bookmark_item.owner.username %}">{{ bookmark_item.owner.username }}</a>
+              </span>
+              {% endif %}
+              {% if bookmark_item.has_extra_actions %}
+                <div class="extra-actions">
+                  <span class="hide-sm">|</span>
+                  {% if bookmark_item.show_mark_as_read %}
+                    <button type="submit" name="mark_as_read" value="{{ bookmark_item.id }}"
+                            class="btn btn-link btn-sm btn-icon"
+                            ld-confirm-button ld-confirm-icon="ld-icon-read" ld-confirm-question="Mark as read?">
+                      <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16">
+                        <use xlink:href="#ld-icon-unread"></use>
+                      </svg>
+                      Unread
+                    </button>
+                  {% endif %}
+                  {% if bookmark_item.show_unshare %}
+                    <button type="submit" name="unshare" value="{{ bookmark_item.id }}"
+                            class="btn btn-link btn-sm btn-icon"
+                            ld-confirm-button ld-confirm-icon="ld-icon-unshare" ld-confirm-question="Unshare?">
+                      <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16">
+                        <use xlink:href="#ld-icon-share"></use>
+                      </svg>
+                      Shared
+                    </button>
+                  {% endif %}
+                  {% if bookmark_item.show_notes_button %}
+                    <button type="button" class="btn btn-link btn-sm btn-icon toggle-notes">
+                      <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16">
+                        <use xlink:href="#ld-icon-note"></use>
+                      </svg>
+                      Notes
+                    </button>
+                  {% endif %}
+                </div>
+              {% endif %}
+            </div>
+          </div>
+          {% if bookmark_list.show_preview_images %}
+            {% if bookmark_item.preview_image_file %}
+              <img class="preview-image" src="{% static bookmark_item.preview_image_file %}" loading="lazy"/>
+            {% else %}
+              <div class="preview-image placeholder">
+                <div class="img"/>
               </div>
             {% endif %}
-          </div>
-        </div>
-        {% if bookmark_list.show_preview_images %}
-          {% if bookmark_item.preview_image_file %}
-            <img class="preview-image" src="{% static bookmark_item.preview_image_file %}" loading="lazy"/>
-          {% else %}
-            <div class="preview-image placeholder">
-              <div class="img"/>
-            </div>
           {% endif %}
-        {% endif %}
-      </li>
-    {% endfor %}
-  </ul>
+        </li>
+      {% endfor %}
+    </ul>
 
-  <div class="bookmark-pagination{% if request.user_profile.sticky_pagination %} sticky{% endif %}">
-    {% pagination bookmark_list.bookmarks_page %}
-  </div>
+    <div class="bookmark-pagination{% if request.user_profile.sticky_pagination %} sticky{% endif %}">
+      {% pagination bookmark_list.bookmarks_page %}
+    </div>
+  </section>
 {% endif %}
diff --git a/bookmarks/templates/bookmarks/edit.html b/bookmarks/templates/bookmarks/edit.html
index 4e1b7664..cbf0ddc3 100644
--- a/bookmarks/templates/bookmarks/edit.html
+++ b/bookmarks/templates/bookmarks/edit.html
@@ -1,15 +1,21 @@
 {% extends 'bookmarks/layout.html' %}
 
+{% block head %}
+  {% with page_title="Edit bookmark - Linkding" %}
+    {{ block.super }}
+  {% endwith %}
+{% endblock %}
+
 {% block content %}
   <div class="bookmarks-form-page">
-    <section class="content-area">
-      <div class="content-area-header">
-        <h2>Edit bookmark</h2>
+    <main aria-labelledby="main-heading">
+      <div class="section-header">
+        <h1 id="main-heading">Edit bookmark</h1>
       </div>
       <form action="{% url 'linkding:bookmarks.edit' bookmark_id %}?return_url={{ return_url|urlencode }}" method="post"
             novalidate>
         {% include 'bookmarks/form.html' %}
       </form>
-    </section>
+    </main>
   </div>
 {% endblock %}
diff --git a/bookmarks/templates/bookmarks/head.html b/bookmarks/templates/bookmarks/head.html
index 81b9c17f..1f64e9d9 100644
--- a/bookmarks/templates/bookmarks/head.html
+++ b/bookmarks/templates/bookmarks/head.html
@@ -12,7 +12,7 @@
   <meta name="description" content="Self-hosted bookmark service">
   <meta name="robots" content="index,follow">
   <meta name="author" content="Sascha Ißbrücker">
-  <title>linkding</title>
+  <title>{{ page_title|default:'Linkding' }}</title>
   {# Include specific theme variant based on user profile setting #}
   {% if request.user_profile.theme == 'light' %}
     <link href="{% static 'theme-light.css' %}?v={{ app_version }}" rel="stylesheet" type="text/css"/>
diff --git a/bookmarks/templates/bookmarks/index.html b/bookmarks/templates/bookmarks/index.html
index e17f5e6c..93c4f7d6 100644
--- a/bookmarks/templates/bookmarks/index.html
+++ b/bookmarks/templates/bookmarks/index.html
@@ -3,14 +3,16 @@
 {% load shared %}
 {% load bookmarks %}
 
+{% block title %}Bookmarks - Linkding{% endblock %}
+
 {% block content %}
   <div ld-bulk-edit
        class="bookmarks-page grid columns-md-1 {% if bookmark_list.collapse_side_panel %}collapse-side-panel{% endif %}">
 
     {# Bookmark list #}
-    <section class="main content-area col-2">
-      <div class="content-area-header mb-0">
-        <h2>Bookmarks</h2>
+    <main class="main col-2" aria-labelledby="main-heading">
+      <div class="section-header mb-0">
+        <h1 id="main-heading">Bookmarks</h1>
         <div class="header-controls">
           {% bookmark_search bookmark_list.search %}
           {% include 'bookmarks/bulk_edit/toggle.html' %}
@@ -28,17 +30,19 @@ <h2>Bookmarks</h2>
           {% include 'bookmarks/bookmark_list.html' %}
         </div>
       </form>
-    </section>
+    </main>
 
     {# Tag cloud #}
-    <section class="side-panel content-area col-1">
-      <div class="content-area-header">
-        <h2>Tags</h2>
-      </div>
-      <div id="tag-cloud-container">
-        {% include 'bookmarks/tag_cloud.html' %}
-      </div>
-    </section>
+    <div class="side-panel col-1">
+      <section aria-labelledby="tags-heading">
+        <div class="section-header">
+          <h2 id="tags-heading">Tags</h2>
+        </div>
+        <div id="tag-cloud-container">
+          {% include 'bookmarks/tag_cloud.html' %}
+        </div>
+      </section>
+    </div>
   </div>
 {% endblock %}
 
diff --git a/bookmarks/templates/bookmarks/layout.html b/bookmarks/templates/bookmarks/layout.html
index fd42b813..9508cf62 100644
--- a/bookmarks/templates/bookmarks/layout.html
+++ b/bookmarks/templates/bookmarks/layout.html
@@ -3,7 +3,7 @@
 <!DOCTYPE html>
 {# Use data attributes as storage for access in static scripts #}
 <html lang="en" data-api-base-url="{% url 'linkding:api-root' %}">
-{% include 'bookmarks/head.html' %}
+{% block head %}{% include 'bookmarks/head.html' %}{% endblock %}
 <body ld-global-shortcuts>
 
 <div class="d-none">
@@ -80,17 +80,19 @@
     </div>
   {% endif %}
   <div class="d-flex justify-between">
-    <a href="{% url 'linkding:root' %}" class="d-flex align-center">
-      <img class="logo" src="{% static 'logo.png' %}" alt="Application logo">
-      <h1>LINKDING</h1>
+    <a href="{% url 'linkding:root' %}" class="app-link d-flex align-center">
+      <img class="app-logo" src="{% static 'logo.png' %}" alt="Application logo">
+      <span class="app-name">LINKDING</span>
     </a>
-    {% if request.user.is_authenticated %}
-      {# Only show nav items menu when logged in #}
-      {% include 'bookmarks/nav_menu.html' %}
-    {% else %}
-      {# Otherwise show login link #}
-      <a href="{% url 'login' %}" class="btn btn-link">Login</a>
-    {% endif %}
+    <nav>
+      {% if request.user.is_authenticated %}
+        {# Only show nav items menu when logged in #}
+        {% include 'bookmarks/nav_menu.html' %}
+      {% else %}
+        {# Otherwise show login link #}
+        <a href="{% url 'login' %}" class="btn btn-link">Login</a>
+      {% endif %}
+    </nav>
   </div>
 </header>
 <div class="content container">
diff --git a/bookmarks/templates/bookmarks/nav_menu.html b/bookmarks/templates/bookmarks/nav_menu.html
index c9a9d9be..f50edf75 100644
--- a/bookmarks/templates/bookmarks/nav_menu.html
+++ b/bookmarks/templates/bookmarks/nav_menu.html
@@ -27,7 +27,24 @@
         </li>
       </ul>
     </div>
-    <a href="{% url 'linkding:settings.index' %}" class="btn btn-link">Settings</a>
+    <div ld-dropdown class="dropdown">
+      <button class="btn btn-link dropdown-toggle" tabindex="0">
+        Settings
+      </button>
+      <ul class="menu" role="list" tabindex="-1">
+        <li class="menu-item">
+          <a href="{% url 'linkding:settings.general' %}" class="menu-link">General</a>
+        </li>
+        <li class="menu-item">
+          <a href="{% url 'linkding:settings.integrations' %}" class="menu-link">Integrations</a>
+        </li>
+        {% if request.user.is_superuser %}
+          <li class="menu-item">
+            <a href="{% url 'admin:index' %}" class="menu-link" data-turbo="false">Admin</a>
+          </li>
+        {% endif %}
+      </ul>
+    </div>
     <form class="d-inline" action="{% url 'logout' %}" method="post" data-turbo="false">
       {% csrf_token %}
       <button type="submit" class="btn btn-link">Logout</button>
@@ -54,11 +71,11 @@
           <a href="{% url 'linkding:bookmarks.index' %}" class="menu-link">Bookmarks</a>
         </li>
         <li class="menu-item">
-          <a href="{% url 'linkding:bookmarks.archived' %}" class="menu-link">Archived</a>
+          <a href="{% url 'linkding:bookmarks.archived' %}" class="menu-link">Archived bookmarks</a>
         </li>
         {% if request.user_profile.enable_sharing %}
           <li class="menu-item">
-            <a href="{% url 'linkding:bookmarks.shared' %}" class="menu-link">Shared</a>
+            <a href="{% url 'linkding:bookmarks.shared' %}" class="menu-link">Shared bookmarks</a>
           </li>
         {% endif %}
         <li class="menu-item">
@@ -69,8 +86,17 @@
         </li>
         <div class="divider"></div>
         <li class="menu-item">
-          <a href="{% url 'linkding:settings.index' %}" class="menu-link">Settings</a>
+          <a href="{% url 'linkding:settings.general' %}" class="menu-link">Settings</a>
+        </li>
+        <li class="menu-item">
+          <a href="{% url 'linkding:settings.integrations' %}" class="menu-link">Integrations</a>
         </li>
+        {% if request.user.is_superuser %}
+          <li class="menu-item">
+            <a href="{% url 'admin:index' %}" class="menu-link" data-turbo="false">Admin</a>
+          </li>
+        {% endif %}
+        <div class="divider"></div>
         <li class="menu-item">
           <form class="d-inline" action="{% url 'logout' %}" method="post" data-turbo="false">
             {% csrf_token %}
diff --git a/bookmarks/templates/bookmarks/new.html b/bookmarks/templates/bookmarks/new.html
index 6fa27a86..43fa7df0 100644
--- a/bookmarks/templates/bookmarks/new.html
+++ b/bookmarks/templates/bookmarks/new.html
@@ -1,14 +1,20 @@
 {% extends 'bookmarks/layout.html' %}
 
+{% block head %}
+  {% with page_title="New bookmark - Linkding" %}
+    {{ block.super }}
+  {% endwith %}
+{% endblock %}
+
 {% block content %}
   <div class="bookmarks-form-page">
-    <section class="content-area">
-      <div class="content-area-header">
-        <h2>New bookmark</h2>
+    <main aria-labelledby="main-heading">
+      <div class="section-header">
+        <h1 id="main-heading">New bookmark</h1>
       </div>
       <form action="{% url 'linkding:bookmarks.new' %}" method="post" novalidate>
         {% include 'bookmarks/form.html' %}
       </form>
-    </section>
+    </main>
   </div>
 {% endblock %}
diff --git a/bookmarks/templates/bookmarks/shared.html b/bookmarks/templates/bookmarks/shared.html
index bc40d061..b60e1590 100644
--- a/bookmarks/templates/bookmarks/shared.html
+++ b/bookmarks/templates/bookmarks/shared.html
@@ -8,9 +8,9 @@
       class="bookmarks-page grid columns-md-1 {% if bookmark_list.collapse_side_panel %}collapse-side-panel{% endif %}">
 
     {# Bookmark list #}
-    <section class="main content-area col-2">
-      <div class="content-area-header">
-        <h2>Shared bookmarks</h2>
+    <main class="main col-2" aria-labelledby="main-heading">
+      <div class="section-header">
+        <h1 id="main-heading">Shared bookmarks</h1>
         <div class="header-controls">
           {% bookmark_search bookmark_list.search mode='shared' %}
           <button ld-filter-drawer-trigger class="btn ml-2">Filters</button>
@@ -25,24 +25,28 @@ <h2>Shared bookmarks</h2>
           {% include 'bookmarks/bookmark_list.html' %}
         </div>
       </form>
-    </section>
+    </main>
 
     {# Filters #}
-    <section class="side-panel content-area col-1">
-      <div class="content-area-header">
-        <h2>User</h2>
-      </div>
-      <div>
-        {% user_select bookmark_list.search users %}
-        <br>
-      </div>
-      <div class="content-area-header">
-        <h2>Tags</h2>
-      </div>
-      <div id="tag-cloud-container">
-        {% include 'bookmarks/tag_cloud.html' %}
-      </div>
-    </section>
+    <div class="side-panel col-1">
+      <section aria-labelledby="user-heading">
+        <div class="section-header">
+          <h2 id="user-heading">User</h2>
+        </div>
+        <div>
+          {% user_select bookmark_list.search users %}
+          <br>
+        </div>
+      </section>
+      <section aria-labelledby="tags-heading">
+        <div class="section-header">
+          <h2 id="tags-heading">Tags</h2>
+        </div>
+        <div id="tag-cloud-container">
+          {% include 'bookmarks/tag_cloud.html' %}
+        </div>
+      </section>
+    </div>
   </div>
 {% endblock %}
 
diff --git a/bookmarks/templates/django_registration/registration_complete.html b/bookmarks/templates/django_registration/registration_complete.html
index fd727ed5..041566df 100644
--- a/bookmarks/templates/django_registration/registration_complete.html
+++ b/bookmarks/templates/django_registration/registration_complete.html
@@ -1,8 +1,19 @@
 {% extends 'bookmarks/layout.html' %}
 {% load widget_tweaks %}
 
-{% block title %}Registration complete{% endblock %}
+{% block head %}
+  {% with page_title="Registration complete - Linkding" %}
+    {{ block.super }}
+  {% endwith %}
+{% endblock %}
 
 {% block content %}
-  <p>Registration complete. You can now use the application.</p>
+  <main class="mx-auto width-50 width-md-100" aria-labelledby="main-heading">
+    <div class="section-header">
+      <h1 id="main-heading">Registration complete</h1>
+    </div>
+    <p class="text-success">
+      You can now use the application.
+    </p>
+  </main>
 {% endblock %}
diff --git a/bookmarks/templates/django_registration/registration_form.html b/bookmarks/templates/django_registration/registration_form.html
index dba0f54c..b02f25e4 100644
--- a/bookmarks/templates/django_registration/registration_form.html
+++ b/bookmarks/templates/django_registration/registration_form.html
@@ -1,12 +1,16 @@
 {% extends 'bookmarks/layout.html' %}
 {% load widget_tweaks %}
 
-{% block title %}Registration{% endblock %}
+{% block head %}
+  {% with page_title="Registration - Linkding" %}
+    {{ block.super }}
+  {% endwith %}
+{% endblock %}
 
 {% block content %}
-  <section class="content-area mx-auto width-50 width-md-100">
-    <div class="content-area-header">
-      <h2>Register</h2>
+  <main class="mx-auto width-50 width-md-100" aria-labelledby="main-heading">
+    <div class="section-header">
+      <h1 id="main-heading">Register</h1>
     </div>
     <form method="post" action="{% url 'django_registration_register' %}" novalidate>
       {% csrf_token %}
@@ -34,5 +38,5 @@ <h2>Register</h2>
       <input type="submit" value="Register" class="btn btn-primary btn-wide">
       <input type="hidden" name="next" value="{{ next }}">
     </form>
-  </section>
+  </main>
 {% endblock %}
diff --git a/bookmarks/templates/registration/login.html b/bookmarks/templates/registration/login.html
index 9b69a606..a94e63f2 100644
--- a/bookmarks/templates/registration/login.html
+++ b/bookmarks/templates/registration/login.html
@@ -1,12 +1,16 @@
 {% extends 'bookmarks/layout.html' %}
 {% load widget_tweaks %}
 
-{% block title %}Login{% endblock %}
+{% block head %}
+  {% with page_title="Login - Linkding" %}
+    {{ block.super }}
+  {% endwith %}
+{% endblock %}
 
 {% block content %}
-  <section class="content-area mx-auto width-50 width-md-100">
-    <div class="content-area-header">
-      <h2>Login</h2>
+  <main class="mx-auto width-50 width-md-100" aria-labelledby="main-heading">
+    <div class="section-header">
+      <h1 id="main-heading">Login</h1>
     </div>
     <form method="post" action="{% url 'login' %}">
       {% csrf_token %}
@@ -36,5 +40,5 @@ <h2>Login</h2>
         {% endif %}
       </div>
     </form>
-  </section>
+  </main>
 {% endblock %}
diff --git a/bookmarks/templates/registration/password_change_done.html b/bookmarks/templates/registration/password_change_done.html
index 15c1bd8d..4b47e4f0 100644
--- a/bookmarks/templates/registration/password_change_done.html
+++ b/bookmarks/templates/registration/password_change_done.html
@@ -1,15 +1,19 @@
 {% extends 'bookmarks/layout.html' %}
 {% load widget_tweaks %}
 
-{% block title %}Password changed{% endblock %}
+{% block head %}
+  {% with page_title="Password changed - Linkding" %}
+    {{ block.super }}
+  {% endwith %}
+{% endblock %}
 
 {% block content %}
-  <section class="content-area mx-auto width-50 width-md-100">
-    <div class="content-area-header">
-      <h2>Password Changed</h2>
+  <main class="mx-auto width-50 width-md-100" aria-labelledby="main-heading">
+    <div class="section-header">
+      <h1 id="main-heading">Password Changed</h1>
     </div>
     <p class="text-success">
       Your password was changed successfully.
     </p>
-  </section>
+  </main>
 {% endblock %}
diff --git a/bookmarks/templates/registration/password_change_form.html b/bookmarks/templates/registration/password_change_form.html
index b4aedcd6..ebf42a9c 100644
--- a/bookmarks/templates/registration/password_change_form.html
+++ b/bookmarks/templates/registration/password_change_form.html
@@ -1,12 +1,16 @@
 {% extends 'bookmarks/layout.html' %}
 {% load widget_tweaks %}
 
-{% block title %}Change Password{% endblock %}
+{% block head %}
+  {% with page_title="Change password - Linkding" %}
+    {{ block.super }}
+  {% endwith %}
+{% endblock %}
 
 {% block content %}
-  <section class="content-area mx-auto width-50 width-md-100">
-    <div class="content-area-header">
-      <h2>Change Password</h2>
+  <main class="mx-auto width-50 width-md-100" aria-labelledby="main-heading">
+    <div class="section-header">
+      <h1 id="main-heading">Change Password</h1>
     </div>
     <form method="post" action="{% url 'change_password' %}">
       {% csrf_token %}
@@ -41,5 +45,5 @@ <h2>Change Password</h2>
       <br/>
       <input type="submit" value="Change Password" class="btn btn-primary btn-wide">
     </form>
-  </section>
+  </main>
 {% endblock %}
diff --git a/bookmarks/templates/settings/general.html b/bookmarks/templates/settings/general.html
index 7a1cde8c..c8cfc6d0 100644
--- a/bookmarks/templates/settings/general.html
+++ b/bookmarks/templates/settings/general.html
@@ -1,10 +1,15 @@
 {% extends "bookmarks/layout.html" %}
 {% load widget_tweaks %}
 
-{% block content %}
-  <div class="settings-page">
+{% block head %}
+  {% with page_title="Settings - Linkding" %}
+    {{ block.super }}
+  {% endwith %}
+{% endblock %}
 
-    {% include 'settings/nav.html' %}
+{% block content %}
+  <main class="settings-page" aria-labelledby="main-heading">
+    <h1 id="main-heading">Settings</h1>
 
     {# Profile section #}
     {% if success_message %}
@@ -14,8 +19,8 @@
       <div class="toast toast-error mb-4">{{ error_message }}</div>
     {% endif %}
 
-    <section class="content-area">
-      <h2>Profile</h2>
+    <section aria-labelledby="profile-heading">
+      <h2 id="profile-heading">Profile</h2>
       <p>
         <a href="{% url 'change_password' %}">Change password</a>
       </p>
@@ -278,8 +283,8 @@ <h2>Profile</h2>
 
     {# Global settings section #}
     {% if global_settings_form %}
-      <section class="content-area">
-        <h2>Global settings</h2>
+      <section aria-labelledby="global-settings-heading">
+        <h2 id="global-settings-heading">Global settings</h2>
         <form action="{% url 'linkding:settings.update' %}" method="post" novalidate data-turbo="false">
           {% csrf_token %}
           <div class="form-group">
@@ -318,8 +323,8 @@ <h2>Global settings</h2>
     {% endif %}
 
     {# Import section #}
-    <section class="content-area">
-      <h2>Import</h2>
+    <section aria-labelledby="import-heading">
+      <h2 id="import-heading">Import</h2>
       <p>Import bookmarks and tags in the Netscape HTML format. This will execute a sync where new bookmarks are
         added and existing ones are updated.</p>
       <form method="post" enctype="multipart/form-data" action="{% url 'linkding:settings.import' %}">
@@ -346,8 +351,8 @@ <h2>Import</h2>
     </section>
 
     {# Export section #}
-    <section class="content-area">
-      <h2>Export</h2>
+    <section aria-labelledby="export-heading">
+      <h2 id="export-heading">Export</h2>
       <p>Export all bookmarks in Netscape HTML format.</p>
       <a class="btn btn-primary" target="_blank" href="{% url 'linkding:settings.export' %}">Download (.html)</a>
       {% if export_error %}
@@ -360,8 +365,8 @@ <h2>Export</h2>
     </section>
 
     {# About section #}
-    <section class="content-area about">
-      <h2>About</h2>
+    <section class="about" aria-labelledby="about-heading">
+      <h2 id="about-heading">About</h2>
       <table class="table">
         <tbody>
         <tr>
@@ -384,7 +389,7 @@ <h2>About</h2>
         </tbody>
       </table>
     </section>
-  </div>
+  </main>
 
   <script>
     (function init() {
diff --git a/bookmarks/templates/settings/integrations.html b/bookmarks/templates/settings/integrations.html
index 66d9d6d5..de01b0bc 100644
--- a/bookmarks/templates/settings/integrations.html
+++ b/bookmarks/templates/settings/integrations.html
@@ -1,12 +1,17 @@
 {% extends "bookmarks/layout.html" %}
 
-{% block content %}
-  <div class="settings-page">
+{% block head %}
+  {% with page_title="Integrations - Linkding" %}
+    {{ block.super }}
+  {% endwith %}
+{% endblock %}
 
-    {% include 'settings/nav.html' %}
+{% block content %}
+  <main class="settings-page" aria-labelledby="main-heading">
+    <h1 id="main-heading">Integrations</h1>
 
-    <section class="content-area">
-      <h2>Browser Extension</h2>
+    <section aria-labelledby="browser-extension-heading">
+      <h2 id="browser-extension-heading">Browser Extension</h2>
       <p>The browser extension allows you to quickly add new bookmarks without leaving the page that you are on. The
         extension is available in the official extension stores for:</p>
       <ul>
@@ -31,8 +36,8 @@ <h2>Bookmarklet</h2>
          class="btn btn-primary">📎 Add bookmark</a>
     </section>
 
-    <section class="content-area">
-      <h2>REST API</h2>
+    <section aria-labelledby="rest-api-heading">
+      <h2 id="rest-api-heading">REST API</h2>
       <p>The following token can be used to authenticate 3rd-party applications against the REST API:</p>
       <div class="form-group">
         <div class="width-50 width-md-100">
@@ -48,8 +53,8 @@ <h2>REST API</h2>
       </p>
     </section>
 
-    <section class="content-area">
-      <h2>RSS Feeds</h2>
+    <section aria-labelledby="rss-feeds-heading">
+      <h2 id="rss-feeds-heading">RSS Feeds</h2>
       <p>The following URLs provide RSS feeds for your bookmarks:</p>
       <ul style="list-style-position: outside;">
         <li><a target="_blank" href="{{ all_feed_url }}">All bookmarks</a></li>
@@ -84,5 +89,5 @@ <h2>RSS Feeds</h2>
         After deleting the feed token, new URLs will be generated when you reload this settings page.
       </p>
     </section>
-  </div>
+  </main>
 {% endblock %}
diff --git a/bookmarks/templates/settings/nav.html b/bookmarks/templates/settings/nav.html
deleted file mode 100644
index 76d60334..00000000
--- a/bookmarks/templates/settings/nav.html
+++ /dev/null
@@ -1,24 +0,0 @@
-{% url 'linkding:settings.index' as index_url %}
-{% url 'linkding:settings.general' as general_url %}
-{% url 'linkding:settings.integrations' as integrations_url %}
-
-<ul class="tab tab-block">
-    <li class="tab-item {% if request.get_full_path == index_url or request.get_full_path == general_url%}active{% endif %}">
-        <a href="{{ general_url }}">General</a>
-    </li>
-    <li class="tab-item {% if request.get_full_path == integrations_url %}active{% endif %}">
-        <a href="{{ integrations_url }}">Integrations</a>
-    </li>
-    {% if request.user.is_superuser %}
-    <li class="tab-item">
-        <a href="{% url 'admin:index' %}" target="_blank">
-            <span>Admin</span>
-            <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="ml-1" style="width: 1.2em; height: 1.2em; vertical-align: -0.2em;">
-                <path d="M11 3a1 1 0 100 2h2.586l-6.293 6.293a1 1 0 101.414 1.414L15 6.414V9a1 1 0 102 0V4a1 1 0 00-1-1h-5z" />
-                <path d="M5 5a2 2 0 00-2 2v8a2 2 0 002 2h8a2 2 0 002-2v-3a1 1 0 10-2 0v3H5V7h3a1 1 0 000-2H5z" />
-            </svg>
-        </a>
-    </li>
-    {% endif %}
-</ul>
-<br>
diff --git a/bookmarks/views/bookmarks.py b/bookmarks/views/bookmarks.py
index 42062cfe..f6bdebb2 100644
--- a/bookmarks/views/bookmarks.py
+++ b/bookmarks/views/bookmarks.py
@@ -51,6 +51,7 @@ def index(request: HttpRequest):
         request,
         "bookmarks/index.html",
         {
+            "page_title": "Bookmarks - Linkding",
             "bookmark_list": bookmark_list,
             "tag_cloud": tag_cloud,
             "details": bookmark_details,
@@ -73,6 +74,7 @@ def archived(request: HttpRequest):
         request,
         "bookmarks/archive.html",
         {
+            "page_title": "Archived bookmarks - Linkding",
             "bookmark_list": bookmark_list,
             "tag_cloud": tag_cloud,
             "details": bookmark_details,
@@ -97,6 +99,7 @@ def shared(request: HttpRequest):
         request,
         "bookmarks/shared.html",
         {
+            "page_title": "Shared bookmarks - Linkding",
             "bookmark_list": bookmark_list,
             "tag_cloud": tag_cloud,
             "details": bookmark_details,
@@ -107,6 +110,9 @@ def shared(request: HttpRequest):
 
 
 def render_bookmarks_view(request: HttpRequest, template_name, context):
+    if context["details"]:
+        context["page_title"] = "Bookmark details - Linkding"
+
     if turbo.is_frame(request, "details-modal"):
         return render(
             request,

test_patch: diff --git a/bookmarks/tests/test_layout.py b/bookmarks/tests/test_layout.py
index 275f54b0..2f3c6e22 100644
--- a/bookmarks/tests/test_layout.py
+++ b/bookmarks/tests/test_layout.py
@@ -24,6 +24,13 @@ def test_nav_menu_should_respect_share_profile_setting(self):
             html,
             count=0,
         )
+        self.assertInHTML(
+            f"""
+            <a href="{reverse('linkding:bookmarks.shared')}" class="menu-link">Shared bookmarks</a>
+        """,
+            html,
+            count=0,
+        )
 
         self.user.profile.enable_sharing = True
         self.user.profile.save()
@@ -35,7 +42,14 @@ def test_nav_menu_should_respect_share_profile_setting(self):
             <a href="{reverse('linkding:bookmarks.shared')}" class="menu-link">Shared</a>
         """,
             html,
-            count=2,
+            count=1,
+        )
+        self.assertInHTML(
+            f"""
+            <a href="{reverse('linkding:bookmarks.shared')}" class="menu-link">Shared bookmarks</a>
+        """,
+            html,
+            count=1,
         )
 
     def test_metadata_should_respect_prefetch_links_setting(self):
diff --git a/bookmarks/tests/test_settings_general_view.py b/bookmarks/tests/test_settings_general_view.py
index f75c00e6..a99a04ab 100644
--- a/bookmarks/tests/test_settings_general_view.py
+++ b/bookmarks/tests/test_settings_general_view.py
@@ -593,7 +593,7 @@ def test_global_settings_only_visible_for_superuser(self):
         html = response.content.decode()
 
         self.assertInHTML(
-            "<h2>Global settings</h2>",
+            '<h2 id="global-settings-heading">Global settings</h2>',
             html,
             count=0,
         )
@@ -605,7 +605,7 @@ def test_global_settings_only_visible_for_superuser(self):
         html = response.content.decode()
 
         self.assertInHTML(
-            "<h2>Global settings</h2>",
+            '<h2 id="global-settings-heading">Global settings</h2>',
             html,
             count=1,
         )
diff --git a/bookmarks/tests_e2e/e2e_test_filter_drawer.py b/bookmarks/tests_e2e/e2e_test_filter_drawer.py
index 29241580..cbd32dd8 100644
--- a/bookmarks/tests_e2e/e2e_test_filter_drawer.py
+++ b/bookmarks/tests_e2e/e2e_test_filter_drawer.py
@@ -16,9 +16,7 @@ def test_show_modal_close_modal(self):
             page.set_viewport_size({"width": 375, "height": 812})
 
             # open drawer
-            drawer_trigger = page.locator(".content-area-header").get_by_role(
-                "button", name="Filters"
-            )
+            drawer_trigger = page.locator(".main").get_by_role("button", name="Filters")
             drawer_trigger.click()
 
             # verify drawer is visible
@@ -49,9 +47,7 @@ def test_select_tag(self):
             page.set_viewport_size({"width": 375, "height": 812})
 
             # open tag cloud modal
-            drawer_trigger = page.locator(".content-area-header").get_by_role(
-                "button", name="Filters"
-            )
+            drawer_trigger = page.locator(".main").get_by_role("button", name="Filters")
             drawer_trigger.click()
 
             # verify tags are displayed