Open graph image for polls (#818)

This commit is contained in:
Luke Vella 2023-08-18 11:55:02 +01:00 committed by GitHub
parent 9168e208c6
commit e7a69ffe1d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 265 additions and 96 deletions

View file

@ -31,7 +31,11 @@ const Page: NextPageWithLayout<Props> = ({ post }) => {
return ( return (
<div> <div>
<NextSeo <NextSeo
title={post.title}
description={post.excerpt}
openGraph={{ openGraph={{
title: post.title,
description: post.excerpt,
images: [ images: [
{ {
url: url:

Binary file not shown.

Binary file not shown.

View file

@ -17,10 +17,11 @@ import { Spinner } from "@/components/spinner";
import { Trans } from "@/components/trans"; import { Trans } from "@/components/trans";
import { UserDropdown } from "@/components/user-dropdown"; import { UserDropdown } from "@/components/user-dropdown";
import { isFeedbackEnabled } from "@/utils/constants"; import { isFeedbackEnabled } from "@/utils/constants";
import { DayjsProvider } from "@/utils/dayjs";
import { IconComponent, NextPageWithLayout } from "../../types"; import { IconComponent, NextPageWithLayout } from "../../types";
import ModalProvider from "../modal/modal-provider"; import ModalProvider from "../modal/modal-provider";
import { IfGuest } from "../user-provider"; import { IfGuest, UserProvider } from "../user-provider";
const NavMenuItem = ({ const NavMenuItem = ({
href, href,
@ -155,36 +156,38 @@ export const StandardLayout: React.FunctionComponent<{
const key = hideNav ? "no-nav" : "nav"; const key = hideNav ? "no-nav" : "nav";
return ( return (
<> <UserProvider>
<Toaster /> <DayjsProvider>
<ModalProvider> <Toaster />
<div className="flex min-h-screen flex-col" {...rest}> <ModalProvider>
<AnimatePresence initial={false}> <div className="flex min-h-screen flex-col" {...rest}>
{!hideNav ? <MainNav /> : null} <AnimatePresence initial={false}>
</AnimatePresence> {!hideNav ? <MainNav /> : null}
<AnimatePresence mode="wait" initial={false}> </AnimatePresence>
<m.div <AnimatePresence mode="wait" initial={false}>
key={key} <m.div
variants={{ key={key}
hidden: { opacity: 0, y: -56 }, variants={{
visible: { opacity: 1, y: 0 }, hidden: { opacity: 0, y: -56 },
}} visible: { opacity: 1, y: 0 },
initial="hidden" }}
animate="visible" initial="hidden"
exit={{ opacity: 0, y: 56 }} animate="visible"
> exit={{ opacity: 0, y: 56 }}
{children} >
</m.div> {children}
</AnimatePresence> </m.div>
</div> </AnimatePresence>
{isFeedbackEnabled ? ( </div>
<> {isFeedbackEnabled ? (
<FeaturebaseIdentify /> <>
<FeedbackButton /> <FeaturebaseIdentify />
</> <FeedbackButton />
) : null} </>
</ModalProvider> ) : null}
</> </ModalProvider>
</DayjsProvider>
</UserProvider>
); );
}; };

View file

@ -16,8 +16,6 @@ import { DefaultSeo } from "next-seo";
import React from "react"; import React from "react";
import Maintenance from "@/components/maintenance"; 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 * as nextI18nNextConfig from "../../next-i18next.config.js";
import { NextPageWithLayout } from "../types"; import { NextPageWithLayout } from "../types";
@ -95,13 +93,9 @@ const MyApp: NextPage<AppPropsWithLayout> = ({ Component, pageProps }) => {
--font-inter: ${inter.style.fontFamily}; --font-inter: ${inter.style.fontFamily};
} }
`}</style> `}</style>
<UserProvider> <TooltipProvider delayDuration={200}>
<DayjsProvider> {getLayout(<Component {...pageProps} />)}
<TooltipProvider delayDuration={200}> </TooltipProvider>
{getLayout(<Component {...pageProps} />)}
</TooltipProvider>
</DayjsProvider>
</UserProvider>
</LazyMotion> </LazyMotion>
); );
}; };

View 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,
},
],
},
);
}

View file

@ -14,7 +14,9 @@ import React from "react";
import { useMount } from "react-use"; import { useMount } from "react-use";
import { AuthLayout } from "@/components/layouts/auth-layout"; import { AuthLayout } from "@/components/layouts/auth-layout";
import { StandardLayout } from "@/components/layouts/standard-layout";
import { Spinner } from "@/components/spinner"; import { Spinner } from "@/components/spinner";
import { NextPageWithLayout } from "@/types";
import { usePostHog } from "@/utils/posthog"; import { usePostHog } from "@/utils/posthog";
import { withPageTranslations } from "@/utils/with-page-translations"; import { withPageTranslations } from "@/utils/with-page-translations";
@ -69,7 +71,7 @@ type PageProps =
} }
| { error: undefined; data: Data }; | { error: undefined; data: Data };
const Page = (props: PageProps) => { const Page: NextPageWithLayout<PageProps> = (props) => {
const { t } = useTranslation(); const { t } = useTranslation();
const posthog = usePostHog(); const posthog = usePostHog();
@ -101,6 +103,10 @@ const Page = (props: PageProps) => {
); );
}; };
Page.getLayout = (page) => {
return <StandardLayout hideNav={true}>{page}</StandardLayout>;
};
export const getServerSideProps = composeGetServerSideProps( export const getServerSideProps = composeGetServerSideProps(
withPageTranslations(), withPageTranslations(),
withSessionSsr(async (ctx) => { withSessionSsr(async (ctx) => {

View file

@ -1,18 +1,24 @@
import { trpc } from "@rallly/backend"; import { trpc } from "@rallly/backend";
import { prisma } from "@rallly/database";
import { ArrowUpLeftIcon } from "@rallly/icons"; import { ArrowUpLeftIcon } from "@rallly/icons";
import { Button } from "@rallly/ui/button"; import { Button } from "@rallly/ui/button";
import { absoluteUrl } from "@rallly/utils";
import { GetStaticProps } from "next";
import Head from "next/head"; import Head from "next/head";
import Link from "next/link"; import Link from "next/link";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { useTranslation } from "next-i18next";
import { NextSeo } from "next-seo";
import React from "react"; import React from "react";
import { Poll } from "@/components/poll"; import { Poll } from "@/components/poll";
import { LegacyPollContextProvider } from "@/components/poll/poll-context-provider"; import { LegacyPollContextProvider } from "@/components/poll/poll-context-provider";
import { Trans } from "@/components/trans"; import { Trans } from "@/components/trans";
import { useUser } from "@/components/user-provider"; import { UserProvider, useUser } from "@/components/user-provider";
import { VisibilityProvider } from "@/components/visibility"; import { VisibilityProvider } from "@/components/visibility";
import { PermissionsContext } from "@/contexts/permissions"; import { PermissionsContext } from "@/contexts/permissions";
import { usePoll } from "@/contexts/poll"; import { usePoll } from "@/contexts/poll";
import { DayjsProvider } from "@/utils/dayjs";
import { getStaticTranslations } from "@/utils/with-page-translations"; import { getStaticTranslations } from "@/utils/with-page-translations";
import Error404 from "../404"; 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 ( return (
<Prefetch> <>
<LegacyPollContextProvider> <NextSeo
<VisibilityProvider> openGraph={{
<div className=""> title,
<svg description: `By ${name}`,
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" images: [
aria-hidden="true" {
> url:
<defs> `${absoluteUrl()}/_next/image?w=1200&q=100&url=${encodeURIComponent(
<pattern `/api/og-image-poll`,
id="1f932ae7-37de-4c0a-a8b0-a6e3b4d44b84" )}` +
width={240} encodeURIComponent(
height={240} `?title=${encodeURIComponent(
x="50%" title,
y={-1} )}&author=${encodeURIComponent(name)}`,
patternUnits="userSpaceOnUse" ),
> width: 1200,
<path d="M.5 240V.5H240" fill="none" /> height: 630,
</pattern> alt: title,
</defs> type: "image/png",
<rect },
width="100%" ],
height="100%" }}
strokeWidth={0} />
fill="url(#1f932ae7-37de-4c0a-a8b0-a6e3b4d44b84)" <UserProvider>
/> <DayjsProvider>
</svg> <Prefetch>
<div className="mx-auto max-w-4xl space-y-4 p-3 sm:py-8"> <LegacyPollContextProvider>
<GoToApp /> <VisibilityProvider>
<Poll /> <div className="">
<div className="mt-4 space-y-4 text-center text-gray-500"> <svg
<div className="py-8"> 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"
<Trans aria-hidden="true"
defaults="Powered by <a>{name}</a>" >
i18nKey="poweredByRallly" <defs>
values={{ name: "rallly.co" }} <pattern
components={{ id="1f932ae7-37de-4c0a-a8b0-a6e3b4d44b84"
a: ( width={240}
<Link height={240}
className="hover:text-primary-600 rounded-none border-b border-b-gray-500 font-semibold" x="50%"
href="https://rallly.co" 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> </VisibilityProvider>
</div> </LegacyPollContextProvider>
</div> </Prefetch>
</VisibilityProvider> </DayjsProvider>
</LegacyPollContextProvider> </UserProvider>
</Prefetch> </>
); );
}; };
export const getStaticPaths = async () => { export const getStaticPaths = async () => {
return { return {
paths: [], //indicates that no page needs be created at build time paths: [], // indicates that no page needs be created at build time
fallback: "blocking", //indicates the type of fallback 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; export default Page;