diff --git a/CHANGELOG.md b/CHANGELOG.md index 821aa1490..3ab5746ba 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,16 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [1.125.0] - Not released +### Added +- Add OpenGraphPreview component and integrate into LinkPreview + - Add OpenGraphPreview component to fetch and parse Open Graph metadata from pages + - Integrated OpenGraphPreview into LinkPreview to show open graph preview if no other specific preview available for a URL + - Created thoughtful styling for the OpenGraphPreview for a clean, polished, and readable design + - OpenGraphPreview provides a fallback preview option for sites that implement Open Graph metadata + - Parsing logic simplified using DOMParser and meta tag selectors + +### Changed +- Update LinkPreview component to check for Open Graph data and show OpenGraphPreview ## [1.124.2] - 2023-10-25 ### Fixed diff --git a/src/components/link-preview/open-graph.jsx b/src/components/link-preview/open-graph.jsx new file mode 100644 index 000000000..1d9f22dd0 --- /dev/null +++ b/src/components/link-preview/open-graph.jsx @@ -0,0 +1,42 @@ +import { useEffect, useState } from 'react'; + +export default function OpenGraphPreview({ url }) { + const [data, setData] = useState(null); + + useEffect(() => { + if (url.startsWith(window.location.origin)) { + return; + } + async function fetchData() { + const response = await fetch(`https://corsproxy.io/?${encodeURIComponent(url)}`); + const html = await response.text(); + const doc = new DOMParser().parseFromString(html, 'text/html'); + + const metaContent = (property) => + doc.querySelector(`meta[property="${property}"]`)?.getAttribute('content'); + + const [title, description, image, source] = [ + 'og:title', + 'og:description', + 'og:image', + 'og:site_name', + ].map(metaContent); + setData({ title, description, image, source }); + } + fetchData(); + }, [url]); + if (!data || data.title === undefined) { + return null; + } + + return ( +
+ {data.image && {data.title}} +
+
{data.title}
+
{data.description}
+
{data.source}
+
+
+ ); +} diff --git a/src/components/link-preview/preview.jsx b/src/components/link-preview/preview.jsx index 39a8b51a5..fca83c35b 100644 --- a/src/components/link-preview/preview.jsx +++ b/src/components/link-preview/preview.jsx @@ -11,7 +11,7 @@ import TikTokPreview, { canShowURL as tikTokCanShowURL } from './tiktok'; import SoundCloudPreview, { canShowURL as soundCloudCanShowURL } from './soundcloud'; import SpotifyPreview, { canShowURL as spotifyCanShowURL } from './spotify'; import AppleMusicPreview, { canShowUrl as appleMusicCanShowURL } from './apple-music'; - +import OpenGraphPreview from './open-graph'; import EmbedlyPreview from './embedly'; export default function LinkPreview({ allowEmbedly, url }) { @@ -41,12 +41,10 @@ export default function LinkPreview({ allowEmbedly, url }) { } else if (appleMusicCanShowURL(url)) { return ; } - if (allowEmbedly) { return ; } - - return false; + return ; } LinkPreview.propTypes = { diff --git a/styles/shared/post.scss b/styles/shared/post.scss index 3618fd80b..32b7b44ef 100644 --- a/styles/shared/post.scss +++ b/styles/shared/post.scss @@ -772,6 +772,48 @@ $post-line-height: rem(20px); .tiktok-video-iframe { width: 100%; } + + .opengraph-preview { + max-width: 500px; + border-radius: 10px; + padding: 10px; + margin-top: 10px; + margin-bottom: 10px; + background-color: #f8f8f8; + border: 1px solid #555; + .dark-theme & { + background-color: #222; + } + } + + .opengraph-image { + max-width: 100%; + max-height: 300px; + } + + .opengraph-title { + font-size: 1.2em; + margin-top: 0.5em; + color: #111; + .dark-theme & { + color: #d9d9d9; + } + } + + .opengraph-description { + font-size: 1em; + margin-top: 0.5em; + color: #333; + .dark-theme & { + color: #a9a9a9; + } + } + + .opengraph-source { + font-size: 1em; + margin-top: 0.5em; + color: #777; + } } .submit-mode-hint {