mirror of
https://github.com/lukevella/rallly.git
synced 2025-06-06 12:41:48 +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 (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<NextSeo
|
<NextSeo
|
||||||
|
title={post.title}
|
||||||
|
description={post.excerpt}
|
||||||
openGraph={{
|
openGraph={{
|
||||||
|
title: post.title,
|
||||||
|
description: post.excerpt,
|
||||||
images: [
|
images: [
|
||||||
{
|
{
|
||||||
url:
|
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 { 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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
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 { 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) => {
|
||||||
|
|
|
@ -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;
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue