aboutsummaryrefslogtreecommitdiffstats
path: root/apps/web
diff options
context:
space:
mode:
authorMohamed Bassem <me@mbassem.com>2025-11-09 11:50:58 +0000
committerGitHub <noreply@github.com>2025-11-09 11:50:58 +0000
commit3083be0c9dc9ec0ded58eda937b83fbdf511f386 (patch)
tree43fd298af4ae45c5f7802e95af0a94cfd9745bc2 /apps/web
parent1b8129a28191c7093818060e39e968fc16bf24b4 (diff)
downloadkarakeep-3083be0c9dc9ec0ded58eda937b83fbdf511f386.tar.zst
feat: Add page titles (#2109)
* feat: add Next.js metadata titles to dynamic and settings pages Add page titles using Next.js metadata API for better SEO and user experience: - List pages: Show list name in format "<name> | Karakeep" - Tag pages: Show tag name in format "<name> | Karakeep" - Admin pages: Add titles for overview, users, and background jobs pages - Settings pages: Add titles for all settings pages (API keys, AI, feeds, import, info, webhooks, subscription, rules, stats, assets, broken links) For client components (rules, stats, assets, broken-links), created layout.tsx files to export metadata since metadata can only be exported from server components. * feat: add Next.js metadata titles to dashboard pages Add page titles using Next.js metadata API to archive, favourites, highlights, and all tags pages: - Archive page: Show "Archive | Karakeep" - Favourites page: Show "Favourites | Karakeep" - Highlights page: Show "Highlights | Karakeep" - All Tags page: Show "All Tags | Karakeep" Improves SEO and user experience across all dashboard browsing pages. * refactor: use i18n translations for dashboard page titles Convert hardcoded page titles to use translations via generateMetadata: - Archive page: Uses common.archive translation - Favourites page: Uses lists.favourites translation - Highlights page: Uses common.highlights translation - All Tags page: Uses tags.all_tags translation Improves localization support across dashboard pages. * feat: add i18n translations to admin and settings page titles Convert hardcoded page titles to use translations via generateMetadata: - Admin Overview: Uses admin.admin_settings translation - AI Settings: Uses settings.ai.ai_settings translation - API Keys: Uses settings.api_keys.api_keys translation - Feed Settings: Uses settings.feeds.rss_subscriptions translation - Import/Export: Uses settings.import.import_export translation - Account Info: Uses settings.info.user_info translation - Subscription: Uses settings.subscription.subscription translation - Webhooks: Uses settings.webhooks.webhooks translation Improves localization support across admin and settings pages. * revert accidental commit * more translations * more fixes --------- Co-authored-by: Claude <noreply@anthropic.com>
Diffstat (limited to 'apps/web')
-rw-r--r--apps/web/app/admin/background_jobs/page.tsx10
-rw-r--r--apps/web/app/admin/overview/page.tsx10
-rw-r--r--apps/web/app/admin/users/page.tsx10
-rw-r--r--apps/web/app/dashboard/archive/page.tsx10
-rw-r--r--apps/web/app/dashboard/favourites/page.tsx10
-rw-r--r--apps/web/app/dashboard/highlights/page.tsx9
-rw-r--r--apps/web/app/dashboard/lists/[listId]/page.tsx18
-rw-r--r--apps/web/app/dashboard/tags/[tagId]/page.tsx18
-rw-r--r--apps/web/app/dashboard/tags/page.tsx10
-rw-r--r--apps/web/app/settings/ai/page.tsx10
-rw-r--r--apps/web/app/settings/api-keys/page.tsx10
-rw-r--r--apps/web/app/settings/assets/layout.tsx18
-rw-r--r--apps/web/app/settings/broken-links/layout.tsx18
-rw-r--r--apps/web/app/settings/feeds/page.tsx10
-rw-r--r--apps/web/app/settings/import/page.tsx10
-rw-r--r--apps/web/app/settings/info/page.tsx10
-rw-r--r--apps/web/app/settings/rules/layout.tsx18
-rw-r--r--apps/web/app/settings/stats/layout.tsx18
-rw-r--r--apps/web/app/settings/subscription/page.tsx10
-rw-r--r--apps/web/app/settings/webhooks/page.tsx10
20 files changed, 247 insertions, 0 deletions
diff --git a/apps/web/app/admin/background_jobs/page.tsx b/apps/web/app/admin/background_jobs/page.tsx
index 6a13dd64..e3958835 100644
--- a/apps/web/app/admin/background_jobs/page.tsx
+++ b/apps/web/app/admin/background_jobs/page.tsx
@@ -1,4 +1,14 @@
+import type { Metadata } from "next";
import BackgroundJobs from "@/components/admin/BackgroundJobs";
+import { useTranslation } from "@/lib/i18n/server";
+
+export async function generateMetadata(): Promise<Metadata> {
+ // oxlint-disable-next-line rules-of-hooks
+ const { t } = await useTranslation();
+ return {
+ title: `${t("admin.background_jobs.background_jobs")} | Karakeep`,
+ };
+}
export default function BackgroundJobsPage() {
return <BackgroundJobs />;
diff --git a/apps/web/app/admin/overview/page.tsx b/apps/web/app/admin/overview/page.tsx
index 66844d04..04d99b91 100644
--- a/apps/web/app/admin/overview/page.tsx
+++ b/apps/web/app/admin/overview/page.tsx
@@ -1,5 +1,15 @@
+import type { Metadata } from "next";
import BasicStats from "@/components/admin/BasicStats";
import ServiceConnections from "@/components/admin/ServiceConnections";
+import { useTranslation } from "@/lib/i18n/server";
+
+export async function generateMetadata(): Promise<Metadata> {
+ // oxlint-disable-next-line rules-of-hooks
+ const { t } = await useTranslation();
+ return {
+ title: `${t("admin.admin_settings")} | Karakeep`,
+ };
+}
export default function AdminOverviewPage() {
return (
diff --git a/apps/web/app/admin/users/page.tsx b/apps/web/app/admin/users/page.tsx
index be5cfe81..5af899a4 100644
--- a/apps/web/app/admin/users/page.tsx
+++ b/apps/web/app/admin/users/page.tsx
@@ -1,4 +1,14 @@
+import type { Metadata } from "next";
import UserList from "@/components/admin/UserList";
+import { useTranslation } from "@/lib/i18n/server";
+
+export async function generateMetadata(): Promise<Metadata> {
+ // oxlint-disable-next-line rules-of-hooks
+ const { t } = await useTranslation();
+ return {
+ title: `${t("admin.users_list.users_list")} | Karakeep`,
+ };
+}
export default function AdminUsersPage() {
return <UserList />;
diff --git a/apps/web/app/dashboard/archive/page.tsx b/apps/web/app/dashboard/archive/page.tsx
index becb6a58..33a6e610 100644
--- a/apps/web/app/dashboard/archive/page.tsx
+++ b/apps/web/app/dashboard/archive/page.tsx
@@ -1,5 +1,15 @@
+import type { Metadata } from "next";
import Bookmarks from "@/components/dashboard/bookmarks/Bookmarks";
import InfoTooltip from "@/components/ui/info-tooltip";
+import { useTranslation } from "@/lib/i18n/server";
+
+export async function generateMetadata(): Promise<Metadata> {
+ // oxlint-disable-next-line rules-of-hooks
+ const { t } = await useTranslation();
+ return {
+ title: `${t("common.archive")} | Karakeep`,
+ };
+}
function header() {
return (
diff --git a/apps/web/app/dashboard/favourites/page.tsx b/apps/web/app/dashboard/favourites/page.tsx
index be20bd2f..7ece9cdf 100644
--- a/apps/web/app/dashboard/favourites/page.tsx
+++ b/apps/web/app/dashboard/favourites/page.tsx
@@ -1,4 +1,14 @@
+import type { Metadata } from "next";
import Bookmarks from "@/components/dashboard/bookmarks/Bookmarks";
+import { useTranslation } from "@/lib/i18n/server";
+
+export async function generateMetadata(): Promise<Metadata> {
+ // oxlint-disable-next-line rules-of-hooks
+ const { t } = await useTranslation();
+ return {
+ title: `${t("lists.favourites")} | Karakeep`,
+ };
+}
export default async function FavouritesBookmarkPage() {
return (
diff --git a/apps/web/app/dashboard/highlights/page.tsx b/apps/web/app/dashboard/highlights/page.tsx
index 1410d1fd..5945de00 100644
--- a/apps/web/app/dashboard/highlights/page.tsx
+++ b/apps/web/app/dashboard/highlights/page.tsx
@@ -1,9 +1,18 @@
+import type { Metadata } from "next";
import AllHighlights from "@/components/dashboard/highlights/AllHighlights";
import { Separator } from "@/components/ui/separator";
import { useTranslation } from "@/lib/i18n/server";
import { api } from "@/server/api/client";
import { Highlighter } from "lucide-react";
+export async function generateMetadata(): Promise<Metadata> {
+ // oxlint-disable-next-line rules-of-hooks
+ const { t } = await useTranslation();
+ return {
+ title: `${t("common.highlights")} | Karakeep`,
+ };
+}
+
export default async function HighlightsPage() {
// oxlint-disable-next-line rules-of-hooks
const { t } = await useTranslation();
diff --git a/apps/web/app/dashboard/lists/[listId]/page.tsx b/apps/web/app/dashboard/lists/[listId]/page.tsx
index 4714a71c..3f9c3416 100644
--- a/apps/web/app/dashboard/lists/[listId]/page.tsx
+++ b/apps/web/app/dashboard/lists/[listId]/page.tsx
@@ -1,3 +1,4 @@
+import type { Metadata } from "next";
import { notFound } from "next/navigation";
import Bookmarks from "@/components/dashboard/bookmarks/Bookmarks";
import ListHeader from "@/components/dashboard/lists/ListHeader";
@@ -6,6 +7,23 @@ import { TRPCError } from "@trpc/server";
import { BookmarkListContextProvider } from "@karakeep/shared-react/hooks/bookmark-list-context";
+export async function generateMetadata(props: {
+ params: Promise<{ listId: string }>;
+}): Promise<Metadata> {
+ const params = await props.params;
+ try {
+ const list = await api.lists.get({ listId: params.listId });
+ return {
+ title: `${list.name} | Karakeep`,
+ };
+ } catch (e) {
+ if (e instanceof TRPCError && e.code === "NOT_FOUND") {
+ notFound();
+ }
+ throw e;
+ }
+}
+
export default async function ListPage(props: {
params: Promise<{ listId: string }>;
searchParams?: Promise<{
diff --git a/apps/web/app/dashboard/tags/[tagId]/page.tsx b/apps/web/app/dashboard/tags/[tagId]/page.tsx
index 7971da1e..fcb5010e 100644
--- a/apps/web/app/dashboard/tags/[tagId]/page.tsx
+++ b/apps/web/app/dashboard/tags/[tagId]/page.tsx
@@ -1,3 +1,4 @@
+import type { Metadata } from "next";
import { notFound } from "next/navigation";
import Bookmarks from "@/components/dashboard/bookmarks/Bookmarks";
import EditableTagName from "@/components/dashboard/tags/EditableTagName";
@@ -7,6 +8,23 @@ import { api } from "@/server/api/client";
import { TRPCError } from "@trpc/server";
import { MoreHorizontal } from "lucide-react";
+export async function generateMetadata(props: {
+ params: Promise<{ tagId: string }>;
+}): Promise<Metadata> {
+ const params = await props.params;
+ try {
+ const tag = await api.tags.get({ tagId: params.tagId });
+ return {
+ title: `${tag.name} | Karakeep`,
+ };
+ } catch (e) {
+ if (e instanceof TRPCError && e.code === "NOT_FOUND") {
+ notFound();
+ }
+ throw e;
+ }
+}
+
export default async function TagPage(props: {
params: Promise<{ tagId: string }>;
searchParams?: Promise<{
diff --git a/apps/web/app/dashboard/tags/page.tsx b/apps/web/app/dashboard/tags/page.tsx
index b2acd45b..2ebcf2f7 100644
--- a/apps/web/app/dashboard/tags/page.tsx
+++ b/apps/web/app/dashboard/tags/page.tsx
@@ -1,4 +1,14 @@
+import type { Metadata } from "next";
import AllTagsView from "@/components/dashboard/tags/AllTagsView";
+import { useTranslation } from "@/lib/i18n/server";
+
+export async function generateMetadata(): Promise<Metadata> {
+ // oxlint-disable-next-line rules-of-hooks
+ const { t } = await useTranslation();
+ return {
+ title: `${t("tags.all_tags")} | Karakeep`,
+ };
+}
export default async function TagsPage() {
return <AllTagsView />;
diff --git a/apps/web/app/settings/ai/page.tsx b/apps/web/app/settings/ai/page.tsx
index 2b3d7a8d..02e2ae4b 100644
--- a/apps/web/app/settings/ai/page.tsx
+++ b/apps/web/app/settings/ai/page.tsx
@@ -1,4 +1,14 @@
+import type { Metadata } from "next";
import AISettings from "@/components/settings/AISettings";
+import { useTranslation } from "@/lib/i18n/server";
+
+export async function generateMetadata(): Promise<Metadata> {
+ // oxlint-disable-next-line rules-of-hooks
+ const { t } = await useTranslation();
+ return {
+ title: `${t("settings.ai.ai_settings")} | Karakeep`,
+ };
+}
export default function AISettingsPage() {
return <AISettings />;
diff --git a/apps/web/app/settings/api-keys/page.tsx b/apps/web/app/settings/api-keys/page.tsx
index 1c3718d6..494a0b5c 100644
--- a/apps/web/app/settings/api-keys/page.tsx
+++ b/apps/web/app/settings/api-keys/page.tsx
@@ -1,4 +1,14 @@
+import type { Metadata } from "next";
import ApiKeySettings from "@/components/settings/ApiKeySettings";
+import { useTranslation } from "@/lib/i18n/server";
+
+export async function generateMetadata(): Promise<Metadata> {
+ // oxlint-disable-next-line rules-of-hooks
+ const { t } = await useTranslation();
+ return {
+ title: `${t("settings.api_keys.api_keys")} | Karakeep`,
+ };
+}
export default async function ApiKeysPage() {
return (
diff --git a/apps/web/app/settings/assets/layout.tsx b/apps/web/app/settings/assets/layout.tsx
new file mode 100644
index 00000000..2324c83a
--- /dev/null
+++ b/apps/web/app/settings/assets/layout.tsx
@@ -0,0 +1,18 @@
+import type { Metadata } from "next";
+import { useTranslation } from "@/lib/i18n/server";
+
+export async function generateMetadata(): Promise<Metadata> {
+ // oxlint-disable-next-line rules-of-hooks
+ const { t } = await useTranslation();
+ return {
+ title: `${t("settings.manage_assets.manage_assets")} | Karakeep`,
+ };
+}
+
+export default function AssetsLayout({
+ children,
+}: {
+ children: React.ReactNode;
+}) {
+ return <>{children}</>;
+}
diff --git a/apps/web/app/settings/broken-links/layout.tsx b/apps/web/app/settings/broken-links/layout.tsx
new file mode 100644
index 00000000..45f3dadf
--- /dev/null
+++ b/apps/web/app/settings/broken-links/layout.tsx
@@ -0,0 +1,18 @@
+import type { Metadata } from "next";
+import { useTranslation } from "@/lib/i18n/server";
+
+export async function generateMetadata(): Promise<Metadata> {
+ // oxlint-disable-next-line rules-of-hooks
+ const { t } = await useTranslation();
+ return {
+ title: `${t("settings.broken_links.broken_links")} | Karakeep`,
+ };
+}
+
+export default function BrokenLinksLayout({
+ children,
+}: {
+ children: React.ReactNode;
+}) {
+ return <>{children}</>;
+}
diff --git a/apps/web/app/settings/feeds/page.tsx b/apps/web/app/settings/feeds/page.tsx
index d2d1f182..603e9a09 100644
--- a/apps/web/app/settings/feeds/page.tsx
+++ b/apps/web/app/settings/feeds/page.tsx
@@ -1,4 +1,14 @@
+import type { Metadata } from "next";
import FeedSettings from "@/components/settings/FeedSettings";
+import { useTranslation } from "@/lib/i18n/server";
+
+export async function generateMetadata(): Promise<Metadata> {
+ // oxlint-disable-next-line rules-of-hooks
+ const { t } = await useTranslation();
+ return {
+ title: `${t("settings.feeds.rss_subscriptions")} | Karakeep`,
+ };
+}
export default function FeedSettingsPage() {
return <FeedSettings />;
diff --git a/apps/web/app/settings/import/page.tsx b/apps/web/app/settings/import/page.tsx
index 11780d51..23ef4019 100644
--- a/apps/web/app/settings/import/page.tsx
+++ b/apps/web/app/settings/import/page.tsx
@@ -1,4 +1,14 @@
+import type { Metadata } from "next";
import ImportExport from "@/components/settings/ImportExport";
+import { useTranslation } from "@/lib/i18n/server";
+
+export async function generateMetadata(): Promise<Metadata> {
+ // oxlint-disable-next-line rules-of-hooks
+ const { t } = await useTranslation();
+ return {
+ title: `${t("settings.import.import_export")} | Karakeep`,
+ };
+}
export default function ImportSettingsPage() {
return <ImportExport />;
diff --git a/apps/web/app/settings/info/page.tsx b/apps/web/app/settings/info/page.tsx
index 96deab96..1807b538 100644
--- a/apps/web/app/settings/info/page.tsx
+++ b/apps/web/app/settings/info/page.tsx
@@ -1,7 +1,17 @@
+import type { Metadata } from "next";
import { ChangePassword } from "@/components/settings/ChangePassword";
import { DeleteAccount } from "@/components/settings/DeleteAccount";
import UserDetails from "@/components/settings/UserDetails";
import UserOptions from "@/components/settings/UserOptions";
+import { useTranslation } from "@/lib/i18n/server";
+
+export async function generateMetadata(): Promise<Metadata> {
+ // oxlint-disable-next-line rules-of-hooks
+ const { t } = await useTranslation();
+ return {
+ title: `${t("settings.info.user_info")} | Karakeep`,
+ };
+}
export default async function InfoPage() {
return (
diff --git a/apps/web/app/settings/rules/layout.tsx b/apps/web/app/settings/rules/layout.tsx
new file mode 100644
index 00000000..03b4316e
--- /dev/null
+++ b/apps/web/app/settings/rules/layout.tsx
@@ -0,0 +1,18 @@
+import type { Metadata } from "next";
+import { useTranslation } from "@/lib/i18n/server";
+
+export async function generateMetadata(): Promise<Metadata> {
+ // oxlint-disable-next-line rules-of-hooks
+ const { t } = await useTranslation();
+ return {
+ title: `${t("settings.rules.rules")} | Karakeep`,
+ };
+}
+
+export default function RulesLayout({
+ children,
+}: {
+ children: React.ReactNode;
+}) {
+ return <>{children}</>;
+}
diff --git a/apps/web/app/settings/stats/layout.tsx b/apps/web/app/settings/stats/layout.tsx
new file mode 100644
index 00000000..0f24c049
--- /dev/null
+++ b/apps/web/app/settings/stats/layout.tsx
@@ -0,0 +1,18 @@
+import type { Metadata } from "next";
+import { useTranslation } from "@/lib/i18n/server";
+
+export async function generateMetadata(): Promise<Metadata> {
+ // oxlint-disable-next-line rules-of-hooks
+ const { t } = await useTranslation();
+ return {
+ title: `${t("settings.stats.usage_statistics")} | Karakeep`,
+ };
+}
+
+export default function StatsLayout({
+ children,
+}: {
+ children: React.ReactNode;
+}) {
+ return <>{children}</>;
+}
diff --git a/apps/web/app/settings/subscription/page.tsx b/apps/web/app/settings/subscription/page.tsx
index e8c46460..5b06798e 100644
--- a/apps/web/app/settings/subscription/page.tsx
+++ b/apps/web/app/settings/subscription/page.tsx
@@ -1,9 +1,19 @@
+import type { Metadata } from "next";
import { redirect } from "next/navigation";
import SubscriptionSettings from "@/components/settings/SubscriptionSettings";
import { QuotaProgress } from "@/components/subscription/QuotaProgress";
+import { useTranslation } from "@/lib/i18n/server";
import serverConfig from "@karakeep/shared/config";
+export async function generateMetadata(): Promise<Metadata> {
+ // oxlint-disable-next-line rules-of-hooks
+ const { t } = await useTranslation();
+ return {
+ title: `${t("settings.subscription.subscription")} | Karakeep`,
+ };
+}
+
export default async function SubscriptionPage() {
if (!serverConfig.stripe.isConfigured) {
redirect("/settings");
diff --git a/apps/web/app/settings/webhooks/page.tsx b/apps/web/app/settings/webhooks/page.tsx
index 327d0d8f..4f798aa1 100644
--- a/apps/web/app/settings/webhooks/page.tsx
+++ b/apps/web/app/settings/webhooks/page.tsx
@@ -1,4 +1,14 @@
+import type { Metadata } from "next";
import WebhookSettings from "@/components/settings/WebhookSettings";
+import { useTranslation } from "@/lib/i18n/server";
+
+export async function generateMetadata(): Promise<Metadata> {
+ // oxlint-disable-next-line rules-of-hooks
+ const { t } = await useTranslation();
+ return {
+ title: `${t("settings.webhooks.webhooks")} | Karakeep`,
+ };
+}
export default function WebhookSettingsPage() {
return <WebhookSettings />;