From 7352bb6810557610710677e63e43bd677c80429e Mon Sep 17 00:00:00 2001 From: Dirk Date: Mon, 17 Aug 2026 08:44:06 +0200 Subject: [PATCH 01/11] Add social share buttons and redesign SingleContent hero header - Add Telegram/WhatsApp share buttons on SingleContent, backed by new brand icon components and new locale strings - Rework the title/summary layout to overlay on the hero image when one is present, unifying the previous separate desktop/mobile blocks - Merge the metadata and bookmark rows into a single divided bar to make room for the new share actions --- api/src/db/seedingDocs/lang-eng.json | 4 +- api/src/db/seedingDocs/lang-fra.json | 4 +- app/src/components/icons/TelegramIcon.vue | 13 ++ app/src/components/icons/WhatsAppIcon.vue | 13 ++ app/src/pages/SingleContent/SingleContent.vue | 173 ++++++++++++------ 5 files changed, 154 insertions(+), 53 deletions(-) create mode 100644 app/src/components/icons/TelegramIcon.vue create mode 100644 app/src/components/icons/WhatsAppIcon.vue diff --git a/api/src/db/seedingDocs/lang-eng.json b/api/src/db/seedingDocs/lang-eng.json index 836e304283..b0fae0e84d 100644 --- a/api/src/db/seedingDocs/lang-eng.json +++ b/api/src/db/seedingDocs/lang-eng.json @@ -160,6 +160,8 @@ "auth.sign_in": "Sign in", "auth.no_methods_available": "No authentication methods available. Please try again in a moment.", "singlecontent.listen": "Listen", - "singlecontent.loading": "Loading..." + "singlecontent.loading": "Loading...", + "singlecontent.shareTelegram": "Share on Telegram", + "singlecontent.shareWhatsApp": "Share on WhatsApp" } } diff --git a/api/src/db/seedingDocs/lang-fra.json b/api/src/db/seedingDocs/lang-fra.json index 02d545a94c..eff3440c69 100644 --- a/api/src/db/seedingDocs/lang-fra.json +++ b/api/src/db/seedingDocs/lang-fra.json @@ -160,6 +160,8 @@ "auth.sign_in": "Se connecter", "auth.no_methods_available": "Aucun méthode d'authentification disponible. Veuillez réessayer dans un moment.", "singlecontent.listen": "Écoutez", - "singlecontent.loading": "Chargement..." + "singlecontent.loading": "Chargement...", + "singlecontent.shareTelegram": "Partager sur Telegram", + "singlecontent.shareWhatsApp": "Partager sur WhatsApp" } } diff --git a/app/src/components/icons/TelegramIcon.vue b/app/src/components/icons/TelegramIcon.vue new file mode 100644 index 0000000000..ed50f75e38 --- /dev/null +++ b/app/src/components/icons/TelegramIcon.vue @@ -0,0 +1,13 @@ + + diff --git a/app/src/components/icons/WhatsAppIcon.vue b/app/src/components/icons/WhatsAppIcon.vue new file mode 100644 index 0000000000..693974c74a --- /dev/null +++ b/app/src/components/icons/WhatsAppIcon.vue @@ -0,0 +1,13 @@ + + diff --git a/app/src/pages/SingleContent/SingleContent.vue b/app/src/pages/SingleContent/SingleContent.vue index 4f05e3f2cf..e1b001c08a 100644 --- a/app/src/pages/SingleContent/SingleContent.vue +++ b/app/src/pages/SingleContent/SingleContent.vue @@ -50,6 +50,8 @@ import RelatedContent from "@/components/content/RelatedContent.vue"; import VerticalTagViewer from "@/components/tags/VerticalTagViewer.vue"; import LImage from "@/components/images/LImage.vue"; +import TelegramIcon from "@/components/icons/TelegramIcon.vue"; +import WhatsAppIcon from "@/components/icons/WhatsAppIcon.vue"; import { userPreferencesAsRef } from "@/globalConfig"; import IgnorePagePadding from "@/components/IgnorePagePadding.vue"; @@ -525,6 +527,26 @@ const isBookmarked = computed(() => { return userPreferencesAsRef.value.bookmarks?.some((b) => b.id == content.value?.parentId); }); +// `window.location.href` (not the build-time `canonicalUrl`) so the shared link is +// always the URL actually open in the browser, regardless of whether VITE_WEB_ORIGIN +// is configured — these buttons only ever run client-side, after a click. +const shareUrl = () => window.location.href; + +const shareToTelegram = () => { + if (!content.value) return; + const url = new URL("https://t.me/share/url"); + url.searchParams.set("url", shareUrl()); + url.searchParams.set("text", content.value.title); + window.open(url.toString(), "_blank"); +}; + +const shareToWhatsApp = () => { + if (!content.value) return; + const url = new URL("https://wa.me/"); + url.searchParams.set("text", `${content.value.title} ${shareUrl()}`); + window.open(url.toString(), "_blank"); +}; + // The normal SPA sets the tab/window title imperatively (it has no @unhead plugin). The web // build's `useContentHead` above owns the whole head there, including the meta // description — serialized into the prerendered HTML that crawlers actually read. @@ -658,6 +680,12 @@ const playAudio = () => { } }; +// Whether the hero image can carry the title/summary as an overlay. Video has its own +// player chrome the overlay would fight with, so it keeps a plain title above instead. +const hasHeroImage = computed( + () => !content.value?.video && !!(content.value?.parentId || content.value?.parentImageData), +); + watch([isLoading, content, is404], async () => { if (is404.value) { await nextTick(); @@ -764,44 +792,28 @@ watch([isLoading, content, is404], async () => { class="w-full lg:w-3/4 lg:max-w-3xl" v-else-if="content" > - -