refactor(components): move from genHTML to hono jsx
refactor(post/image): parse and validate embed data from Bluesky to sort which image to show feat(post/description): add quote replies feat(post/details): add post details (likes, reskeets, comments) feat(profiles): add profile embed fix: added domain key to env vars so that oembed works locally and in self-hosted instances
This commit is contained in:
30
src/components/Layout.tsx
Normal file
30
src/components/Layout.tsx
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
import { html } from "hono/html";
|
||||||
|
|
||||||
|
export interface LayoutProps {
|
||||||
|
url: string;
|
||||||
|
children: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Layout = ({ url, children }: LayoutProps) => {
|
||||||
|
const removeLeadingSlash = url.substring(1);
|
||||||
|
const redirectUrl = removeLeadingSlash.startsWith("https://")
|
||||||
|
? removeLeadingSlash
|
||||||
|
: `https://bsky.app/${removeLeadingSlash}`;
|
||||||
|
return html`
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<link rel="canonical" href="${url.substring(1)}" />
|
||||||
|
<meta content="text/html; charset=UTF-8" http-equiv="Content-Type" />
|
||||||
|
<meta content="#0085ff" name="theme-color" />
|
||||||
|
<meta property="og:site_name" content="FixBluesky" />
|
||||||
|
|
||||||
|
${children}
|
||||||
|
</head>
|
||||||
|
</html>
|
||||||
|
`;
|
||||||
|
};
|
||||||
|
|
||||||
|
{
|
||||||
|
/* <meta http-equiv="refresh" content="0;url=${redirectUrl}" /> */
|
||||||
|
}
|
||||||
38
src/components/Post.tsx
Normal file
38
src/components/Post.tsx
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
import { AppBskyFeedDefs } from "@atproto/api";
|
||||||
|
|
||||||
|
import { Layout } from "./Layout";
|
||||||
|
import { OEmbedTypes } from "../routes/getOEmbed";
|
||||||
|
import { parseEmbedImage } from "../lib/parseEmbedImage";
|
||||||
|
import { parseEmbedDescription } from "../lib/parseEmbedDescription";
|
||||||
|
|
||||||
|
interface PostProps {
|
||||||
|
post: AppBskyFeedDefs.PostView;
|
||||||
|
url: string;
|
||||||
|
appDomain: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Post = ({ post, url, appDomain }: PostProps) => (
|
||||||
|
<Layout url={url}>
|
||||||
|
<meta name="twitter:creator" content={`@${post.author.handle}`} />
|
||||||
|
<meta property="og:description" content={parseEmbedDescription(post)} />
|
||||||
|
<meta
|
||||||
|
property="og:title"
|
||||||
|
content={`${post.author.displayName} (@${post.author.handle})`}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{!(parseEmbedImage(post) === post.author.avatar) && (
|
||||||
|
<meta name="twitter:card" content="summary_large_image" />
|
||||||
|
)}
|
||||||
|
|
||||||
|
<meta property="og:image" content={parseEmbedImage(post)} />
|
||||||
|
|
||||||
|
<link
|
||||||
|
type="application/json+oembed"
|
||||||
|
href={`https:/${appDomain}/oembed?type=${OEmbedTypes.Post}&replies=${
|
||||||
|
post.replyCount
|
||||||
|
}&reposts=${post.repostCount}&likes=${
|
||||||
|
post.likeCount
|
||||||
|
}&avatar=${encodeURIComponent(post.author.avatar ?? "")}`}
|
||||||
|
/>
|
||||||
|
</Layout>
|
||||||
|
);
|
||||||
31
src/components/Profile.tsx
Normal file
31
src/components/Profile.tsx
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
import { AppBskyActorDefs } from "@atproto/api";
|
||||||
|
|
||||||
|
import { Layout } from "./Layout";
|
||||||
|
import { OEmbedTypes } from "../routes/getOEmbed";
|
||||||
|
|
||||||
|
interface ProfileProps {
|
||||||
|
profile: AppBskyActorDefs.ProfileViewDetailed;
|
||||||
|
url: string;
|
||||||
|
appDomain: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Profile = ({ profile, url, appDomain }: ProfileProps) => (
|
||||||
|
<Layout url={url}>
|
||||||
|
<meta name="twitter:creator" content={`@${profile.handle}`} />
|
||||||
|
<meta property="og:description" content={profile.description ?? ""} />
|
||||||
|
<meta
|
||||||
|
property="og:title"
|
||||||
|
content={`${profile.displayName} (@${profile.handle})`}
|
||||||
|
/>
|
||||||
|
<meta property="og:image" content={profile.avatar} />
|
||||||
|
|
||||||
|
<link
|
||||||
|
type="application/json+oembed"
|
||||||
|
href={`https://${appDomain}/oembed?type=${OEmbedTypes.Profile}&follows=${
|
||||||
|
profile.followsCount
|
||||||
|
}&posts=${profile.postsCount}&avatar=${encodeURIComponent(
|
||||||
|
profile.avatar ?? ""
|
||||||
|
)}`}
|
||||||
|
/>
|
||||||
|
</Layout>
|
||||||
|
);
|
||||||
1
src/globals.d.ts
vendored
1
src/globals.d.ts
vendored
@@ -6,6 +6,7 @@ declare global {
|
|||||||
BSKY_SERVICE_URL: string;
|
BSKY_SERVICE_URL: string;
|
||||||
BSKY_AUTH_USERNAME: string;
|
BSKY_AUTH_USERNAME: string;
|
||||||
BSKY_AUTH_PASSWORD: string;
|
BSKY_AUTH_PASSWORD: string;
|
||||||
|
FIXBLUESKY_APP_DOMAIN: string;
|
||||||
};
|
};
|
||||||
Variables: {
|
Variables: {
|
||||||
Agent: BskyAgent;
|
Agent: BskyAgent;
|
||||||
|
|||||||
16
src/index.ts
16
src/index.ts
@@ -1,8 +1,10 @@
|
|||||||
import { Hono } from "hono";
|
import { Hono } from "hono";
|
||||||
import { BskyAgent } from "@atproto/api";
|
import { BskyAgent } from "@atproto/api";
|
||||||
import { getPost } from "./lib/getPost";
|
import { getPost } from "./routes/getPost";
|
||||||
import { getPostData } from "./lib/getPostData";
|
import { getPostData } from "./routes/getPostData";
|
||||||
import { getPostOEmbed } from "./lib/getPostOEmbed";
|
import { getOEmbed } from "./routes/getOEmbed";
|
||||||
|
import { getProfileData } from "./routes/getProfileData";
|
||||||
|
import { getProfile } from "./routes/getProfile";
|
||||||
|
|
||||||
const app = new Hono<Env>();
|
const app = new Hono<Env>();
|
||||||
|
|
||||||
@@ -26,6 +28,12 @@ app.get("/https://bsky.app/profile/:user/post/:post", getPost);
|
|||||||
app.get("/profile/:user/post/:post/json", getPostData);
|
app.get("/profile/:user/post/:post/json", getPostData);
|
||||||
app.get("/https://bsky.app/profile/:user/post/:post/json", getPostData);
|
app.get("/https://bsky.app/profile/:user/post/:post/json", getPostData);
|
||||||
|
|
||||||
app.get("/oembed", getPostOEmbed);
|
app.get("/profile/:user", getProfile);
|
||||||
|
app.get("/https://bsky.app/profile/:user", getProfile);
|
||||||
|
|
||||||
|
app.get("/profile/:user/json", getProfileData);
|
||||||
|
app.get("/https://bsky.app/profile/:user/json", getProfileData);
|
||||||
|
|
||||||
|
app.get("/oembed", getOEmbed);
|
||||||
|
|
||||||
export default app;
|
export default app;
|
||||||
|
|||||||
18
src/lib/fetchPostData.ts
Normal file
18
src/lib/fetchPostData.ts
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import { BskyAgent } from "@atproto/api";
|
||||||
|
|
||||||
|
export interface fetchPostOptions {
|
||||||
|
user: string;
|
||||||
|
post: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchPost(
|
||||||
|
agent: BskyAgent,
|
||||||
|
{ user, post }: fetchPostOptions
|
||||||
|
) {
|
||||||
|
const { data: userData } = await agent.getProfile({
|
||||||
|
actor: user,
|
||||||
|
});
|
||||||
|
return agent.getPosts({
|
||||||
|
uris: [`at://${userData.did}/app.bsky.feed.post/${post}`],
|
||||||
|
});
|
||||||
|
}
|
||||||
14
src/lib/fetchProfile.ts
Normal file
14
src/lib/fetchProfile.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import { BskyAgent } from "@atproto/api";
|
||||||
|
|
||||||
|
export interface fetchProfileOptions {
|
||||||
|
user: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchProfile(
|
||||||
|
agent: BskyAgent,
|
||||||
|
{ user }: fetchProfileOptions
|
||||||
|
) {
|
||||||
|
return agent.getProfile({
|
||||||
|
actor: user,
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
import { Handler } from "hono";
|
|
||||||
import { genHTML } from "../util/genHTML";
|
|
||||||
|
|
||||||
export const getPost: Handler<
|
|
||||||
Env,
|
|
||||||
"/profile/:user/post/:post" | "/https://bsky.app/profile/:user/post/:post"
|
|
||||||
> = async (c) => {
|
|
||||||
const { user, post } = c.req.param();
|
|
||||||
const agent = c.get("Agent");
|
|
||||||
const { data: userData } = await agent.getProfile({
|
|
||||||
actor: user,
|
|
||||||
});
|
|
||||||
const { data: postData } = await agent.getPosts({
|
|
||||||
uris: [`at://${userData.did}/app.bsky.feed.post/${post}`],
|
|
||||||
});
|
|
||||||
return c.html(genHTML(postData.posts[0], c.req.path));
|
|
||||||
};
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
import { Handler } from "hono";
|
|
||||||
|
|
||||||
export const getPostOEmbed: Handler<Env, "/oembed"> = async (c) => {
|
|
||||||
const { handle, display_name, avatar } = c.req.query();
|
|
||||||
return c.json({
|
|
||||||
author_name: `${display_name} (@${handle})`,
|
|
||||||
author_url: `https://bsky.app/profile/${handle}`,
|
|
||||||
provider_name: "FixBluesky",
|
|
||||||
provider_url: "https://bsyy.app/",
|
|
||||||
thumbnail_url: avatar,
|
|
||||||
thumbnail_width: 1000,
|
|
||||||
thumbnail_height: 1000,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
39
src/lib/parseEmbedDescription.ts
Normal file
39
src/lib/parseEmbedDescription.ts
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
import {
|
||||||
|
AppBskyEmbedRecord,
|
||||||
|
AppBskyEmbedRecordWithMedia,
|
||||||
|
AppBskyFeedDefs,
|
||||||
|
AppBskyFeedPost,
|
||||||
|
} from "@atproto/api";
|
||||||
|
|
||||||
|
export function parseEmbedDescription(post: AppBskyFeedDefs.PostView) {
|
||||||
|
if (AppBskyFeedPost.isRecord(post.record)) {
|
||||||
|
if (AppBskyEmbedRecord.isView(post.embed)) {
|
||||||
|
const { success: isView } = AppBskyEmbedRecord.validateView(post.embed);
|
||||||
|
if (isView && AppBskyEmbedRecord.isViewRecord(post.embed.record)) {
|
||||||
|
const { success: isViewRecord } = AppBskyEmbedRecord.validateViewRecord(
|
||||||
|
post.embed.record
|
||||||
|
);
|
||||||
|
if (isViewRecord) {
|
||||||
|
// @ts-expect-error For some reason the original post value is typed as {}
|
||||||
|
return `${post.record.text}\n\nQuoting @${post.embed.record.author.handle}\n➥ ${post.embed.record.value.text}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (AppBskyEmbedRecordWithMedia.isView(post.embed)) {
|
||||||
|
const { success: isView } = AppBskyEmbedRecordWithMedia.validateView(
|
||||||
|
post.embed
|
||||||
|
);
|
||||||
|
if (isView && AppBskyEmbedRecord.isViewRecord(post.embed.record.record)) {
|
||||||
|
const { success: isViewRecord } = AppBskyEmbedRecord.validateViewRecord(
|
||||||
|
post.embed.record.record
|
||||||
|
);
|
||||||
|
if (isViewRecord) {
|
||||||
|
// @ts-expect-error For some reason the original post value is typed as {}
|
||||||
|
return `${post.record.text}\n\nQuoting @${post.embed.record.record.author.handle}\n➥ ${post.embed.record.record.value.text}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return post.record.text;
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
}
|
||||||
51
src/lib/parseEmbedImage.ts
Normal file
51
src/lib/parseEmbedImage.ts
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
import {
|
||||||
|
AppBskyEmbedImages,
|
||||||
|
AppBskyEmbedRecord,
|
||||||
|
AppBskyEmbedRecordWithMedia,
|
||||||
|
AppBskyFeedDefs,
|
||||||
|
} from "@atproto/api";
|
||||||
|
|
||||||
|
export function parseEmbedImage(post: AppBskyFeedDefs.PostView) {
|
||||||
|
if (AppBskyEmbedRecord.isView(post.embed)) {
|
||||||
|
const { success: isView } = AppBskyEmbedRecord.validateView(post.embed);
|
||||||
|
if (isView && AppBskyEmbedRecord.isViewRecord(post.embed.record)) {
|
||||||
|
const { success: isViewRecord } = AppBskyEmbedRecord.validateViewRecord(
|
||||||
|
post.embed.record
|
||||||
|
);
|
||||||
|
if (
|
||||||
|
isViewRecord &&
|
||||||
|
post.embed.record.embeds &&
|
||||||
|
AppBskyEmbedImages.isView(post.embed.record.embeds[0])
|
||||||
|
) {
|
||||||
|
const { success: isImageView } = AppBskyEmbedImages.validateView(
|
||||||
|
post.embed.record.embeds[0]
|
||||||
|
);
|
||||||
|
if (isImageView) {
|
||||||
|
return post.embed.record.embeds[0].images[0].fullsize;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (AppBskyEmbedRecordWithMedia.isView(post.embed)) {
|
||||||
|
const { success: isView } = AppBskyEmbedRecordWithMedia.validateView(
|
||||||
|
post.embed
|
||||||
|
);
|
||||||
|
if (isView && AppBskyEmbedImages.isView(post.embed.media)) {
|
||||||
|
const { success: isImageView } = AppBskyEmbedImages.validateView(
|
||||||
|
post.embed.media
|
||||||
|
);
|
||||||
|
if (isImageView) {
|
||||||
|
return post.embed.media.images[0].fullsize;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (AppBskyEmbedImages.isView(post.embed)) {
|
||||||
|
const { success: isImageView } = AppBskyEmbedImages.validateView(
|
||||||
|
post.embed
|
||||||
|
);
|
||||||
|
if (isImageView) {
|
||||||
|
return post.embed.images[0].fullsize;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return post.author.avatar ?? "";
|
||||||
|
}
|
||||||
35
src/routes/getOEmbed.ts
Normal file
35
src/routes/getOEmbed.ts
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import { Handler } from "hono";
|
||||||
|
|
||||||
|
export enum OEmbedTypes {
|
||||||
|
Post = 1,
|
||||||
|
Profile,
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getOEmbed: Handler<Env, "/oembed"> = async (c) => {
|
||||||
|
const type = +(c.req.query("type") ?? 0);
|
||||||
|
const avatar = c.req.query("avatar");
|
||||||
|
|
||||||
|
const defaults = {
|
||||||
|
provider_name: "FixBluesky",
|
||||||
|
provider_url: "https://bsyy.app/",
|
||||||
|
thumbnail_url: avatar,
|
||||||
|
thumbnail_width: 1000,
|
||||||
|
thumbnail_height: 1000,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (type === OEmbedTypes.Post) {
|
||||||
|
const { replies, reposts, likes } = c.req.query();
|
||||||
|
return c.json({
|
||||||
|
author_name: `🗨️ ${replies} ♻️ ${reposts} 💙 ${likes}`,
|
||||||
|
...defaults,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (type === OEmbedTypes.Profile) {
|
||||||
|
const { follows, posts } = c.req.query();
|
||||||
|
return c.json({
|
||||||
|
author_name: `👤 ${follows} followers\n🗨️ ${posts} skeets`,
|
||||||
|
...defaults,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return c.json(defaults, 400);
|
||||||
|
};
|
||||||
26
src/routes/getPost.tsx
Normal file
26
src/routes/getPost.tsx
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import { Handler } from "hono";
|
||||||
|
import { HTTPException } from "hono/http-exception";
|
||||||
|
import { fetchPost } from "../lib/fetchPostData";
|
||||||
|
import { Post } from "../components/Post";
|
||||||
|
|
||||||
|
export const getPost: Handler<
|
||||||
|
Env,
|
||||||
|
"/profile/:user/post/:post" | "/https://bsky.app/profile/:user/post/:post"
|
||||||
|
> = async (c) => {
|
||||||
|
const { user, post } = c.req.param();
|
||||||
|
const agent = c.get("Agent");
|
||||||
|
const { data, success } = await fetchPost(agent, { user, post });
|
||||||
|
if (!success) {
|
||||||
|
throw new HTTPException(500, {
|
||||||
|
message: "Failed to fetch the post!",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// return c.html(genHTML(data.posts[0], c.req.path));
|
||||||
|
return c.html(
|
||||||
|
<Post
|
||||||
|
post={data.posts[0]}
|
||||||
|
url={c.req.path}
|
||||||
|
appDomain={c.env.FIXBLUESKY_APP_DOMAIN}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,4 +1,6 @@
|
|||||||
import { Handler } from "hono";
|
import { Handler } from "hono";
|
||||||
|
import { HTTPException } from "hono/http-exception";
|
||||||
|
import { fetchPost } from "../lib/fetchPostData";
|
||||||
|
|
||||||
export const getPostData: Handler<
|
export const getPostData: Handler<
|
||||||
Env,
|
Env,
|
||||||
@@ -7,11 +9,11 @@ export const getPostData: Handler<
|
|||||||
> = async (c) => {
|
> = async (c) => {
|
||||||
const { user, post } = c.req.param();
|
const { user, post } = c.req.param();
|
||||||
const agent = c.get("Agent");
|
const agent = c.get("Agent");
|
||||||
const { data: userData } = await agent.getProfile({
|
const { data, success } = await fetchPost(agent, { user, post });
|
||||||
actor: user,
|
if (!success) {
|
||||||
});
|
throw new HTTPException(500, {
|
||||||
const { data } = await agent.getPosts({
|
message: "Failed to fetch the post!",
|
||||||
uris: [`at://${userData.did}/app.bsky.feed.post/${post}`],
|
});
|
||||||
});
|
}
|
||||||
return c.json(data);
|
return c.json(data);
|
||||||
};
|
};
|
||||||
25
src/routes/getProfile.tsx
Normal file
25
src/routes/getProfile.tsx
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import { Handler } from "hono";
|
||||||
|
import { HTTPException } from "hono/http-exception";
|
||||||
|
import { fetchProfile } from "../lib/fetchProfile";
|
||||||
|
import { Profile } from "../components/Profile";
|
||||||
|
|
||||||
|
export const getProfile: Handler<
|
||||||
|
Env,
|
||||||
|
"/profile/:user" | "/https://bsky.app/profile/:user"
|
||||||
|
> = async (c) => {
|
||||||
|
const { user } = c.req.param();
|
||||||
|
const agent = c.get("Agent");
|
||||||
|
const { data, success } = await fetchProfile(agent, { user });
|
||||||
|
if (!success) {
|
||||||
|
throw new HTTPException(500, {
|
||||||
|
message: "Failed to fetch the profile!",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return c.html(
|
||||||
|
<Profile
|
||||||
|
profile={data}
|
||||||
|
url={c.req.url}
|
||||||
|
appDomain={c.env.FIXBLUESKY_APP_DOMAIN}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
20
src/routes/getProfileData.ts
Normal file
20
src/routes/getProfileData.ts
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
/** @jsx jsx */
|
||||||
|
import { jsx } from "hono/jsx";
|
||||||
|
import { Handler } from "hono";
|
||||||
|
import { HTTPException } from "hono/http-exception";
|
||||||
|
import { fetchProfile } from "../lib/fetchProfile";
|
||||||
|
|
||||||
|
export const getProfileData: Handler<
|
||||||
|
Env,
|
||||||
|
"/profile/:user/json" | "/https://bsky.app/profile/:user/json"
|
||||||
|
> = async (c) => {
|
||||||
|
const { user } = c.req.param();
|
||||||
|
const agent = c.get("Agent");
|
||||||
|
const { data, success } = await fetchProfile(agent, { user });
|
||||||
|
if (!success) {
|
||||||
|
throw new HTTPException(500, {
|
||||||
|
message: "Failed to fetch the profile!",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return c.json(data);
|
||||||
|
};
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
import {
|
|
||||||
AppBskyFeedDefs,
|
|
||||||
AppBskyEmbedImages,
|
|
||||||
AppBskyFeedPost,
|
|
||||||
} from "@atproto/api";
|
|
||||||
import { html } from "hono/html";
|
|
||||||
|
|
||||||
export function genHTML(post: AppBskyFeedDefs.PostView, url: string) {
|
|
||||||
const removeLeadingSlash = url.substring(1);
|
|
||||||
const redirectUrl = removeLeadingSlash.startsWith("https://")
|
|
||||||
? removeLeadingSlash
|
|
||||||
: `https://bsky.app/${removeLeadingSlash}`;
|
|
||||||
return html`
|
|
||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<link rel="canonical" href="${url.substring(1)}" />
|
|
||||||
<meta content="text/html; charset=UTF-8" http-equiv="Content-Type" />
|
|
||||||
<meta content="#0085ff" name="theme-color" />
|
|
||||||
<meta property="og:site_name" content="FixBluesky" />
|
|
||||||
<meta name="twitter:creator" content="@${post.author.handle}" />
|
|
||||||
<meta
|
|
||||||
property="og:description"
|
|
||||||
content="${AppBskyFeedPost.isRecord(post.record) && post.record.text}"
|
|
||||||
/>
|
|
||||||
|
|
||||||
${AppBskyEmbedImages.isView(post.embed) &&
|
|
||||||
html`<meta name="twitter:card" content="summary_large_image" />`}
|
|
||||||
<meta
|
|
||||||
property="og:image"
|
|
||||||
content="${AppBskyEmbedImages.isView(post.embed)
|
|
||||||
? post.embed.images[0].fullsize
|
|
||||||
: post.author.avatar}"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<meta http-equiv="refresh" content="0;url=${redirectUrl}" />
|
|
||||||
|
|
||||||
<link
|
|
||||||
type="application/json+oembed"
|
|
||||||
href="https://bsyy.app/oembed?display_name=${encodeURIComponent(
|
|
||||||
post.author.displayName ?? ""
|
|
||||||
)}&handle=${encodeURIComponent(
|
|
||||||
post.author.handle
|
|
||||||
)}&avatar=${encodeURIComponent(post.author.avatar ?? "")}"
|
|
||||||
/>
|
|
||||||
</head>
|
|
||||||
</html>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
@@ -5,13 +5,9 @@
|
|||||||
"moduleResolution": "node",
|
"moduleResolution": "node",
|
||||||
"esModuleInterop": true,
|
"esModuleInterop": true,
|
||||||
"strict": true,
|
"strict": true,
|
||||||
"lib": [
|
"lib": ["esnext"],
|
||||||
"esnext"
|
"types": ["@cloudflare/workers-types"],
|
||||||
],
|
|
||||||
"types": [
|
|
||||||
"@cloudflare/workers-types"
|
|
||||||
],
|
|
||||||
"jsx": "react-jsx",
|
"jsx": "react-jsx",
|
||||||
"jsxImportSource": "hono/jsx"
|
"jsxImportSource": "hono/jsx"
|
||||||
},
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user