Skip to content

Commit

Permalink
Add dynamic opengraph images (#143)
Browse files Browse the repository at this point in the history
* initial generating of images for open graph / x posts.

need ttf font for this generator not woof woof

* wip generated images

* gvoernance proposal image

* drop the the its cleaner

* generating

* moved validators component

* unused

* do we need this?

* rename font

* fetch names and logos for validators and delegates

* test open graph

* work on getting stuff to show on twitter

* cant use with use Client

* does this work?

* move components to new file so we can have server rendered head content

* move proposal components out of app folder

* refactor staking validator page so that the "use client" is not use top level

* dynamc og title for staking

* pages render on server

* uniform mondo logo width/size

show x names in the twitter metadata.

use short address for twitter titles

delegatees page metadata

show fallback image on more pages

* deadcode
add delegate reg image

* use large summary cards

---------

Co-authored-by: Leszek Stachowski <leszek.stachowski@clabs.co>
  • Loading branch information
aaronmgdr and shazarre authored Jan 23, 2025
1 parent 4fddad7 commit 74570e5
Show file tree
Hide file tree
Showing 30 changed files with 1,171 additions and 678 deletions.
1 change: 1 addition & 0 deletions next-env.d.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
/// <reference types="next/navigation-types/compat/navigation" />

// NOTE: This file should not be edited
// see https://nextjs.org/docs/app/building-your-application/configuring/typescript for more information.
Binary file added public/fonts/alpina-standard-regular.ttf
Binary file not shown.
3 changes: 3 additions & 0 deletions public/fonts/readme.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
the Alpina ttf here is for use in generating ImageResponse for opengraph images.

more alpina woff files are found in src/styles which are used in the app itself
9 changes: 9 additions & 0 deletions src/app/account/twitter-image.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { FallBack, OpenGraphImage } from 'src/app/twitter-image';

export { contentType, size } from 'src/app/twitter-image';
export const runtime = 'edge';

export const alt = 'Celo Mondo | Account';
export default function Image() {
return OpenGraphImage({ children: <FallBack /> });
}
9 changes: 9 additions & 0 deletions src/app/bridge/twitter-image.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { FallBack, OpenGraphImage } from 'src/app/twitter-image';

export { contentType, size } from 'src/app/twitter-image';
export const runtime = 'edge';

export const alt = 'Celo Mondo | Bridges';
export default function Image() {
return OpenGraphImage({ children: <FallBack /> });
}
158 changes: 26 additions & 132 deletions src/app/delegate/[address]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,137 +1,31 @@
'use client';

import { useMemo } from 'react';
import { FullWidthSpinner, SpinnerWithLabel } from 'src/components/animation/Spinner';
import { BackLink } from 'src/components/buttons/BackLink';
import { Section } from 'src/components/layout/Section';
import { SocialLogoLink } from 'src/components/logos/SocialLogo';
import { CollapsibleResponsiveMenu } from 'src/components/menus/CollapsibleResponsiveMenu';
import { ShortAddress } from 'src/components/text/ShortAddress';
import { SocialLinkType } from 'src/config/types';
import { DelegateButton } from 'src/features/delegation/components/DelegateButton';
import { DelegateeLogo } from 'src/features/delegation/components/DelegateeLogo';
import { DelegatorsTable } from 'src/features/delegation/components/DelegatorsTable';
import { useDelegateeHistory } from 'src/features/delegation/hooks/useDelegateeHistory';
import { useDelegatees } from 'src/features/delegation/hooks/useDelegatees';
import { Delegatee } from 'src/features/delegation/types';
import { ProposalCard } from 'src/features/governance/components/ProposalCard';
import { useGovernanceProposals } from 'src/features/governance/hooks/useGovernanceProposals';
import { VoteTypeToIcon } from 'src/features/governance/types';
import { getLargestVoteType } from 'src/features/governance/utils';
import { usePageInvariant } from 'src/utils/navigation';

// DO NOT USE "use client" here as it breaks metadata for openGraph
import DelegatePage from 'src/features/delegation/components/delegatePage';
import { getDelegateeMetadata } from 'src/features/delegation/delegateeMetadata';
import { getXName } from 'src/features/delegation/utils';
import { shortenAddress } from 'src/utils/addresses';
export const dynamicParams = true;

export default function Page({ params: { address } }: { params: { address: Address } }) {
const { addressToDelegatee } = useDelegatees();
const delegatee = addressToDelegatee?.[address];

usePageInvariant(!addressToDelegatee || delegatee, '/delegate', 'Delegate not found');

if (!addressToDelegatee || !delegatee) {
return <FullWidthSpinner>Loading delegate data</FullWidthSpinner>;
}

return (
<Section containerClassName="mt-4 lg:flex lg:gap-6 lg:items-start">
<DelegateeDescription delegatee={delegatee} />
<CollapsibleResponsiveMenu>
<DelegateeDetails delegatee={delegatee} />
</CollapsibleResponsiveMenu>
</Section>
);
}

function DelegateeDescription({ delegatee }: { delegatee: Delegatee }) {
const dateString = new Date(delegatee.date).toLocaleDateString();

return (
<div className="space-y-4">
<BackLink href="/delegate">Browse delegates</BackLink>
<div className="flex items-center gap-1">
<DelegateeLogo address={delegatee.address} size={90} />
<div className="ml-4 flex flex-col">
<h1 className="font-serif text-2xl md:text-3xl">{delegatee.name}</h1>
<div className="flex items-center space-x-2">
<ShortAddress
address={delegatee.address}
className="font-mono text-sm text-taupe-600"
/>
<span className="text-sm text-taupe-600"></span>
<span className="text-sm text-taupe-600">{`Since ${dateString}`}</span>
</div>
<div className="mt-1.5 flex items-center space-x-3">
{Object.entries(delegatee.links).map(([type, href], i) => (
<SocialLogoLink key={i} type={type as SocialLinkType} href={href} />
))}
{delegatee.interests.map((interest, i) => (
<span
key={i}
className="hidden rounded-full border border-taupe-300 px-2 text-sm sm:block"
>
{interest}
</span>
))}
</div>
</div>
</div>
<h2 className="font-serif text-xl">Introduction</h2>
<p style={{ maxWidth: 'min(96vw, 700px)' }} className="overflow-auto leading-relaxed">
{delegatee.description}
</p>
<GovernanceParticipation delegatee={delegatee} />
</div>
);
}

function GovernanceParticipation({ delegatee }: { delegatee: Delegatee }) {
const { proposalToVotes, isLoading: isLoadingHistory } = useDelegateeHistory(delegatee.address);
const { proposals, isLoading: isLoadingProposals } = useGovernanceProposals();

const isLoading = isLoadingHistory || isLoadingProposals;
const sortedIds = useMemo(
() =>
Object.keys(proposalToVotes || {})
.map((p) => parseInt(p))
.sort((a, b) => b - a),
[proposalToVotes],
);
const hasVotes = proposalToVotes && sortedIds.length > 0;

return (
<div className="flex flex-col space-y-2.5 divide-y border-taupe-300 py-1">
<h2 className="font-serif text-xl">Governance Participation</h2>
{isLoading ? (
<SpinnerWithLabel className="py-10">Loading governance history</SpinnerWithLabel>
) : proposals && hasVotes ? (
sortedIds.map((id, i) => {
const votes = proposalToVotes[id];
const proposal = proposals.find((p) => p.id === id);
if (!proposal) return null;
const { type } = getLargestVoteType(votes);
return (
<div key={i} className="pt-2.5">
<ProposalCard propData={proposal} isCompact={true} />
<div className="mt-1.5 text-sm">{`Voted ${type} ${VoteTypeToIcon[type]}`}</div>
</div>
);
})
) : (
<p className="text-gray-600">This delegate has not voted for governance proposals yet</p>
)}
</div>
);
export type DelegateParams = { params: { address: Address } };

export async function generateMetadata({ params: { address } }: DelegateParams) {
const metadata = getDelegateeMetadata();

const data = metadata[address];

return {
openGraph: {
title: `${data.name} - ${shortenAddress(address)}`,
description: `Delegate to ${data.name} | ${address}`,
},
twitter: {
title: shortenAddress(address),
creator: getXName(data),
site: '@celo',
card: 'summary_large_image',
},
};
}

function DelegateeDetails({ delegatee }: { delegatee: Delegatee }) {
return (
<div className="space-y-4 lg:w-[330px] lg:min-w-[20rem]">
<div className="border-taupe-300 p-3 lg:border">
<DelegateButton delegatee={delegatee} />
</div>
<div className="hidden border-taupe-300 p-3 lg:block lg:border">
<DelegatorsTable delegatee={delegatee} />
</div>
</div>
);
export default function Page({ params: { address } }: DelegateParams) {
return <DelegatePage address={address} />;
}
26 changes: 26 additions & 0 deletions src/app/delegate/[address]/twitter-image.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { OpenGraphImage } from 'src/app/twitter-image';
import { Background } from 'src/components/open-graph/Background';
import { MondoWithSubText } from 'src/components/open-graph/MondoLogo';
import { Portrait } from 'src/components/open-graph/Portrait';
import { getDelegateeMetadata } from 'src/features/delegation/delegateeMetadata';

export { contentType, size } from 'src/app/twitter-image';

export const alt = 'Delegate';

export default function Image({ params: { address } }: { params: { address: Address } }) {
const metadata = getDelegateeMetadata();

const { name, logoUri } = metadata[address];

return OpenGraphImage({ children: <Delgatee name={name} address={address} logoUri={logoUri} /> });
}

function Delgatee({ name, logoUri, address }: { name: string; address: Address; logoUri: string }) {
return (
<Background direction="h">
<MondoWithSubText baseSize={40} subText="Delegate" />
<Portrait name={name} relativeImage={logoUri} address={address} />
</Background>
);
}
37 changes: 17 additions & 20 deletions src/app/delegate/page.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,24 @@
'use client';

import { Metadata } from 'next';
import Link from 'next/link';
import { Fade } from 'src/components/animation/Fade';
import { FullWidthSpinner } from 'src/components/animation/Spinner';
import { CtaCard } from 'src/components/layout/CtaCard';
import { Section } from 'src/components/layout/Section';
import { H1 } from 'src/components/text/headers';
import { DelegateesTable } from 'src/features/delegation/components/DelegateesTable';
import { useDelegatees } from 'src/features/delegation/hooks/useDelegatees';
import { DelegateeTableSection } from 'src/features/delegation/components/DelegateesTable';

const basicTitleDecription = {
title: 'Celo Mondo | Delegatees',
description: 'Delegate voting power to a delegatee of your choice.',
};

export const metadata: Metadata = {
...basicTitleDecription,
openGraph: basicTitleDecription,
twitter: {
title: 'Celo Mondo', // shown on twitter cards
site: '@celo',
card: 'summary_large_image',
},
};

export default function Page() {
return (
Expand All @@ -21,20 +32,6 @@ export default function Page() {
);
}

function DelegateeTableSection() {
const { delegatees } = useDelegatees();

if (!delegatees) {
return <FullWidthSpinner>Loading delegate data</FullWidthSpinner>;
}

return (
<Fade show>
<DelegateesTable delegatees={delegatees} />
</Fade>
);
}

function RegisterCtaCard() {
return (
<CtaCard>
Expand Down
9 changes: 9 additions & 0 deletions src/app/delegate/register/twitter-image.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { OpenGraphImage } from 'src/app/twitter-image';
import { BasePage } from 'src/components/open-graph/BasePage';
export { contentType, size } from 'src/app/twitter-image';
export const runtime = 'edge';

export const alt = 'Delegate Registration';
export default function Image() {
return OpenGraphImage({ children: <BasePage title="Become a Delegate" /> });
}
9 changes: 9 additions & 0 deletions src/app/delegate/twitter-image.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { OpenGraphImage } from 'src/app/twitter-image';
import { BasePage } from 'src/components/open-graph/BasePage';
export { contentType, size } from 'src/app/twitter-image';
export const runtime = 'edge';

export const alt = 'Delegate';
export default function Image() {
return OpenGraphImage({ children: <BasePage title="Delegate" /> });
}
Loading

0 comments on commit 74570e5

Please sign in to comment.