mirror of
https://github.com/lukevella/rallly.git
synced 2025-06-02 10:41:54 +02:00
✨ Open graph image for polls (#818)
This commit is contained in:
parent
9168e208c6
commit
e7a69ffe1d
8 changed files with 265 additions and 96 deletions
|
@ -31,7 +31,11 @@ const Page: NextPageWithLayout<Props> = ({ post }) => {
|
|||
return (
|
||||
<div>
|
||||
<NextSeo
|
||||
title={post.title}
|
||||
description={post.excerpt}
|
||||
openGraph={{
|
||||
title: post.title,
|
||||
description: post.excerpt,
|
||||
images: [
|
||||
{
|
||||
url:
|
||||
|
|
BIN
apps/web/public/static/fonts/inter-bold.ttf
Normal file
BIN
apps/web/public/static/fonts/inter-bold.ttf
Normal file
Binary file not shown.
BIN
apps/web/public/static/fonts/inter-regular.ttf
Normal file
BIN
apps/web/public/static/fonts/inter-regular.ttf
Normal file
Binary file not shown.
|
@ -17,10 +17,11 @@ import { Spinner } from "@/components/spinner";
|
|||
import { Trans } from "@/components/trans";
|
||||
import { UserDropdown } from "@/components/user-dropdown";
|
||||
import { isFeedbackEnabled } from "@/utils/constants";
|
||||
import { DayjsProvider } from "@/utils/dayjs";
|
||||
|
||||
import { IconComponent, NextPageWithLayout } from "../../types";
|
||||
import ModalProvider from "../modal/modal-provider";
|
||||
import { IfGuest } from "../user-provider";
|
||||
import { IfGuest, UserProvider } from "../user-provider";
|
||||
|
||||
const NavMenuItem = ({
|
||||
href,
|
||||
|
@ -155,36 +156,38 @@ export const StandardLayout: React.FunctionComponent<{
|
|||
const key = hideNav ? "no-nav" : "nav";
|
||||
|
||||
return (
|
||||
<>
|
||||
<Toaster />
|
||||
<ModalProvider>
|
||||
<div className="flex min-h-screen flex-col" {...rest}>
|
||||
<AnimatePresence initial={false}>
|
||||
{!hideNav ? <MainNav /> : null}
|
||||
</AnimatePresence>
|
||||
<AnimatePresence mode="wait" initial={false}>
|
||||
<m.div
|
||||
key={key}
|
||||
variants={{
|
||||
hidden: { opacity: 0, y: -56 },
|
||||
visible: { opacity: 1, y: 0 },
|
||||
}}
|
||||
initial="hidden"
|
||||
animate="visible"
|
||||
exit={{ opacity: 0, y: 56 }}
|
||||
>
|
||||
{children}
|
||||
</m.div>
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
{isFeedbackEnabled ? (
|
||||
<>
|
||||
<FeaturebaseIdentify />
|
||||
<FeedbackButton />
|
||||
</>
|
||||
) : null}
|
||||
</ModalProvider>
|
||||
</>
|
||||
<UserProvider>
|
||||
<DayjsProvider>
|
||||
<Toaster />
|
||||
<ModalProvider>
|
||||
<div className="flex min-h-screen flex-col" {...rest}>
|
||||
<AnimatePresence initial={false}>
|
||||
{!hideNav ? <MainNav /> : null}
|
||||
</AnimatePresence>
|
||||
<AnimatePresence mode="wait" initial={false}>
|
||||
<m.div
|
||||
key={key}
|
||||
variants={{
|
||||
hidden: { opacity: 0, y: -56 },
|
||||
visible: { opacity: 1, y: 0 },
|
||||
}}
|
||||
initial="hidden"
|
||||
animate="visible"
|
||||
exit={{ opacity: 0, y: 56 }}
|
||||
>
|
||||
{children}
|
||||
</m.div>
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
{isFeedbackEnabled ? (
|
||||
<>
|
||||
<FeaturebaseIdentify />
|
||||
<FeedbackButton />
|
||||
</>
|
||||
) : null}
|
||||
</ModalProvider>
|
||||
</DayjsProvider>
|
||||
</UserProvider>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -16,8 +16,6 @@ import { DefaultSeo } from "next-seo";
|
|||
import React from "react";
|
||||
|
||||
import Maintenance from "@/components/maintenance";
|
||||
import { UserProvider } from "@/components/user-provider";
|
||||
import { DayjsProvider } from "@/utils/dayjs";
|
||||
|
||||
import * as nextI18nNextConfig from "../../next-i18next.config.js";
|
||||
import { NextPageWithLayout } from "../types";
|
||||
|
@ -95,13 +93,9 @@ const MyApp: NextPage<AppPropsWithLayout> = ({ Component, pageProps }) => {
|
|||
--font-inter: ${inter.style.fontFamily};
|
||||
}
|
||||
`}</style>
|
||||
<UserProvider>
|
||||
<DayjsProvider>
|
||||
<TooltipProvider delayDuration={200}>
|
||||
{getLayout(<Component {...pageProps} />)}
|
||||
</TooltipProvider>
|
||||
</DayjsProvider>
|
||||
</UserProvider>
|
||||
<TooltipProvider delayDuration={200}>
|
||||
{getLayout(<Component {...pageProps} />)}
|
||||
</TooltipProvider>
|
||||
</LazyMotion>
|
||||
);
|
||||
};
|
||||
|
|
90
apps/web/src/pages/api/og-image-poll.tsx
Normal file
90
apps/web/src/pages/api/og-image-poll.tsx
Normal file
|
@ -0,0 +1,90 @@
|
|||
/* eslint-disable @next/next/no-img-element */
|
||||
import { ImageResponse } from "@vercel/og";
|
||||
import { NextRequest } from "next/server";
|
||||
import { z } from "zod";
|
||||
|
||||
const schema = z.object({
|
||||
title: z.string().min(1),
|
||||
author: z.string().min(1),
|
||||
});
|
||||
|
||||
export const config = {
|
||||
runtime: "edge",
|
||||
};
|
||||
|
||||
const regularFont = fetch(
|
||||
new URL("/public/static/fonts/inter-regular.ttf", import.meta.url),
|
||||
).then((res) => res.arrayBuffer());
|
||||
|
||||
const boldFont = fetch(
|
||||
new URL("/public/static/fonts/inter-bold.ttf", import.meta.url),
|
||||
).then((res) => res.arrayBuffer());
|
||||
|
||||
export default async function handler(req: NextRequest) {
|
||||
const [regularFontData, boldFontData] = await Promise.all([
|
||||
regularFont,
|
||||
boldFont,
|
||||
]);
|
||||
const { searchParams } = req.nextUrl;
|
||||
|
||||
const { title, author } = schema.parse({
|
||||
title: searchParams.get("title"),
|
||||
author: searchParams.get("author"),
|
||||
});
|
||||
|
||||
return new ImageResponse(
|
||||
(
|
||||
<div tw="flex relative flex-col bg-gray-100 w-full h-full px-[80px] py-[70px] items-start justify-center">
|
||||
<div tw="h-full flex flex-col w-full justify-start">
|
||||
<div tw="flex justify-between items-center w-full">
|
||||
<img
|
||||
alt="Rallly"
|
||||
src="https://rallly.co/logo-color.svg"
|
||||
height={64}
|
||||
/>
|
||||
<div tw="flex text-gray-800 text-3xl tracking-tight font-bold">
|
||||
<span tw="bg-gray-200 px-6 py-3 rounded-full">Invite</span>
|
||||
</div>
|
||||
</div>
|
||||
<div tw="relative flex w-full flex-col mt-auto">
|
||||
<div
|
||||
tw="flex text-gray-500 text-[48px] w-[1040px] overflow-hidden"
|
||||
style={{
|
||||
width: 1000,
|
||||
whiteSpace: "nowrap",
|
||||
textOverflow: "ellipsis",
|
||||
}}
|
||||
>
|
||||
By {author}
|
||||
</div>
|
||||
<div
|
||||
tw="flex mt-3 text-[64px] font-bold w-[1040px] overflow-hidden"
|
||||
style={{
|
||||
whiteSpace: "nowrap",
|
||||
textOverflow: "ellipsis",
|
||||
}}
|
||||
>
|
||||
{title}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
{
|
||||
width: 1200,
|
||||
height: 630,
|
||||
fonts: [
|
||||
{
|
||||
name: "Inter",
|
||||
data: regularFontData,
|
||||
weight: 400,
|
||||
},
|
||||
{
|
||||
name: "Inter",
|
||||
data: boldFontData,
|
||||
weight: 700,
|
||||
},
|
||||
],
|
||||
},
|
||||
);
|
||||
}
|
|
@ -14,7 +14,9 @@ import React from "react";
|
|||
import { useMount } from "react-use";
|
||||
|
||||
import { AuthLayout } from "@/components/layouts/auth-layout";
|
||||
import { StandardLayout } from "@/components/layouts/standard-layout";
|
||||
import { Spinner } from "@/components/spinner";
|
||||
import { NextPageWithLayout } from "@/types";
|
||||
import { usePostHog } from "@/utils/posthog";
|
||||
import { withPageTranslations } from "@/utils/with-page-translations";
|
||||
|
||||
|
@ -69,7 +71,7 @@ type PageProps =
|
|||
}
|
||||
| { error: undefined; data: Data };
|
||||
|
||||
const Page = (props: PageProps) => {
|
||||
const Page: NextPageWithLayout<PageProps> = (props) => {
|
||||
const { t } = useTranslation();
|
||||
const posthog = usePostHog();
|
||||
|
||||
|
@ -101,6 +103,10 @@ const Page = (props: PageProps) => {
|
|||
);
|
||||
};
|
||||
|
||||
Page.getLayout = (page) => {
|
||||
return <StandardLayout hideNav={true}>{page}</StandardLayout>;
|
||||
};
|
||||
|
||||
export const getServerSideProps = composeGetServerSideProps(
|
||||
withPageTranslations(),
|
||||
withSessionSsr(async (ctx) => {
|
||||
|
|
|
@ -1,18 +1,24 @@
|
|||
import { trpc } from "@rallly/backend";
|
||||
import { prisma } from "@rallly/database";
|
||||
import { ArrowUpLeftIcon } from "@rallly/icons";
|
||||
import { Button } from "@rallly/ui/button";
|
||||
import { absoluteUrl } from "@rallly/utils";
|
||||
import { GetStaticProps } from "next";
|
||||
import Head from "next/head";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
import { useTranslation } from "next-i18next";
|
||||
import { NextSeo } from "next-seo";
|
||||
import React from "react";
|
||||
|
||||
import { Poll } from "@/components/poll";
|
||||
import { LegacyPollContextProvider } from "@/components/poll/poll-context-provider";
|
||||
import { Trans } from "@/components/trans";
|
||||
import { useUser } from "@/components/user-provider";
|
||||
import { UserProvider, useUser } from "@/components/user-provider";
|
||||
import { VisibilityProvider } from "@/components/visibility";
|
||||
import { PermissionsContext } from "@/contexts/permissions";
|
||||
import { usePoll } from "@/contexts/poll";
|
||||
import { DayjsProvider } from "@/utils/dayjs";
|
||||
import { getStaticTranslations } from "@/utils/with-page-translations";
|
||||
|
||||
import Error404 from "../404";
|
||||
|
@ -79,70 +85,136 @@ const GoToApp = () => {
|
|||
);
|
||||
};
|
||||
|
||||
const Page = () => {
|
||||
type PageProps = {
|
||||
title: string;
|
||||
user: string | null;
|
||||
};
|
||||
|
||||
const Page = ({ title, user }: PageProps) => {
|
||||
const { t } = useTranslation();
|
||||
const name = user ?? t("guest");
|
||||
return (
|
||||
<Prefetch>
|
||||
<LegacyPollContextProvider>
|
||||
<VisibilityProvider>
|
||||
<div className="">
|
||||
<svg
|
||||
className="absolute inset-x-0 top-0 -z-10 hidden h-[64rem] w-full stroke-gray-300/75 [mask-image:radial-gradient(800px_800px_at_center,white,transparent)] sm:block"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<defs>
|
||||
<pattern
|
||||
id="1f932ae7-37de-4c0a-a8b0-a6e3b4d44b84"
|
||||
width={240}
|
||||
height={240}
|
||||
x="50%"
|
||||
y={-1}
|
||||
patternUnits="userSpaceOnUse"
|
||||
>
|
||||
<path d="M.5 240V.5H240" fill="none" />
|
||||
</pattern>
|
||||
</defs>
|
||||
<rect
|
||||
width="100%"
|
||||
height="100%"
|
||||
strokeWidth={0}
|
||||
fill="url(#1f932ae7-37de-4c0a-a8b0-a6e3b4d44b84)"
|
||||
/>
|
||||
</svg>
|
||||
<div className="mx-auto max-w-4xl space-y-4 p-3 sm:py-8">
|
||||
<GoToApp />
|
||||
<Poll />
|
||||
<div className="mt-4 space-y-4 text-center text-gray-500">
|
||||
<div className="py-8">
|
||||
<Trans
|
||||
defaults="Powered by <a>{name}</a>"
|
||||
i18nKey="poweredByRallly"
|
||||
values={{ name: "rallly.co" }}
|
||||
components={{
|
||||
a: (
|
||||
<Link
|
||||
className="hover:text-primary-600 rounded-none border-b border-b-gray-500 font-semibold"
|
||||
href="https://rallly.co"
|
||||
<>
|
||||
<NextSeo
|
||||
openGraph={{
|
||||
title,
|
||||
description: `By ${name}`,
|
||||
images: [
|
||||
{
|
||||
url:
|
||||
`${absoluteUrl()}/_next/image?w=1200&q=100&url=${encodeURIComponent(
|
||||
`/api/og-image-poll`,
|
||||
)}` +
|
||||
encodeURIComponent(
|
||||
`?title=${encodeURIComponent(
|
||||
title,
|
||||
)}&author=${encodeURIComponent(name)}`,
|
||||
),
|
||||
width: 1200,
|
||||
height: 630,
|
||||
alt: title,
|
||||
type: "image/png",
|
||||
},
|
||||
],
|
||||
}}
|
||||
/>
|
||||
<UserProvider>
|
||||
<DayjsProvider>
|
||||
<Prefetch>
|
||||
<LegacyPollContextProvider>
|
||||
<VisibilityProvider>
|
||||
<div className="">
|
||||
<svg
|
||||
className="absolute inset-x-0 top-0 -z-10 hidden h-[64rem] w-full stroke-gray-300/75 [mask-image:radial-gradient(800px_800px_at_center,white,transparent)] sm:block"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<defs>
|
||||
<pattern
|
||||
id="1f932ae7-37de-4c0a-a8b0-a6e3b4d44b84"
|
||||
width={240}
|
||||
height={240}
|
||||
x="50%"
|
||||
y={-1}
|
||||
patternUnits="userSpaceOnUse"
|
||||
>
|
||||
<path d="M.5 240V.5H240" fill="none" />
|
||||
</pattern>
|
||||
</defs>
|
||||
<rect
|
||||
width="100%"
|
||||
height="100%"
|
||||
strokeWidth={0}
|
||||
fill="url(#1f932ae7-37de-4c0a-a8b0-a6e3b4d44b84)"
|
||||
/>
|
||||
</svg>
|
||||
<div className="mx-auto max-w-4xl space-y-4 p-3 sm:py-8">
|
||||
<GoToApp />
|
||||
<Poll />
|
||||
<div className="mt-4 space-y-4 text-center text-gray-500">
|
||||
<div className="py-8">
|
||||
<Trans
|
||||
defaults="Powered by <a>{name}</a>"
|
||||
i18nKey="poweredByRallly"
|
||||
values={{ name: "rallly.co" }}
|
||||
components={{
|
||||
a: (
|
||||
<Link
|
||||
className="hover:text-primary-600 rounded-none border-b border-b-gray-500 font-semibold"
|
||||
href="https://rallly.co"
|
||||
/>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</VisibilityProvider>
|
||||
</LegacyPollContextProvider>
|
||||
</Prefetch>
|
||||
</VisibilityProvider>
|
||||
</LegacyPollContextProvider>
|
||||
</Prefetch>
|
||||
</DayjsProvider>
|
||||
</UserProvider>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const getStaticPaths = async () => {
|
||||
return {
|
||||
paths: [], //indicates that no page needs be created at build time
|
||||
fallback: "blocking", //indicates the type of fallback
|
||||
paths: [], // indicates that no page needs be created at build time
|
||||
fallback: "blocking", // indicates the type of fallback
|
||||
};
|
||||
};
|
||||
|
||||
export const getStaticProps = getStaticTranslations;
|
||||
export const getStaticProps: GetStaticProps = async (ctx) => {
|
||||
// We get these props to be able to render the og:image
|
||||
const poll = await prisma.poll.findUniqueOrThrow({
|
||||
where: {
|
||||
id: ctx.params?.urlId as string,
|
||||
},
|
||||
select: {
|
||||
title: true,
|
||||
user: {
|
||||
select: {
|
||||
name: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const res = await getStaticTranslations(ctx);
|
||||
|
||||
if ("props" in res) {
|
||||
return {
|
||||
props: {
|
||||
...res.props,
|
||||
title: poll.title,
|
||||
user: poll.user?.name ?? null,
|
||||
},
|
||||
revalidate: 10,
|
||||
};
|
||||
}
|
||||
|
||||
return res;
|
||||
};
|
||||
|
||||
export default Page;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue