mirror of
https://github.com/FlipsideCrypto/near-bos-gateway.git
synced 2026-02-06 03:07:12 +00:00
Introduce cloudflare workers to generate static previews (#179)
Use cloudflare's functions to generate and inject static metadata as well as noscript content for crawlers and previews
This commit is contained in:
parent
9d33d8821f
commit
e4dde5449f
1
.gitignore
vendored
1
.gitignore
vendored
@ -18,6 +18,7 @@
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
.wrangler/
|
||||
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
|
||||
323
functions/[[accountId]]/widget/[[index]].js
Normal file
323
functions/[[accountId]]/widget/[[index]].js
Normal file
@ -0,0 +1,323 @@
|
||||
import { Buffer } from "node:buffer";
|
||||
|
||||
class MetaTitleInjector {
|
||||
constructor({ title }) {
|
||||
this.title = title;
|
||||
}
|
||||
|
||||
element(element) {
|
||||
element.setAttribute("content", this.title);
|
||||
}
|
||||
}
|
||||
|
||||
class MetaImageInjector {
|
||||
constructor({ image, authorImage }) {
|
||||
this.image = image;
|
||||
this.authorImage = authorImage;
|
||||
}
|
||||
|
||||
element(element) {
|
||||
if (this.image) {
|
||||
element.setAttribute("content", this.image);
|
||||
} else if (this.authorImage) {
|
||||
element.setAttribute("content", this.authorImage);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class MetaTwitterCardInjector {
|
||||
constructor({ image }) {
|
||||
this.image = image;
|
||||
}
|
||||
|
||||
element(element) {
|
||||
if (!this.image) {
|
||||
element.setAttribute("content", "summary");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class MetaDescriptionInjector {
|
||||
constructor({ shortDescription }) {
|
||||
this.shortDescription = shortDescription;
|
||||
}
|
||||
|
||||
element(element) {
|
||||
element.setAttribute(
|
||||
"content",
|
||||
this.shortDescription?.replaceAll("\n", " ")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class TitleInjector {
|
||||
constructor({ title }) {
|
||||
this.title = title;
|
||||
}
|
||||
|
||||
element(element) {
|
||||
element.setInnerContent(this.title);
|
||||
}
|
||||
}
|
||||
|
||||
class NoscriptDescriptionInjector {
|
||||
constructor({ description }) {
|
||||
this.description = description;
|
||||
}
|
||||
|
||||
element(element) {
|
||||
element.setInnerContent(this.description);
|
||||
}
|
||||
}
|
||||
|
||||
function defaultData() {
|
||||
const image = "https://near.social/assets/logo.png";
|
||||
const title = "Near Social";
|
||||
const description = "Social data protocol built on NEAR";
|
||||
return {
|
||||
image,
|
||||
title,
|
||||
description,
|
||||
};
|
||||
}
|
||||
|
||||
async function socialGet(keys, blockHeight, parse) {
|
||||
const request = await fetch("https://api.near.social/get", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
keys: [keys],
|
||||
blockHeight,
|
||||
}),
|
||||
});
|
||||
let data = await request.json();
|
||||
const parts = keys.split("/");
|
||||
for (let i = 0; i < parts.length; i++) {
|
||||
const part = parts[i];
|
||||
if (part === "*" || part === "**") {
|
||||
break;
|
||||
}
|
||||
data = data?.[part];
|
||||
}
|
||||
if (parse) {
|
||||
try {
|
||||
data = JSON.parse(data);
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
async function viewCall({ contractId, method, args }) {
|
||||
const res = await fetch("https://rpc.mainnet.near.org", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
jsonrpc: "2.0",
|
||||
id: "dontcare",
|
||||
method: "query",
|
||||
params: {
|
||||
request_type: "call_function",
|
||||
finality: "final",
|
||||
account_id: contractId,
|
||||
method_name: method,
|
||||
args_base64: btoa(JSON.stringify(args)),
|
||||
},
|
||||
}),
|
||||
});
|
||||
const json = await res.json();
|
||||
const result = Buffer.from(json.result.result).toString("utf-8");
|
||||
return JSON.parse(result);
|
||||
}
|
||||
|
||||
async function nftToImageUrl({ contractId, tokenId }) {
|
||||
const [token, nftMetadata] = await Promise.all([
|
||||
viewCall({
|
||||
contractId,
|
||||
method: "nft_token",
|
||||
args: { token_id: tokenId },
|
||||
}),
|
||||
viewCall({
|
||||
contractId,
|
||||
method: "nft_metadata",
|
||||
args: {},
|
||||
}),
|
||||
]);
|
||||
|
||||
const tokenMetadata = token?.metadata || {};
|
||||
const tokenMedia = tokenMetadata.media || "";
|
||||
|
||||
let imageUrl =
|
||||
tokenMedia.startsWith("https://") ||
|
||||
tokenMedia.startsWith("http://") ||
|
||||
tokenMedia.startsWith("data:image")
|
||||
? tokenMedia
|
||||
: nftMetadata.base_uri
|
||||
? `${nftMetadata.base_uri}/${tokenMedia}`
|
||||
: tokenMedia.startsWith("Qm") || tokenMedia.startsWith("ba")
|
||||
? `https://ipfs.near.social/ipfs/${tokenMedia}`
|
||||
: tokenMedia;
|
||||
|
||||
if (!tokenMedia && tokenMetadata.reference) {
|
||||
const metadataUrl =
|
||||
nftMetadata.base_uri === "https://arweave.net" &&
|
||||
!tokenMetadata.reference.startsWith("https://")
|
||||
? `${nftMetadata.base_uri}/${tokenMetadata.reference}`
|
||||
: tokenMetadata.reference.startsWith("https://") ||
|
||||
tokenMetadata.reference.startsWith("http://")
|
||||
? tokenMetadata.reference
|
||||
: tokenMetadata.reference.startsWith("ar://")
|
||||
? `https://arweave.net/${tokenMetadata.reference.split("//")[1]}`
|
||||
: null;
|
||||
if (metadataUrl) {
|
||||
const res = await fetch(metadataUrl);
|
||||
const json = await res.json();
|
||||
imageUrl = json.media;
|
||||
}
|
||||
}
|
||||
|
||||
return imageUrl;
|
||||
}
|
||||
|
||||
function wrapImage(url) {
|
||||
return url ? `https://i.near.social/large/${url}` : null;
|
||||
}
|
||||
|
||||
async function internalImageToUrl(env, image) {
|
||||
if (image?.url) {
|
||||
return image.url;
|
||||
} else if (image?.ipfs_cid) {
|
||||
return `https://ipfs.near.social/ipfs/${image.ipfs_cid}`;
|
||||
} else if (image?.nft) {
|
||||
try {
|
||||
const { contractId, tokenId } = image.nft;
|
||||
const NftKV = env.NftKV;
|
||||
|
||||
let imageUrl = await NftKV.get(`${contractId}/${tokenId}`);
|
||||
if (!imageUrl) {
|
||||
imageUrl = await nftToImageUrl({ contractId, tokenId });
|
||||
if (imageUrl) {
|
||||
await NftKV.put(`${contractId}/${tokenId}`, imageUrl);
|
||||
}
|
||||
}
|
||||
return imageUrl;
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
async function imageToUrl(env, image) {
|
||||
return wrapImage(await internalImageToUrl(env, image));
|
||||
}
|
||||
|
||||
async function postData(env, url, data, isPost) {
|
||||
const accountId = url.searchParams.get("accountId");
|
||||
const blockHeight = url.searchParams.get("blockHeight");
|
||||
const [content, name, authorImage] = await Promise.all([
|
||||
socialGet(
|
||||
`${accountId}/post/${isPost ? "main" : "comment"}`,
|
||||
blockHeight,
|
||||
true
|
||||
),
|
||||
socialGet(`${accountId}/profile/name`),
|
||||
socialGet(`${accountId}/profile/image/**`),
|
||||
]);
|
||||
|
||||
data.raw = content;
|
||||
data.description = content?.text || "";
|
||||
data.image = await imageToUrl(env, content?.image);
|
||||
if (!data.image) {
|
||||
data.authorImage = await imageToUrl(env, authorImage);
|
||||
}
|
||||
data.title = isPost
|
||||
? `Post by ${name ?? accountId} | Near Social`
|
||||
: `Comment by ${name ?? accountId} | Near Social`;
|
||||
data.accountName = name;
|
||||
data.accountId = accountId;
|
||||
}
|
||||
|
||||
async function profileData(env, url, data) {
|
||||
const accountId = url.searchParams.get("accountId");
|
||||
const profile = await socialGet(`${accountId}/profile/**`);
|
||||
|
||||
const name = profile?.name;
|
||||
data.raw = profile;
|
||||
data.description =
|
||||
profile?.description || `Profile of ${accountId} on Near Social`;
|
||||
data.image = await imageToUrl(env, profile?.image);
|
||||
data.authorImage =
|
||||
data.image ||
|
||||
wrapImage(
|
||||
"https://ipfs.near.social/ipfs/bafkreibmiy4ozblcgv3fm3gc6q62s55em33vconbavfd2ekkuliznaq3zm"
|
||||
);
|
||||
data.title = name
|
||||
? `${name} (${accountId}) | Near Social`
|
||||
: `${accountId} | Near Social`;
|
||||
data.accountName = name;
|
||||
data.accountId = accountId;
|
||||
}
|
||||
|
||||
async function widgetData(env, url, data) {
|
||||
const parts = url.pathname.split("/");
|
||||
const accountId = parts[1];
|
||||
const widgetId = parts[3];
|
||||
const metadata = await socialGet(
|
||||
`${accountId}/widget/${widgetId}/metadata/**`
|
||||
);
|
||||
|
||||
const name = metadata?.name || widgetId;
|
||||
data.raw = metadata;
|
||||
data.description =
|
||||
metadata?.description || `Component ${name} created by ${accountId}`;
|
||||
data.image = await imageToUrl(env, metadata?.image);
|
||||
data.title = `${name} by ${accountId} | Near Social`;
|
||||
data.accountName = name;
|
||||
data.accountId = accountId;
|
||||
}
|
||||
|
||||
async function generateData(env, url) {
|
||||
const data = defaultData();
|
||||
try {
|
||||
if (url.pathname === "/mob.near/widget/MainPage.Post.Page") {
|
||||
await postData(env, url, data, true);
|
||||
} else if (url.pathname === "/mob.near/widget/MainPage.Comment.Page") {
|
||||
await postData(env, url, data, false);
|
||||
} else if (url.pathname === "/mob.near/widget/ProfilePage") {
|
||||
await profileData(env, url, data);
|
||||
} else {
|
||||
await widgetData(env, url, data);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
data.shortDescription = data.description.slice(0, 250);
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
export async function onRequest({ request, next, env }) {
|
||||
const url = new URL(request.url);
|
||||
if (url.pathname.split("/").length < 4) {
|
||||
return next();
|
||||
}
|
||||
const data = await generateData(env, url);
|
||||
return (
|
||||
new HTMLRewriter()
|
||||
.on('meta[property="og:title"]', new MetaTitleInjector(data))
|
||||
.on('meta[property="og:image"]', new MetaImageInjector(data))
|
||||
.on('meta[name="twitter:card"]', new MetaTwitterCardInjector(data))
|
||||
.on('meta[property="og:description"]', new MetaDescriptionInjector(data))
|
||||
.on('meta[name="description"]', new MetaDescriptionInjector(data))
|
||||
// .on("head", new MetaTagInjector(data))
|
||||
.on("title", new TitleInjector(data))
|
||||
.on("noscript", new NoscriptDescriptionInjector(data))
|
||||
.transform(await next())
|
||||
);
|
||||
}
|
||||
@ -5,27 +5,28 @@
|
||||
<link rel="icon" href="/favicon.png" />
|
||||
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta
|
||||
name="description"
|
||||
content="Social data protocol built on NEAR"
|
||||
/>
|
||||
<meta name="twitter:card" content="summary_large_image" />
|
||||
<meta name="twitter:site" content="@NearSocial_">
|
||||
<meta name="twitter:image" content="https://near.social/assets/logo.png">
|
||||
<meta property="og:image" content="https://near.social/assets/logo.png">
|
||||
<meta property="og:type" content="website">
|
||||
<meta property="og:title" content="Near Social" />
|
||||
<meta property="og:description" content="Social data protocol built on NEAR" />
|
||||
<link rel="apple-touch-icon" href="/favicon.png" />
|
||||
<!--
|
||||
manifest.json provides metadata used when your web app is installed on a
|
||||
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
|
||||
-->
|
||||
<link rel="manifest" href="/manifest.json" />
|
||||
<meta
|
||||
name="description"
|
||||
content="Social data protocol built on NEAR"
|
||||
/>
|
||||
<meta name="twitter:card" content="summary_large_image" />
|
||||
<meta name="twitter:site" content="@NearSocial_">
|
||||
<meta property="og:image" content="https://near.social/assets/logo.png">
|
||||
<meta property="og:type" content="website">
|
||||
<meta property="og:title" content="Near Social" />
|
||||
<meta property="og:description" content="Social data protocol built on NEAR" />
|
||||
<title>Near Social</title>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
<noscript>
|
||||
You need to enable JavaScript to run this app.
|
||||
</noscript>
|
||||
<div id="root"></div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user