Skip to content

Commit

Permalink
Feat/page title token name (solana-foundation#249)
Browse files Browse the repository at this point in the history
For the title metadata generation, Next.js doesn't allow using
client-side hooks on server-side components (see [this
issue](vercel/next.js#46372)). Thus, we can't
use `useTokenRegistry` as it's marked with 'use client' for frontend.

When users visit each address page with the token name title, both the
client-side and server-side rendering fetches the token list. These
identical fetches are optimized by Next.js via [automatic fetch request
deduping](https://nextjs.org/docs/app/building-your-application/data-fetching#automatic-fetch-request-deduping).

> It's very important not to create two URLs with the same page title.
Consider the case where two tokens have identical names; how would you
prevent their URLs from rendering the same title?
1. We could append a small slice of address to the token name, e.g. USD
Coin (EPjFW), but this wouldn't prevent same token names with the same
"small slice" (using vanity address).
2. Another method could be just highlighting the official tokens, e.g.
USD Coin (official), and leaving the other non-official tokens as-is,
but this requires an active management of "official" tokens.

Fixes solana-foundation#243.

---------

Co-authored-by: steveluscher <[email protected]>
  • Loading branch information
jdubpark and steveluscher authored Jun 5, 2023
1 parent 15eff2f commit a12cbc1
Show file tree
Hide file tree
Showing 19 changed files with 125 additions and 61 deletions.
7 changes: 4 additions & 3 deletions app/address/[address]/anchor-account/page.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import getReadableTitleFromAddress, { AddressPageMetadataProps } from '@utils/get-readable-title-from-address';
import { Metadata } from 'next/types';

import AnchorAccountPageClient from './page-client';
Expand All @@ -8,10 +9,10 @@ type Props = Readonly<{
};
}>;

export async function generateMetadata({ params: { address } }: Props): Promise<Metadata> {
export async function generateMetadata(props: AddressPageMetadataProps): Promise<Metadata> {
return {
description: `Contents of the Anchor Account at address ${address} on Solana`,
title: `Anchor Account Data | ${address} | Solana`,
description: `Contents of the Anchor Account at address ${props.params.address} on Solana`,
title: `Anchor Account Data | ${await getReadableTitleFromAddress(props)} | Solana`,
};
}

Expand Down
7 changes: 4 additions & 3 deletions app/address/[address]/anchor-program/page.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { AnchorProgramCard } from '@components/account/AnchorProgramCard';
import { LoadingCard } from '@components/common/LoadingCard';
import getReadableTitleFromAddress, { AddressPageMetadataProps } from '@utils/get-readable-title-from-address';
import { Metadata } from 'next/types';
import { Suspense } from 'react';

Expand All @@ -9,10 +10,10 @@ type Props = Readonly<{
};
}>;

export async function generateMetadata({ params: { address } }: Props): Promise<Metadata> {
export async function generateMetadata(props: AddressPageMetadataProps): Promise<Metadata> {
return {
description: `The Interface Definition Language (IDL) file for the Anchor program at address ${address} on Solana`,
title: `Anchor Program IDL | ${address} | Solana`,
description: `The Interface Definition Language (IDL) file for the Anchor program at address ${props.params.address} on Solana`,
title: `Anchor Program IDL | ${await getReadableTitleFromAddress(props)} | Solana`,
};
}

Expand Down
7 changes: 4 additions & 3 deletions app/address/[address]/attributes/page.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import getReadableTitleFromAddress, { AddressPageMetadataProps } from '@utils/get-readable-title-from-address';
import { Metadata } from 'next/types';

import NFTAttributesPageClient from './page-client';
Expand All @@ -8,10 +9,10 @@ type Props = Readonly<{
};
}>;

export async function generateMetadata({ params: { address } }: Props): Promise<Metadata> {
export async function generateMetadata(props: AddressPageMetadataProps): Promise<Metadata> {
return {
description: `Attributes of the Metaplex NFT with address ${address} on Solana`,
title: `Metaplex NFT Attributes | ${address} | Solana`,
description: `Attributes of the Metaplex NFT with address ${props.params.address} on Solana`,
title: `Metaplex NFT Attributes | ${await getReadableTitleFromAddress(props)} | Solana`,
};
}

Expand Down
7 changes: 4 additions & 3 deletions app/address/[address]/concurrent-merkle-tree/page.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import getReadableTitleFromAddress, { AddressPageMetadataProps } from '@utils/get-readable-title-from-address';
import { Metadata } from 'next/types';

import ConcurrentMerkleTreePageClient from './page-client';
Expand All @@ -8,10 +9,10 @@ type Props = Readonly<{
};
}>;

export async function generateMetadata({ params: { address } }: Props): Promise<Metadata> {
export async function generateMetadata(props: AddressPageMetadataProps): Promise<Metadata> {
return {
description: `Contents of the SPL Concurrent Merkle Tree at address ${address} on Solana`,
title: `Concurrent Merkle Tree | ${address} | Solana`,
description: `Contents of the SPL Concurrent Merkle Tree at address ${props.params.address} on Solana`,
title: `Concurrent Merkle Tree | ${await getReadableTitleFromAddress(props)} | Solana`,
};
}

Expand Down
7 changes: 4 additions & 3 deletions app/address/[address]/domains/page.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { DomainsCard } from '@components/account/DomainsCard';
import getReadableTitleFromAddress, { AddressPageMetadataProps } from '@utils/get-readable-title-from-address';
import { Metadata } from 'next/types';

type Props = Readonly<{
Expand All @@ -7,10 +8,10 @@ type Props = Readonly<{
};
}>;

export async function generateMetadata({ params: { address } }: Props): Promise<Metadata> {
export async function generateMetadata(props: AddressPageMetadataProps): Promise<Metadata> {
return {
description: `Domain names owned by the address ${address} on Solana`,
title: `Domains | ${address} | Solana`,
description: `Domain names owned by the address ${props.params.address} on Solana`,
title: `Domains | ${await getReadableTitleFromAddress(props)} | Solana`,
};
}

Expand Down
7 changes: 4 additions & 3 deletions app/address/[address]/entries/page.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import getReadableTitleFromAddress, { AddressPageMetadataProps } from '@utils/get-readable-title-from-address';
import { Metadata } from 'next/types';

import AddressLookupTableEntriesPageClient from './page-client';
Expand All @@ -8,10 +9,10 @@ type Props = Readonly<{
};
}>;

export async function generateMetadata({ params: { address } }: Props): Promise<Metadata> {
export async function generateMetadata(props: AddressPageMetadataProps): Promise<Metadata> {
return {
description: `Entries of the address lookup table at ${address} on Solana`,
title: `Address Lookup Table Entries | ${address} | Solana`,
description: `Entries of the address lookup table at ${props.params.address} on Solana`,
title: `Address Lookup Table Entries | ${await getReadableTitleFromAddress(props)} | Solana`,
};
}

Expand Down
7 changes: 4 additions & 3 deletions app/address/[address]/instructions/page.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { TokenInstructionsCard } from '@components/account/history/TokenInstructionsCard';
import getReadableTitleFromAddress, { AddressPageMetadataProps } from '@utils/get-readable-title-from-address';
import { Metadata } from 'next/types';

type Props = Readonly<{
Expand All @@ -7,10 +8,10 @@ type Props = Readonly<{
};
}>;

export async function generateMetadata({ params: { address } }: Props): Promise<Metadata> {
export async function generateMetadata(props: AddressPageMetadataProps): Promise<Metadata> {
return {
description: `A list of transactions that include an instruction involving the token with address ${address} on Solana`,
title: `Token Instructions | ${address} | Solana`,
description: `A list of transactions that include an instruction involving the token with address ${props.params.address} on Solana`,
title: `Token Instructions | ${await getReadableTitleFromAddress(props)} | Solana`,
};
}

Expand Down
7 changes: 4 additions & 3 deletions app/address/[address]/largest/page.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { TokenLargestAccountsCard } from '@components/account/TokenLargestAccountsCard';
import getReadableTitleFromAddress, { AddressPageMetadataProps } from '@utils/get-readable-title-from-address';
import { Metadata } from 'next/types';

type Props = Readonly<{
Expand All @@ -7,10 +8,10 @@ type Props = Readonly<{
};
}>;

export async function generateMetadata({ params: { address } }: Props): Promise<Metadata> {
export async function generateMetadata(props: AddressPageMetadataProps): Promise<Metadata> {
return {
description: `Largest holders of the token with address ${address} on Solana`,
title: `Token Distribution | ${address} | Solana`,
description: `Largest holders of the token with address ${props.params.address} on Solana`,
title: `Token Distribution | ${await getReadableTitleFromAddress(props)} | Solana`,
};
}

Expand Down
7 changes: 4 additions & 3 deletions app/address/[address]/metadata/page.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import getReadableTitleFromAddress, { AddressPageMetadataProps } from '@utils/get-readable-title-from-address';
import { Metadata } from 'next/types';

import MetaplexNFTMetadataPageClient from './page-client';
Expand All @@ -8,10 +9,10 @@ type Props = Readonly<{
};
}>;

export async function generateMetadata({ params: { address } }: Props): Promise<Metadata> {
export async function generateMetadata(props: AddressPageMetadataProps): Promise<Metadata> {
return {
description: `Metadata for the Metaplex NFT with address ${address} on Solana`,
title: `Metaplex NFT Metadata | ${address} | Solana`,
description: `Metadata for the Metaplex NFT with address ${props.params.address} on Solana`,
title: `Metaplex NFT Metadata | ${await getReadableTitleFromAddress(props)} | Solana`,
};
}

Expand Down
7 changes: 4 additions & 3 deletions app/address/[address]/nftoken-collection-nfts/page.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { NFTokenCollectionNFTGrid } from '@components/account/nftoken/NFTokenCollectionNFTGrid';
import getReadableTitleFromAddress, { AddressPageMetadataProps } from '@utils/get-readable-title-from-address';
import { Metadata } from 'next/types';

type Props = Readonly<{
Expand All @@ -7,10 +8,10 @@ type Props = Readonly<{
};
}>;

export async function generateMetadata({ params: { address } }: Props): Promise<Metadata> {
export async function generateMetadata(props: AddressPageMetadataProps): Promise<Metadata> {
return {
description: `NFToken NFTs belonging to the collection ${address} on Solana`,
title: `NFToken Collection NFTs | ${address} | Solana`,
description: `NFToken NFTs belonging to the collection ${props.params.address} on Solana`,
title: `NFToken Collection NFTs | ${await getReadableTitleFromAddress(props)} | Solana`,
};
}

Expand Down
7 changes: 4 additions & 3 deletions app/address/[address]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { TransactionHistoryCard } from '@components/account/history/TransactionHistoryCard';
import getReadableTitleFromAddress, { AddressPageMetadataProps } from '@utils/get-readable-title-from-address';
import { Metadata } from 'next/types';

type Props = Readonly<{
Expand All @@ -7,10 +8,10 @@ type Props = Readonly<{
};
}>;

export async function generateMetadata({ params: { address } }: Props): Promise<Metadata> {
export async function generateMetadata(props: AddressPageMetadataProps): Promise<Metadata> {
return {
description: `History of all transactions involving the address ${address} on Solana`,
title: `Transaction History | ${address} | Solana`,
description: `History of all transactions involving the address ${props.params.address} on Solana`,
title: `Transaction History | ${await getReadableTitleFromAddress(props)} | Solana`,
};
}

Expand Down
7 changes: 4 additions & 3 deletions app/address/[address]/rewards/page.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { RewardsCard } from '@components/account/RewardsCard';
import getReadableTitleFromAddress, { AddressPageMetadataProps } from '@utils/get-readable-title-from-address';
import { Metadata } from 'next/types';

type Props = Readonly<{
Expand All @@ -7,10 +8,10 @@ type Props = Readonly<{
};
}>;

export async function generateMetadata({ params: { address } }: Props): Promise<Metadata> {
export async function generateMetadata(props: AddressPageMetadataProps): Promise<Metadata> {
return {
description: `Rewards due to the address ${address} by epoch on Solana`,
title: `Address Rewards | ${address} | Solana`,
description: `Rewards due to the address ${props.params.address} by epoch on Solana`,
title: `Address Rewards | ${await getReadableTitleFromAddress(props)} | Solana`,
};
}

Expand Down
7 changes: 4 additions & 3 deletions app/address/[address]/security/page.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import getReadableTitleFromAddress, { AddressPageMetadataProps } from '@utils/get-readable-title-from-address';
import { Metadata } from 'next/types';

import SecurityPageClient from './page-client';

export async function generateMetadata({ params: { address } }: Props): Promise<Metadata> {
export async function generateMetadata(props: AddressPageMetadataProps): Promise<Metadata> {
return {
description: `Contents of the security.txt for the program with address ${address} on Solana`,
title: `Security | ${address} | Solana`,
description: `Contents of the security.txt for the program with address ${props.params.address} on Solana`,
title: `Security | ${await getReadableTitleFromAddress(props)} | Solana`,
};
}

Expand Down
7 changes: 4 additions & 3 deletions app/address/[address]/tokens/page.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { OwnedTokensCard } from '@components/account/OwnedTokensCard';
import { TokenHistoryCard } from '@components/account/TokenHistoryCard';
import getReadableTitleFromAddress, { AddressPageMetadataProps } from '@utils/get-readable-title-from-address';
import { Metadata } from 'next/types';

type Props = Readonly<{
Expand All @@ -8,10 +9,10 @@ type Props = Readonly<{
};
}>;

export async function generateMetadata({ params: { address } }: Props): Promise<Metadata> {
export async function generateMetadata(props: AddressPageMetadataProps): Promise<Metadata> {
return {
description: `All tokens owned by the address ${address} on Solana`,
title: `Tokens | ${address} | Solana`,
description: `All tokens owned by the address ${props.params.address} on Solana`,
title: `Tokens | ${await getReadableTitleFromAddress(props)} | Solana`,
};
}

Expand Down
7 changes: 4 additions & 3 deletions app/address/[address]/transfers/page.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { TokenTransfersCard } from '@components/account/history/TokenTransfersCard';
import getReadableTitleFromAddress, { AddressPageMetadataProps } from '@utils/get-readable-title-from-address';
import { Metadata } from 'next/types';

type Props = Readonly<{
Expand All @@ -7,10 +8,10 @@ type Props = Readonly<{
};
}>;

export async function generateMetadata({ params: { address } }: Props): Promise<Metadata> {
export async function generateMetadata(props: AddressPageMetadataProps): Promise<Metadata> {
return {
description: `History of all token transfers involving the address ${address} on Solana`,
title: `Transfers | ${address} | Solana`,
description: `History of all token transfers involving the address ${props.params.address} on Solana`,
title: `Transfers | ${await getReadableTitleFromAddress(props)} | Solana`,
};
}

Expand Down
7 changes: 4 additions & 3 deletions app/address/[address]/vote-history/page.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import getReadableTitleFromAddress, { AddressPageMetadataProps } from '@utils/get-readable-title-from-address';
import { Metadata } from 'next/types';

import VoteHistoryPageClient from './page-client';
Expand All @@ -8,10 +9,10 @@ type Props = Readonly<{
};
}>;

export async function generateMetadata({ params: { address } }: Props): Promise<Metadata> {
export async function generateMetadata(props: AddressPageMetadataProps): Promise<Metadata> {
return {
description: `Vote history of the address ${address} by slot on Solana`,
title: `Vote History | ${address} | Solana`,
description: `Vote history of the address ${props.params.address} by slot on Solana`,
title: `Vote History | ${await getReadableTitleFromAddress(props)} | Solana`,
};
}

Expand Down
16 changes: 3 additions & 13 deletions app/providers/mints/token-registry.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
'use client';

import { useCluster } from '@providers/cluster';
import { Strategy, TokenInfo, TokenInfoMap, TokenListContainer, TokenListProvider } from '@solana/spl-token-registry';
import { Cluster, clusterSlug } from '@utils/cluster';
import { Strategy, TokenInfoMap } from '@solana/spl-token-registry';
import getTokenList from '@utils/get-token-list';
import React from 'react';

const TokenRegistryContext = React.createContext<TokenInfoMap>(new Map());
Expand All @@ -14,17 +14,7 @@ export function TokenRegistryProvider({ children }: ProviderProps) {
const { cluster } = useCluster();

React.useEffect(() => {
new TokenListProvider().resolve(Strategy.Solana).then((tokens: TokenListContainer) => {
const tokenList =
cluster === Cluster.Custom ? [] : tokens.filterByClusterSlug(clusterSlug(cluster)).getList();

setTokenRegistry(
tokenList.reduce((map: TokenInfoMap, item: TokenInfo) => {
map.set(item.address, item);
return map;
}, new Map())
);
});
getTokenList(cluster, Strategy.Solana).then((tokens: TokenInfoMap) => setTokenRegistry(tokens));
}, [cluster]);

return <TokenRegistryContext.Provider value={tokenRegistry}>{children}</TokenRegistryContext.Provider>;
Expand Down
46 changes: 46 additions & 0 deletions app/utils/get-readable-title-from-address.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { Strategy } from '@solana/spl-token-registry';
import { Cluster } from '@utils/cluster';
import getTokenList from '@utils/get-token-list';

export type AddressPageMetadataProps = Readonly<{
params: {
address: string;
};
searchParams: {
cluster: string;
};
}>;

export default async function getReadableTitleFromAddress(props: AddressPageMetadataProps): Promise<string> {
const {
params: { address },
searchParams: { cluster: clusterParam },
} = props;

let cluster: Cluster;
switch (clusterParam) {
case 'custom':
cluster = Cluster.Custom;
break;
case 'devnet':
cluster = Cluster.Devnet;
break;
case 'testnet':
cluster = Cluster.Testnet;
break;
default:
cluster = Cluster.MainnetBeta;
}

try {
const tokenList = await getTokenList(cluster, Strategy.Solana);
const tokenName = tokenList.get(address)?.name;
if (tokenName == null) {
return address;
}
const tokenDisplayAddress = address.slice(0, 2) + '\u2026' + address.slice(-2);
return `Token | ${tokenName} (${tokenDisplayAddress})`;
} catch {
return address;
}
}
Loading

0 comments on commit a12cbc1

Please sign in to comment.