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,7 +156,8 @@ export const StandardLayout: React.FunctionComponent<{
const key = hideNav ? "no-nav" : "nav"; const key = hideNav ? "no-nav" : "nav";
return ( return (
<> <UserProvider>
<DayjsProvider>
<Toaster /> <Toaster />
<ModalProvider> <ModalProvider>
<div className="flex min-h-screen flex-col" {...rest}> <div className="flex min-h-screen flex-col" {...rest}>
@ -184,7 +186,8 @@ export const StandardLayout: React.FunctionComponent<{
</> </>
) : null} ) : null}
</ModalProvider> </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>
<DayjsProvider>
<TooltipProvider delayDuration={200}> <TooltipProvider delayDuration={200}>
{getLayout(<Component {...pageProps} />)} {getLayout(<Component {...pageProps} />)}
</TooltipProvider> </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,8 +85,41 @@ 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 (
<>
<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> <Prefetch>
<LegacyPollContextProvider> <LegacyPollContextProvider>
<VisibilityProvider> <VisibilityProvider>
@ -133,16 +172,49 @@ const Page = () => {
</VisibilityProvider> </VisibilityProvider>
</LegacyPollContextProvider> </LegacyPollContextProvider>
</Prefetch> </Prefetch>
</DayjsProvider>
</UserProvider>
</>
); );
}; };
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;