diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index e5ffec09..8dbf922f 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -76,9 +76,7 @@ You should now be able to access your local JSON Hero server on [localhost:8787] ### Previewing URLs -We currently use [Peekalink](https://www.peekalink.io) to power some of the Preview URL functionality. This feature is disabled unless there is a valid `PEEKALINK_API_KEY` environment variable set in your `.env` file created above. - -If you'd like to enable this functionality locally, signup for Peekalink and set the `PEEKALINK_API_KEY` +We currently use [OpenGraph Ninja](https://opengraph.ninja/) to power some of the Preview URL functionality. ### Deploying to Cloudflare diff --git a/app/bindings.d.ts b/app/bindings.d.ts index 5751bfad..36789ec4 100644 --- a/app/bindings.d.ts +++ b/app/bindings.d.ts @@ -5,6 +5,5 @@ declare global { const SESSION_SECRET: string; const GRAPH_JSON_API_KEY: string; const GRAPH_JSON_COLLECTION: string; - const PEEKALINK_API_KEY: string; const APIHERO_PROJECT_KEY: string; } diff --git a/app/components/Preview/Types/PreviewHtml.tsx b/app/components/Preview/Types/PreviewHtml.tsx index 35e67052..6fab1690 100644 --- a/app/components/Preview/Types/PreviewHtml.tsx +++ b/app/components/Preview/Types/PreviewHtml.tsx @@ -1,78 +1,13 @@ -import { - ArrowRightIcon, - CalendarIcon, - EyeIcon, - ThumbUpIcon, -} from "@heroicons/react/outline"; -import { inferType } from "@jsonhero/json-infer-types"; import { Body } from "~/components/Primitives/Body"; import { Title } from "~/components/Primitives/Title"; -import { formatNumber, formatValue } from "~/utilities/formatter"; import { PreviewBox } from "../PreviewBox"; -import { PreviewProperties, PreviewProperty } from "../PreviewProperties"; import { PreviewHtml } from "./preview.types"; -import { RetweetIcon } from "./RetweetIcon"; export type PreviewHtmlProps = { info: PreviewHtml; }; export function PreviewHtml({ info }: PreviewHtmlProps) { - const formatDate = (dateString: string): string => { - return formatValue(inferType(dateString)) ?? dateString; - }; - - const details = () => { - if (!info.details) { - return <>; - } - - switch (info.details.type) { - case "youtube": { - const properties: Array = [ - { - key: "likeCount", - title: formatNumber(info.details.likeCount), - icon: , - }, - { - key: "viewCount", - title: formatNumber(info.details.viewCount), - icon: , - }, - { - key: "date", - title: formatDate(info.details.publishedAt), - icon: , - }, - ]; - return ; - } - case "twitter": { - const properties: Array = [ - { - key: "likeCount", - title: formatNumber(info.details.likesCount), - icon: , - }, - { - key: "retweetCount", - title: formatNumber(info.details.retweetCount), - icon: , - }, - { - key: "date", - title: formatDate(info.details.publishedAt), - icon: , - }, - ]; - return ; - } - } - - return <>; - }; - return (
@@ -88,10 +23,9 @@ export function PreviewHtml({ info }: PreviewHtmlProps) {
{info.image && (
- + {info.image?.alt}
)} - {details()}
); } diff --git a/app/components/Preview/Types/preview.types.d.ts b/app/components/Preview/Types/preview.types.d.ts index cf9467a0..c0c974ca 100644 --- a/app/components/Preview/Types/preview.types.d.ts +++ b/app/components/Preview/Types/preview.types.d.ts @@ -56,6 +56,223 @@ declare type TwitterLinkDetails = { declare type ImageAssetDetails = { url: string; - width: number; - height: number; + alt?: string; + width?: number; + height?: number; +}; + +/* OpenGraph Ninja Types */ +// Types adapted from https://github.com/opengraphninja/react/blob/main/src/types.d.ts +type OpenGraphMedia = { + height: string | null; + type: string | null; + url: string; + width: string | null; +}; + +type OpenGraphTwitterImage = { + height: string | null; + alt: string | null; + url: string; + width: string | null; +}; + +type OpenGraphTwitterPlayer = { + height: string | null; + stream: string | null; + url: string; + width: string | null; +}; + +type OpenGraphMusicSong = { + url: string; + track: string | null; + disc: string | null; +}; + +type OpenGraphDetails = { + alAndroidAppName?: string; + alAndroidClass?: string; + alAndroidPackage?: string; + alAndroidUrl?: string; + alIosAppName?: string; + alIosAppStoreId?: string; + alIosUrl?: string; + alIpadAppName?: string; + alIpadAppStoreId?: string; + alIpadUrl?: string; + alIphoneAppName?: string; + alIphoneAppStoreId?: string; + alIphoneUrl?: string; + alWebShouldFallback?: string; + alWebUrl?: string; + alWindowsAppId?: string; + alWindowsAppName?: string; + alWindowsPhoneAppId?: string; + alWindowsPhoneAppName?: string; + alWindowsPhoneUrl?: string; + alWindowsUniversalAppId?: string; + alWindowsUniversalAppName?: string; + alWindowsUniversalUrl?: string; + alWindowsUrl?: string; + articleAuthor?: string; + articleExpirationTime?: string; + articleModifiedTime?: string; + articlePublishedTime?: string; + articlePublisher?: string; + articleSection?: string; + articleTag?: string; + author?: string; + bookAuthor?: string; + bookCanonicalName?: string; + bookIsbn?: string; + bookReleaseDate?: string; + booksBook?: string; + booksRatingScale?: string; + booksRatingValue?: string; + bookTag?: string; + businessContactDataCountryName?: string; + businessContactDataLocality?: string; + businessContactDataPostalCode?: string; + businessContactDataRegion?: string; + businessContactDataStreetAddress?: string; + dcContributor?: string; + dcCoverage?: string; + dcCreator?: string; + dcDate?: string; + dcDateCreated?: string; + dcDateIssued?: string; + dcDescription?: string; + dcFormatMedia?: string; + dcFormatSize?: string; + dcIdentifier?: string; + dcLanguage?: string; + dcPublisher?: string; + dcRelation?: string; + dcRights?: string; + dcSource?: string; + dcSubject?: string; + dcTitle?: string; + dcType?: string; + modifiedTime?: string; + musicAlbum?: string | string[]; + musicAlbumDisc?: string; + musicAlbumTrack?: string; + musicAlbumUrl?: string; + musicCreator?: string | string[]; + musicDuration?: string; + musicMusician?: string | string[]; + musicReleaseDate?: string; + musicSong?: OpenGraphMusicSong; + musicSongDisc?: string | string[]; + musicSongTrack?: string | string[]; + musicSongUrl?: string | string[]; + ogArticleAuthor?: string; + ogArticleExpirationTime?: string; + ogArticleModifiedTime?: string; + ogArticlePublishedTime?: string; + ogArticlePublisher?: string; + ogArticleSection?: string; + ogArticleTag?: string; + ogAudio?: string; + ogAudioSecureURL?: string; + ogAudioType?: string; + ogAudioURL?: string; + ogAvailability?: string; + ogDate?: string; + ogDescription?: string; + ogDeterminer?: string; + ogImage?: OpenGraphMedia | OpenGraphMedia[]; + ogImageHeight?: string | string[]; + ogImageSecureURL?: string | string[]; + ogImageType?: string | string[]; + ogImageURL?: string | string[]; + ogImageWidth?: string | string[]; + ogLocale?: string; + ogLocaleAlternate?: string; + ogLogo?: string; + ogPriceAmount?: string; + ogPriceCurrency?: string; + ogProductAvailability?: string; + ogProductCondition?: string; + ogProductPriceAmount?: string; + ogProductPriceCurrency?: string; + ogProductRetailerItemId?: string; + ogSiteName?: string; + ogTitle?: string; + ogType?: string; + ogUrl?: string; + ogVideo?: OpenGraphMedia | OpenGraphMedia[]; + ogVideoActorId?: string | string[]; + ogVideoHeight?: string | string[]; + ogVideoSecureURL?: string | string[]; + ogVideoType?: string | string[]; + ogVideoWidth?: string | string[]; + placeLocationLatitude?: string; + placeLocationLongitude?: string; + profileFirstName?: string; + profileGender?: string; + profileLastName?: string; + profileUsername?: string; + publishedTime?: string; + releaseDate?: string; + restaurantContactInfoCountryName?: string; + restaurantContactInfoEmail?: string; + restaurantContactInfoLocality?: string; + restaurantContactInfoPhoneNumber?: string; + restaurantContactInfoPostalCode?: string; + restaurantContactInfoRegion?: string; + restaurantContactInfoStreetAddress?: string; + restaurantContactInfoWebsite?: string; + restaurantMenu?: string; + restaurantRestaurant?: string; + restaurantSection?: string; + restaurantVariationPriceAmount?: string; + restaurantVariationPriceCurrency?: string; + twitterAppIdGooglePlay?: string; + twitterAppIdiPad?: string; + twitterAppIdiPhone?: string; + twitterAppNameGooglePlay?: string; + twitterAppNameiPad?: string; + twitterAppNameiPhone?: string; + twitterAppUrlGooglePlay?: string; + twitterAppUrliPad?: string; + twitterAppUrliPhone?: string; + twitterCard?: string; + twitterCreator?: string; + twitterCreatorId?: string; + twitterDescription?: string; + twitterImage?: OpenGraphTwitterImage | OpenGraphTwitterImage[]; + twitterImageAlt?: string | string[]; + twitterImageHeight?: string | string[]; + twitterImageSrc?: string | string[]; + twitterImageWidth?: string | string[]; + twitterPlayer?: OpenGraphTwitterPlayer | OpenGraphTwitterPlayer[]; + twitterPlayerHeight?: string | string[]; + twitterPlayerStream?: string | string[]; + twitterPlayerStreamContentType?: string | string[]; + twitterPlayerWidth?: string | string[]; + twitterSite?: string; + twitterSiteId?: string; + twitterTitle?: string; + twitterUrl?: string; + updatedTime?: string; + favicon?: string; + [key: string]: any; +}; + +export type OpenGraphPreviewData = { + hostname: string; + requestUrl: string; + title: string; + description: string; + image?: { + url: string; + alt?: string; + }; + details: Details; +}; + +export type OpenGraphPreviewDataError = { + error: string; }; diff --git a/app/services/uriPreview.server.ts b/app/services/uriPreview.server.ts index 78e6800c..aca8cbd2 100644 --- a/app/services/uriPreview.server.ts +++ b/app/services/uriPreview.server.ts @@ -2,6 +2,8 @@ import { PreviewImage, PreviewJson, PreviewResult, + OpenGraphPreviewData, + OpenGraphPreviewDataError } from "~/components/Preview/Types/preview.types"; import safeFetch from "~/utilities/safeFetch"; import { fetchProxy } from "./apihero.server"; @@ -14,25 +16,29 @@ const imageContentTypes = [ "image/svg+xml", ]; -async function getPeekalink(link: string): Promise { - if (typeof PEEKALINK_API_KEY === "undefined") { - return { error: "Preview unavailable" }; - } - - const response = await fetchProxy("https://api.peekalink.io/", { - method: "POST", - headers: { - "X-API-Key": PEEKALINK_API_KEY, - "content-type": "application/json", - }, - body: JSON.stringify({ link }), - }); +async function getOpenGraphNinja(link: string): Promise { + const response = await fetchProxy(`https://opengraph.ninja/api/v1?url=${link}`); if (response.ok) { - const result = await response.json(); - - return result; + const body: OpenGraphPreviewData = await response.json(); + return { + url: body.requestUrl, + contentType: 'html', + mimeType: 'text/html', + title: body.title, + description: body.description, + icon: { url: body.details.favicon ?? '' }, + image: { + url: body.image?.url ?? '', + alt: body.image?.alt + } + }; } else { + const body: OpenGraphPreviewDataError = await response.json(); + + // Log the error instead of propagating the internal error to the UI + console.log(`OpenGraph Ninja failed to get preview data: ${body.error}`); + return { error: "No preview available for this URL" }; } } @@ -71,9 +77,7 @@ export async function getUriPreview(uri: string): Promise { return createPreviewJson(url.href, jsonBody); } - const peekalinkResult = await getPeekalink(url.href); - - return peekalinkResult; + return await getOpenGraphNinja(url.href); } type HeadInfo = { diff --git a/wrangler.toml b/wrangler.toml index c3ab3cf5..737fb4da 100644 --- a/wrangler.toml +++ b/wrangler.toml @@ -36,5 +36,4 @@ GRAPH_JSON_COLLECTION = "jsonhero-prod" # Secrets # [SESSION_STORAGE] # [GRAPH_JSON_API_KEY] -# [PEEKALINK_API_KEY] -# [APIHERO_PROJECT_KEY] \ No newline at end of file +# [APIHERO_PROJECT_KEY] diff --git a/wrangler.toml.dev b/wrangler.toml.dev index 508b9f6c..3a8f4939 100644 --- a/wrangler.toml.dev +++ b/wrangler.toml.dev @@ -33,4 +33,3 @@ GRAPH_JSON_COLLECTION = "jsonhero-prod" # Secrets # [SESSION_STORAGE] # [GRAPH_JSON_API_KEY] -# [PEEKALINK_API_KEY] \ No newline at end of file