From 810580ee16aae828bbf7586eeca7773ca17b5390 Mon Sep 17 00:00:00 2001 From: pithwalahussain16 <159010205+pithwalahussain16@users.noreply.github.com> Date: Tue, 3 Nov 2026 18:35:15 +0530 Subject: [PATCH 1/2] elevenlabs=plugin-headless --- .../components/ElevenLabsHeadlessReader.css | 8 ++ .../components/ElevenLabsHeadlessReader.tsx | 80 +++++++++++++++++++ .../src/components/wordpressShortcodes.tsx | 2 + apps/storefront/src/lib/shortcodeRegistry.mjs | 1 + apps/storefront/src/pages/PostMockupPage.tsx | 6 +- 5 files changed, 96 insertions(+), 1 deletion(-) create mode 100644 apps/storefront/src/components/ElevenLabsHeadlessReader.css create mode 100644 apps/storefront/src/components/ElevenLabsHeadlessReader.tsx diff --git a/apps/storefront/src/components/ElevenLabsHeadlessReader.css b/apps/storefront/src/components/ElevenLabsHeadlessReader.css new file mode 100644 index 0000000..6f24d05 --- /dev/null +++ b/apps/storefront/src/components/ElevenLabsHeadlessReader.css @@ -0,0 +1,8 @@ +/* Headless fallback styling. The authoritative shortcode styles are fetched from WP. */ +.elwr-headless-root { width:100%; min-width:0; } +.elwr-headless-root .elwr-reader { box-sizing:border-box; width:100%; max-width:100%; border-radius:18px; background:#c94321; color:#fff; padding:20px; overflow:hidden; } +.elwr-headless-root .elwr-reader button { cursor:pointer; } +.elwr-headless-root .elwr-reader input[type="range"] { accent-color:#fff; } +.elwr-headless-root .elwr-reader audio { max-width:100%; } +.elwr-headless-error { color:#b91c1c; } +.elwr-headless-loading { opacity:.7; } diff --git a/apps/storefront/src/components/ElevenLabsHeadlessReader.tsx b/apps/storefront/src/components/ElevenLabsHeadlessReader.tsx new file mode 100644 index 0000000..2deb686 --- /dev/null +++ b/apps/storefront/src/components/ElevenLabsHeadlessReader.tsx @@ -0,0 +1,80 @@ +import { useEffect, useState } from "react"; +import { BACKEND_ORIGIN } from "@funky/sdk"; +import "./ElevenLabsHeadlessReader.css"; + +type ReaderData = { + post?: { elwrReaderHtml?: string | null } | null; + elwrHeadlessAssets?: { styleUrl?: string | null; scriptUrl?: string | null } | null; +}; + +const loadedStyles = new Set(); +const loadedScripts = new Set(); + +/** Uses the existing WP shortcode renderer, not a second React-only player. */ +export function ElevenLabsHeadlessReader({ postId }: { postId: number }) { + const [data, setData] = useState(null); + const [error, setError] = useState(""); + useEffect(() => { + if (!BACKEND_ORIGIN || !Number.isSafeInteger(postId) || postId <= 0) { + setError("A valid WordPress database post ID and backend are required."); + return; + } + const controller = new AbortController(); + setData(null); + setError(""); + fetch(`${BACKEND_ORIGIN.replace(/\/$/, "")}/graphql`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + query: `query Reader($id: ID!) { post(id: $id, idType: DATABASE_ID) { elwrReaderHtml } elwrHeadlessAssets { styleUrl scriptUrl } }`, + variables: { id: String(postId) }, + }), + signal: controller.signal, + }) + .then(async (response) => { + if (!response.ok) throw new Error(`GraphQL HTTP ${response.status}`); + const payload = await response.json(); + if (payload.errors?.length) throw new Error(payload.errors[0].message); + return payload.data as ReaderData; + }) + .then((result) => { + if (!controller.signal.aborted) setData(result); + }) + .catch((reason) => { + if (!controller.signal.aborted) setError(String(reason?.message || reason)); + }); + return () => controller.abort(); + }, [postId]); + + useEffect(() => { + const styleUrl = data?.elwrHeadlessAssets?.styleUrl; + const scriptUrl = data?.elwrHeadlessAssets?.scriptUrl; + const origin = BACKEND_ORIGIN?.replace(/\/$/, ""); + const allowed = (value: string) => { + try { return new URL(value).origin === new URL(origin!).origin; } + catch { return false; } + }; + if (styleUrl && allowed(styleUrl) && !loadedStyles.has(styleUrl)) { + const link = document.createElement("link"); + link.rel = "stylesheet"; + link.href = styleUrl; + link.dataset.elwrAsset = "style"; + document.head.appendChild(link); + loadedStyles.add(styleUrl); + } + if (scriptUrl && allowed(scriptUrl) && !loadedScripts.has(scriptUrl)) { + const script = document.createElement("script"); + script.src = scriptUrl; + script.async = true; + script.dataset.elwrAsset = "script"; + document.body.appendChild(script); + loadedScripts.add(scriptUrl); + } + }, [data]); + + if (error) return

Audio reader: {error}

; + if (!data) return

Loading audio player…

; + if (!data.post?.elwrReaderHtml) return

Audio player is not available for this article. Check the shortcode and generated audio in WordPress.

; + // Only inject shortcode HTML returned by the trusted, configured WordPress backend. + return
; +} diff --git a/apps/storefront/src/components/wordpressShortcodes.tsx b/apps/storefront/src/components/wordpressShortcodes.tsx index 7305429..323af81 100644 --- a/apps/storefront/src/components/wordpressShortcodes.tsx +++ b/apps/storefront/src/components/wordpressShortcodes.tsx @@ -1,3 +1,4 @@ +import { ElevenLabsHeadlessReader } from "./ElevenLabsHeadlessReader"; import { PostCard, PaginablePostGrid, @@ -1554,6 +1555,7 @@ function UnsubscribeFormShortcode({ attributes }: ShortcodeProps) { } export const WORDPRESS_SHORTCODE_RENDERERS: Record = { + elevenlabs_reader: (attributes) => , hero: (attributes) => , "video-hero": (attributes) => , "spotify-radio": (attributes) => , diff --git a/apps/storefront/src/lib/shortcodeRegistry.mjs b/apps/storefront/src/lib/shortcodeRegistry.mjs index 3f0da5c..5692521 100644 --- a/apps/storefront/src/lib/shortcodeRegistry.mjs +++ b/apps/storefront/src/lib/shortcodeRegistry.mjs @@ -1,4 +1,5 @@ export const CONTENT_SHORTCODE_NAMES = Object.freeze([ + "elevenlabs_reader", "hero", "video-hero", "spotify-radio", diff --git a/apps/storefront/src/pages/PostMockupPage.tsx b/apps/storefront/src/pages/PostMockupPage.tsx index 21c1d2c..a01c18b 100644 --- a/apps/storefront/src/pages/PostMockupPage.tsx +++ b/apps/storefront/src/pages/PostMockupPage.tsx @@ -13,6 +13,7 @@ import { Breadcrumbs, type BreadcrumbItem } from "../components/Breadcrumbs"; import { ContentLoadingState } from "../components/ContentLoadingState"; import { GuestStarRating } from "../components/GuestStarRating"; import { renderCmsContent } from "../components/CmsPageContent"; +import { ElevenLabsHeadlessReader } from "../components/ElevenLabsHeadlessReader"; import { WORDPRESS_SHORTCODE_RENDERERS } from "../components/wordpressShortcodes"; import { APPLICATION_SHORTCODE_RENDERERS } from "../components/applicationShortcodeRenderers"; import { useIncrementalData } from "@funky/sdk/react"; @@ -277,6 +278,7 @@ function PostMockupPageInner({
@@ -473,10 +475,12 @@ function TocDotRail({ entries, side }: { entries: TocEntry[]; side: "left" | "ri function PostContentBody({ contentRef, html, + postId, className, }: { contentRef: RefObject; html: string; + postId: number; className?: string; }) { return ( @@ -487,7 +491,7 @@ function PostContentBody({ > {renderCmsContent( html, - { ...WORDPRESS_SHORTCODE_RENDERERS, ...APPLICATION_SHORTCODE_RENDERERS }, + { ...WORDPRESS_SHORTCODE_RENDERERS, elevenlabs_reader: (attrs) => , ...APPLICATION_SHORTCODE_RENDERERS }, )} ); From 83a64a0f7af21f3265cbba3f70327d3cb87c31e6 Mon Sep 17 00:00:00 2001 From: pithwalahussain16 <159010205+pithwalahussain16@users.noreply.github.com> Date: Thu, 5 Nov 2026 21:03:08 +0530 Subject: [PATCH 2/2] elevenlab-plugin --- .env.example | 10 + apps/storefront/.env.example | 34 --- .../src/components/CmsPageContent.tsx | 105 ++++----- .../ElevenLabsExistingMarkupAssets.tsx | 219 ++++++++++++++++++ .../components/ElevenLabsHeadlessReader.css | 24 +- .../components/ElevenLabsHeadlessReader.tsx | 156 ++++++++----- .../src/components/wordpressShortcodes.tsx | 101 ++++---- apps/storefront/src/lib/elevenlabs.ts | 46 ++++ apps/storefront/src/lib/pages.ts | 30 +-- apps/storefront/src/pages/PostMockupPage.tsx | 63 +++-- ....timestamp-1793711596638-fe440ddd99829.mjs | 29 +++ 11 files changed, 569 insertions(+), 248 deletions(-) create mode 100644 .env.example delete mode 100644 apps/storefront/.env.example create mode 100644 apps/storefront/src/components/ElevenLabsExistingMarkupAssets.tsx create mode 100644 apps/storefront/src/lib/elevenlabs.ts create mode 100644 apps/storefront/vite.config.ts.timestamp-1793711596638-fe440ddd99829.mjs diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..d3211da --- /dev/null +++ b/.env.example @@ -0,0 +1,10 @@ +VITE_GRAPHQL_ENDPOINT=https://admin.e-multicontent.com/graphql +VITE_ARTIFACT_ROUTE_HYDRATION=true +VITE_BACKEND_PROFILE=full +VITE_SITE_URL=https://e-multicontent.com +STOREFRONT_ARTIFACT_SITE_KEY=e-multi +STOREFRONT_ARTIFACT_MODE=off +CMS_TAILWIND_SOURCE_API_URL=https://admin.e-multicontent.com/wp-json/funkycommerce-tailwind/v1 +CMS_TAILWIND_BOOTSTRAP=true +CMS_TAILWIND_REQUIRED=true +STOREFRONT_ARTIFACT_SIGNING_SECRET=e200a0a19a618dd149af13f7f6a83124a0b57e50251506692a1e07d908f04a54 \ No newline at end of file diff --git a/apps/storefront/.env.example b/apps/storefront/.env.example deleted file mode 100644 index 4702126..0000000 --- a/apps/storefront/.env.example +++ /dev/null @@ -1,34 +0,0 @@ -# Single source of truth for the backend connection. Every other backend-derived -# value (auth mutations, abandoned-cart tracking endpoint, live shop currency, payment -# gateway availability) is derived from this one URL's origin — see lib/env.ts. Point -# it at your WPGraphQL endpoint, e.g.: -VITE_GRAPHQL_ENDPOINT= -# -# Optional direct URL to the static Tailwind class manifest generated by the -# Optional static Tailwind manifest for deployments that publish one independently. -# Superfunky production leaves this empty and compiles only the reviewed local -# contract, performing no CMS Tailwind inventory request. A configured manifest -# fails before Vite when unavailable, incomplete, or invalid. -VITE_CMS_TAILWIND_MANIFEST_URL= -# -# Preferred incremental mode: a signed, read-only source endpoint. Requires -# STOREFRONT_ARTIFACT_SIGNING_SECRET and VITE_SITE_URL. The build downloads the -# previous deployed index, fetches only changed source bodies, and publishes the -# next index with the successful deployment. -CMS_TAILWIND_SOURCE_API_URL= -# Set only for the first deployment when no previous index exists. A successful -# deployment publishes the index; remove this flag afterward. -CMS_TAILWIND_BOOTSTRAP=false -# -# Development and production builds default to the public -# https://dev.superfunky.pro/graphql reference backend when this is empty. Set an -# explicit endpoint for every customer or production deployment. -# -# Web Push subscribe, unsubscribe, and VAPID public-key routes are derived from this -# backend origin. The Stripe publishable key is likewise supplied by backend store -# settings. Private VAPID and Stripe keys must never be exposed to the frontend. - -# Edge/CDN geolocation endpoint (see lib/geolocation.ts) — typically injected by the -# hosting platform (e.g. Netlify's /geolocation) rather than the WP backend itself, so -# it stays a separate env var. -VITE_GEOLOCATION_ENDPOINT= diff --git a/apps/storefront/src/components/CmsPageContent.tsx b/apps/storefront/src/components/CmsPageContent.tsx index 1aba92c..07d46ea 100644 --- a/apps/storefront/src/components/CmsPageContent.tsx +++ b/apps/storefront/src/components/CmsPageContent.tsx @@ -245,47 +245,47 @@ export function CmsPageContent({ ? `${window.location.origin}${pathname === "/" ? "" : pathname.replace(/\/+$/, "")}` : page.seo.canonical || page.seo.opengraphUrl || `${window.location.origin}${pathname}`} languageCode={contentLanguageCode} - keywords={page.seo.keywords || undefined} - siteName={page.seo.siteName || undefined} - appendSiteName={false} - robots={ - robots - || ( - homePath - ? "index, follow" - : page.seo.robotsSource === "explicit" - ? page.seo.robots - : fallbackRobots || page.seo.robots - ) + keywords={page.seo.keywords || undefined} + siteName={page.seo.siteName || undefined} + appendSiteName={false} + robots={ + robots + || ( + homePath + ? "index, follow" + : page.seo.robotsSource === "explicit" + ? page.seo.robots + : fallbackRobots || page.seo.robots + ) + } + opengraphType={page.seo.opengraphType === "article" ? "article" : "website"} + opengraphTitle={page.seo.opengraphTitle || undefined} + opengraphDescription={page.seo.opengraphDescription || undefined} + image={page.featuredImage + ? { + url: page.featuredImage.sourceUrl, + alt: page.featuredImage.altText || page.title, + width: page.featuredImage.width, + height: page.featuredImage.height, } - opengraphType={page.seo.opengraphType === "article" ? "article" : "website"} - opengraphTitle={page.seo.opengraphTitle || undefined} - opengraphDescription={page.seo.opengraphDescription || undefined} - image={page.featuredImage - ? { - url: page.featuredImage.sourceUrl, - alt: page.featuredImage.altText || page.title, - width: page.featuredImage.width, - height: page.featuredImage.height, - } - : page.seo.opengraphImage - ? { url: page.seo.opengraphImage, alt: page.title } - : undefined} - opengraphPublishedTime={page.seo.opengraphPublishedTime || undefined} - opengraphModifiedTime={page.seo.opengraphModifiedTime || undefined} - opengraphAuthor={page.seo.opengraphAuthor || undefined} - opengraphPublisher={page.seo.opengraphPublisher || undefined} - twitterTitle={page.seo.twitterTitle || undefined} - twitterDescription={page.seo.twitterDescription || undefined} - schema={{ - pageType: page.seo.pageType || undefined, - articleType: page.seo.articleType || undefined, - }} - breadcrumbs={page.seo.breadcrumbs} - translations={page.translations.map((translation) => ({ - languageCode: translation.languageCode, - url: translation.uri, - }))} + : page.seo.opengraphImage + ? { url: page.seo.opengraphImage, alt: page.title } + : undefined} + opengraphPublishedTime={page.seo.opengraphPublishedTime || undefined} + opengraphModifiedTime={page.seo.opengraphModifiedTime || undefined} + opengraphAuthor={page.seo.opengraphAuthor || undefined} + opengraphPublisher={page.seo.opengraphPublisher || undefined} + twitterTitle={page.seo.twitterTitle || undefined} + twitterDescription={page.seo.twitterDescription || undefined} + schema={{ + pageType: page.seo.pageType || undefined, + articleType: page.seo.articleType || undefined, + }} + breadcrumbs={page.seo.breadcrumbs} + translations={page.translations.map((translation) => ({ + languageCode: translation.languageCode, + url: translation.uri, + }))} /> {breadcrumbs.length > 1 ? : null} @@ -372,7 +372,8 @@ function NestedWordPressShortcodes({ html, slots }: { html: string; slots: Neste return ( <> -
+ +
{slots.map(({ marker, content }, index) => ( -
-
- Loading {marker.name} -
+
+
+
+ Loading {marker.name}
- ), +
+ ), target, marker.slotId, ); diff --git a/apps/storefront/src/components/ElevenLabsExistingMarkupAssets.tsx b/apps/storefront/src/components/ElevenLabsExistingMarkupAssets.tsx new file mode 100644 index 0000000..063caa8 --- /dev/null +++ b/apps/storefront/src/components/ElevenLabsExistingMarkupAssets.tsx @@ -0,0 +1,219 @@ + +import { useEffect } from "react"; +import { graphqlRequest } from "@funky/sdk"; +import "./ElevenLabsHeadlessReader.css"; + +type AssetsResponse = { + elwrHeadlessAssets: { + css: string | null; + scriptUrl: string | null; + styleUrl: string | null; + } | null; +}; + +type ReaderResponse = { + post: { + elwrReaderHtml: string | null; + } | null; +}; + +const ASSETS_QUERY = ` + query ElevenLabsAssets { + elwrHeadlessAssets { + css + scriptUrl + styleUrl + } + } +`; + +const READER_QUERY = ` + query ElevenLabsReaderColor($id: ID!) { + post(id: $id, idType: DATABASE_ID) { + elwrReaderHtml + } + } +`; + +type Props = { + postId?: number; +}; + +function extractReaderColor(html: string): string | null { + const doc = new DOMParser().parseFromString(html, "text/html"); + + const reader = doc.querySelector(".elwr-reader"); + if (!reader) return null; + + const color = reader.style + .getPropertyValue("--elwr-color") + .trim(); + + // Accept only hex colors from the backend. + return /^#[0-9a-fA-F]{3,8}$/.test(color) ? color : null; +} + +function applyReaderColor(color: string) { + document.querySelectorAll(".elwr-reader").forEach( + (reader) => { + reader.style.setProperty("--elwr-color", color); + } + ); +} + +export function ElevenLabsExistingMarkupAssets({ + postId, +}: Props) { + useEffect(() => { + let cancelled = false; + let observer: MutationObserver | null = null; + + async function loadAssets() { + try { + const { data, errors } = + await graphqlRequest(ASSETS_QUERY); + + if (errors?.length) { + throw new Error( + errors.map((error) => error.message).join("; ") + ); + } + + if (cancelled || !data?.elwrHeadlessAssets) { + return; + } + + const { css, scriptUrl, styleUrl } = + data.elwrHeadlessAssets; + + // 1. Load the plugin's original CSS. + if (styleUrl) { + let link = document.getElementById( + "elevenlabs-reader-stylesheet" + ) as HTMLLinkElement | null; + + if (!link) { + link = document.createElement("link"); + link.id = "elevenlabs-reader-stylesheet"; + link.rel = "stylesheet"; + document.head.appendChild(link); + } + + if (link.href !== styleUrl) { + link.href = styleUrl; + } + } + + // 2. Inject WordPress dynamic CSS. + if (css) { + let style = document.getElementById( + "elevenlabs-dynamic-css" + ) as HTMLStyleElement | null; + + if (!style) { + style = document.createElement("style"); + style.id = "elevenlabs-dynamic-css"; + document.head.appendChild(style); + } + + style.textContent = css; + } + + // 3. Restore the WordPress admin color if + // the frontend sanitizer removed it. + if (Number.isInteger(postId) && (postId ?? 0) > 0) { + try { + const readerResult = + await graphqlRequest( + READER_QUERY, + { id: String(postId) } + ); + + if (readerResult.errors?.length) { + throw new Error( + readerResult.errors + .map((error) => error.message) + .join("; ") + ); + } + + if (cancelled) return; + + const html = + readerResult.data?.post?.elwrReaderHtml; + + const color = html + ? extractReaderColor(html) + : null; + + if (color) { + applyReaderColor(color); + + // Also handle reader markup mounted later. + observer = new MutationObserver(() => { + applyReaderColor(color); + }); + + observer.observe(document.body, { + childList: true, + subtree: true, + }); + + console.info( + "[ELWR] WordPress admin color:", + color + ); + } else { + console.warn( + "[ELWR] No admin color found in reader HTML" + ); + } + } catch (error) { + console.warn( + "[ELWR] Could not restore reader color:", + error + ); + } + } + + if (cancelled) return; + + // 4. Load the plugin's JavaScript. + if (scriptUrl) { + let script = document.getElementById( + "elevenlabs-reader-script" + ) as HTMLScriptElement | null; + + if (!script) { + script = document.createElement("script"); + script.id = "elevenlabs-reader-script"; + script.async = true; + script.src = scriptUrl; + document.body.appendChild(script); + } + } + + console.info("[ELWR] Assets loaded", { + css: Boolean(css), + styleUrl, + scriptUrl, + postId, + }); + } catch (error) { + console.error( + "[ELWR] Failed to load assets:", + error + ); + } + } + + loadAssets(); + + return () => { + cancelled = true; + observer?.disconnect(); + }; + }, [postId]); + + return null; +} diff --git a/apps/storefront/src/components/ElevenLabsHeadlessReader.css b/apps/storefront/src/components/ElevenLabsHeadlessReader.css index 6f24d05..e1c769d 100644 --- a/apps/storefront/src/components/ElevenLabsHeadlessReader.css +++ b/apps/storefront/src/components/ElevenLabsHeadlessReader.css @@ -1,8 +1,16 @@ -/* Headless fallback styling. The authoritative shortcode styles are fetched from WP. */ -.elwr-headless-root { width:100%; min-width:0; } -.elwr-headless-root .elwr-reader { box-sizing:border-box; width:100%; max-width:100%; border-radius:18px; background:#c94321; color:#fff; padding:20px; overflow:hidden; } -.elwr-headless-root .elwr-reader button { cursor:pointer; } -.elwr-headless-root .elwr-reader input[type="range"] { accent-color:#fff; } -.elwr-headless-root .elwr-reader audio { max-width:100%; } -.elwr-headless-error { color:#b91c1c; } -.elwr-headless-loading { opacity:.7; } +.elwr-headless-root { + width: 100%; + max-width: 100%; +} + +.elwr-headless-root .elwr-reader { + width: 100%; + max-width: 100%; + box-sizing: border-box; +} + +.elwr-headless-root .elwr-reader *, +.elwr-headless-root .elwr-reader *::before, +.elwr-headless-root .elwr-reader *::after { + box-sizing: border-box; +} \ No newline at end of file diff --git a/apps/storefront/src/components/ElevenLabsHeadlessReader.tsx b/apps/storefront/src/components/ElevenLabsHeadlessReader.tsx index 2deb686..e0c6909 100644 --- a/apps/storefront/src/components/ElevenLabsHeadlessReader.tsx +++ b/apps/storefront/src/components/ElevenLabsHeadlessReader.tsx @@ -1,80 +1,108 @@ -import { useEffect, useState } from "react"; -import { BACKEND_ORIGIN } from "@funky/sdk"; + import "./ElevenLabsHeadlessReader.css"; +import { fetchElevenLabsReader } from "../lib/elevenlabs"; +import { useEffect, useState } from "react"; -type ReaderData = { - post?: { elwrReaderHtml?: string | null } | null; - elwrHeadlessAssets?: { styleUrl?: string | null; scriptUrl?: string | null } | null; +type Props = { + postId: number; }; -const loadedStyles = new Set(); -const loadedScripts = new Set(); +export function ElevenLabsHeadlessReader({ postId }: Props) { + const [html, setHtml] = useState(""); + const [css, setCss] = useState(""); + const [scriptUrl, setScriptUrl] = useState(""); + const [error, setError] = useState(null); -/** Uses the existing WP shortcode renderer, not a second React-only player. */ -export function ElevenLabsHeadlessReader({ postId }: { postId: number }) { - const [data, setData] = useState(null); - const [error, setError] = useState(""); useEffect(() => { - if (!BACKEND_ORIGIN || !Number.isSafeInteger(postId) || postId <= 0) { - setError("A valid WordPress database post ID and backend are required."); - return; - } - const controller = new AbortController(); - setData(null); - setError(""); - fetch(`${BACKEND_ORIGIN.replace(/\/$/, "")}/graphql`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - query: `query Reader($id: ID!) { post(id: $id, idType: DATABASE_ID) { elwrReaderHtml } elwrHeadlessAssets { styleUrl scriptUrl } }`, - variables: { id: String(postId) }, - }), - signal: controller.signal, - }) - .then(async (response) => { - if (!response.ok) throw new Error(`GraphQL HTTP ${response.status}`); - const payload = await response.json(); - if (payload.errors?.length) throw new Error(payload.errors[0].message); - return payload.data as ReaderData; - }) + let active = true; + + setHtml(""); + setError(null); + + fetchElevenLabsReader(postId) .then((result) => { - if (!controller.signal.aborted) setData(result); + if (!active) return; + + setHtml(result.post?.elwrReaderHtml ?? ""); + setCss(result.elwrHeadlessAssets?.css ?? ""); + setScriptUrl(result.elwrHeadlessAssets?.scriptUrl ?? ""); + console.log('asdasd', result.post?.elwrReaderHtml ?? "") + console.log('asdasd', result.elwrHeadlessAssets?.css ?? "") + console.log(result.elwrHeadlessAssets?.scriptUrl ?? "") }) - .catch((reason) => { - if (!controller.signal.aborted) setError(String(reason?.message || reason)); + .catch((reason: unknown) => { + if (!active) return; + + setError( + reason instanceof Error + ? reason.message + : "ElevenLabs reader could not be loaded" + ); }); - return () => controller.abort(); + + return () => { + active = false; + }; }, [postId]); + // Load WordPress plugin CSS useEffect(() => { - const styleUrl = data?.elwrHeadlessAssets?.styleUrl; - const scriptUrl = data?.elwrHeadlessAssets?.scriptUrl; - const origin = BACKEND_ORIGIN?.replace(/\/$/, ""); - const allowed = (value: string) => { - try { return new URL(value).origin === new URL(origin!).origin; } - catch { return false; } - }; - if (styleUrl && allowed(styleUrl) && !loadedStyles.has(styleUrl)) { - const link = document.createElement("link"); - link.rel = "stylesheet"; - link.href = styleUrl; - link.dataset.elwrAsset = "style"; - document.head.appendChild(link); - loadedStyles.add(styleUrl); + if (!css) return; + + const id = "elevenlabs-dynamic-css"; + + let style = document.getElementById(id) as HTMLStyleElement | null; + + if (!style) { + style = document.createElement("style"); + style.id = id; + document.head.appendChild(style); } - if (scriptUrl && allowed(scriptUrl) && !loadedScripts.has(scriptUrl)) { - const script = document.createElement("script"); - script.src = scriptUrl; - script.async = true; - script.dataset.elwrAsset = "script"; - document.body.appendChild(script); - loadedScripts.add(scriptUrl); + + style.textContent = css; + }, [css]); + + // Load WordPress plugin JavaScript + useEffect(() => { + if (!scriptUrl) return; + + const id = "elevenlabs-reader-script"; + + let script = document.getElementById(id) as HTMLScriptElement | null; + + if (script?.src === scriptUrl) return; + + if (script) { + script.remove(); } - }, [data]); - if (error) return

Audio reader: {error}

; - if (!data) return

Loading audio player…

; - if (!data.post?.elwrReaderHtml) return

Audio player is not available for this article. Check the shortcode and generated audio in WordPress.

; - // Only inject shortcode HTML returned by the trusted, configured WordPress backend. - return
; + script = document.createElement("script"); + script.id = id; + script.src = scriptUrl; + script.async = true; + + script.onerror = () => { + setError("ElevenLabs player JavaScript failed to load."); + }; + + document.body.appendChild(script); + + return () => { + // Keep the shared script loaded; its click handler uses event delegation. + }; + }, [scriptUrl]); + + if (error) { + return

{error}

; + } + + if (!html) return null; + + return ( +
+ ); } diff --git a/apps/storefront/src/components/wordpressShortcodes.tsx b/apps/storefront/src/components/wordpressShortcodes.tsx index 323af81..61fcc26 100644 --- a/apps/storefront/src/components/wordpressShortcodes.tsx +++ b/apps/storefront/src/components/wordpressShortcodes.tsx @@ -1,4 +1,3 @@ -import { ElevenLabsHeadlessReader } from "./ElevenLabsHeadlessReader"; import { PostCard, PaginablePostGrid, @@ -59,6 +58,7 @@ import { communityMemberMatchesRoles, normalizeCommunityMemberTypes, } from "../lib/communityMemberTypes"; +import { ElevenLabsHeadlessReader } from "./ElevenLabsHeadlessReader"; export type WordPressShortcodeAttributes = Record; export type WordPressShortcodeRenderer = (attributes: WordPressShortcodeAttributes) => ReactNode; @@ -285,47 +285,46 @@ function SliderShortcode({ attributes }: ShortcodeProps) { renderItem={(slide, index, meta) => { const SlideHeading = index === 0 ? firstSlideHeadingLevel : slideHeadingLevel; return ( -
- {slide.image ? : null} -
+ {slide.image ? : null} + @@ -414,11 +420,10 @@ function TableOfContents({ entries }: { entries: TocEntry[] }) { {entry.text} @@ -443,24 +448,21 @@ function TocDotRail({ entries, side }: { entries: TocEntry[]; side: "left" | "ri return (