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 (
<div>
<NextSeo
title={post.title}
description={post.excerpt}
openGraph={{
title: post.title,
description: post.excerpt,
images: [
{
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 { 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>
);
};

View file

@ -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>
);
};

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 { 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) => {

View file

@ -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;