diff --git a/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/(chainPage)/apis/chain-seo.ts b/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/(chainPage)/apis/chain-seo.ts new file mode 100644 index 00000000000..1afcb4b0c18 --- /dev/null +++ b/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/(chainPage)/apis/chain-seo.ts @@ -0,0 +1,50 @@ +import "server-only"; +import { unstable_cache } from "next/cache"; + +export type ChainSeo = { + title: string; + description: string; + og: { + title: string; + description: string; + site_name: string; + url: string; + }; + faqs: Array<{ + title: string; + description: string; + }>; + chain: { + chainId: number; + name: string; + slug: string; + nativeCurrency: { + name: string; + symbol: string; + decimals: number; + }; + testnet: boolean; + is_deprecated: boolean; + }; +}; + +export const fetchChainSeo = unstable_cache( + async (chainId: number) => { + const url = new URL( + `https://seo-pages-generator-5814.zeet-nftlabs.zeet.app/chain/${chainId}`, + ); + const res = await fetch(url, { + headers: { + "Content-Type": "application/json", + }, + }); + + if (!res.ok) { + return undefined; + } + + return res.json() as Promise; + }, + ["chain-seo"], + { revalidate: 60 * 60 * 24 }, // 24 hours +); diff --git a/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/(chainPage)/components/server/ChainOverviewSection.tsx b/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/(chainPage)/components/server/ChainOverviewSection.tsx index 398ded3900a..268c25c58dd 100644 --- a/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/(chainPage)/components/server/ChainOverviewSection.tsx +++ b/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/(chainPage)/components/server/ChainOverviewSection.tsx @@ -11,7 +11,7 @@ export function ChainOverviewSection(props: { chain: ChainMetadata }) { return (
-
+
{/* Info */} {chain.infoURL && ( diff --git a/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/(chainPage)/components/server/SupportedProductsSection.tsx b/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/(chainPage)/components/server/SupportedProductsSection.tsx index 5ba10320dab..ec7ec06c3af 100644 --- a/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/(chainPage)/components/server/SupportedProductsSection.tsx +++ b/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/(chainPage)/components/server/SupportedProductsSection.tsx @@ -1,4 +1,3 @@ -import { CircleCheckIcon } from "lucide-react"; import Link from "next/link"; import type { ChainMetadataWithServices } from "@/types/chain"; import { products } from "../../../../components/server/products"; @@ -24,15 +23,18 @@ export function SupportedProductsSection(props: { {enabledProducts.map((product) => { return (
- - +
+
+ +
+
-

+

-

+

{explorer.name}

+
+

+ Frequently asked questions +

+
+ {props.faqs.map((faq, faqIndex) => ( + + ))} +
+
+

+ ); +} + +function FaqItem(props: { + title: string; + description: string; + className?: string; +}) { + const [isOpen, setIsOpenn] = useState(false); + const contentId = useId(); + return ( + +
+

+ +

+ +

+ {props.description} +

+
+
+ ); +} diff --git a/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/(chainPage)/components/server/primary-info-item.tsx b/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/(chainPage)/components/server/primary-info-item.tsx index 7986cb54b50..ca86888bfbb 100644 --- a/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/(chainPage)/components/server/primary-info-item.tsx +++ b/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/(chainPage)/components/server/primary-info-item.tsx @@ -6,9 +6,7 @@ export function PrimaryInfoItem(props: { return (
-

- {props.title} -

+

{props.title}

{props.titleIcon}
{props.children} diff --git a/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/(chainPage)/layout.tsx b/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/(chainPage)/layout.tsx index 475a4a99c86..333eadc8e8c 100644 --- a/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/(chainPage)/layout.tsx +++ b/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/(chainPage)/layout.tsx @@ -22,39 +22,51 @@ import { getClientThirdwebClient } from "@/constants/thirdweb-client.client"; import { mapV4ChainToV5Chain } from "@/utils/map-chains"; import { TeamHeader } from "../../../../team/components/TeamHeader/team-header"; import { StarButton } from "../../components/client/star-button"; -import { getChain, getChainMetadata } from "../../utils"; +import { getChain, getCustomChainMetadata } from "../../utils"; +import { fetchChainSeo } from "./apis/chain-seo"; import { AddChainToWallet } from "./components/client/add-chain-to-wallet"; import { ChainPageView } from "./components/client/chain-pageview"; import { ChainHeader } from "./components/server/chain-header"; -// TODO: improve the behavior when clicking "Get started with thirdweb", currently just redirects to the dashboard +type Params = Promise<{ chain_id: string }>; export async function generateMetadata(props: { - params: Promise<{ chain_id: string }>; -}): Promise { + params: Params; +}): Promise { const params = await props.params; const chain = await getChain(params.chain_id); - const sanitizedChainName = chain.name.replace("Mainnet", "").trim(); - const title = `${sanitizedChainName}: RPC and Chain Settings`; + const chainSeo = await fetchChainSeo(Number(chain.chainId)).catch( + () => undefined, + ); - const description = `Use the best ${sanitizedChainName} RPC and add to your wallet. Discover the chain ID, native token, explorers, and ${ - chain.testnet && chain.faucets?.length ? "faucet options" : "more" - }.`; + if (!chainSeo) { + return undefined; + } return { - description, + title: chainSeo.title, + description: chainSeo.description, + metadataBase: new URL("https://thirdweb.com"), openGraph: { - description, - title, + title: chainSeo.og.title, + description: chainSeo.og.description, + siteName: "thirdweb", + type: "website", + url: "https://thirdweb.com", + }, + twitter: { + title: chainSeo.og.title, + description: chainSeo.og.description, + card: "summary_large_image", + creator: "@thirdweb", + site: "@thirdweb", }, - title, }; } -// this is the dashboard layout file export default async function ChainPageLayout(props: { children: React.ReactNode; - params: Promise<{ chain_id: string }>; + params: Params; }) { const params = await props.params; const { children } = props; @@ -67,8 +79,10 @@ export default async function ChainPageLayout(props: { redirect(chain.slug); } - const chainMetadata = await getChainMetadata(chain.chainId); + const customChainMetadata = getCustomChainMetadata(chain.chainId); + const chainSeo = await fetchChainSeo(chain.chainId); const client = getClientThirdwebClient(undefined); + const description = customChainMetadata?.about || chainSeo?.description; return (
@@ -123,7 +137,7 @@ export default async function ChainPageLayout(props: { @@ -131,7 +145,7 @@ export default async function ChainPageLayout(props: {
{/* Gas Sponsored badge - Mobile */} - {chainMetadata?.gasSponsored && ( + {customChainMetadata?.gasSponsored && (
@@ -153,7 +167,7 @@ export default async function ChainPageLayout(props: { )} {/* Gas Sponsored badge - Desktop */} - {chainMetadata?.gasSponsored && ( + {customChainMetadata?.gasSponsored && (
@@ -161,9 +175,9 @@ export default async function ChainPageLayout(props: {
{/* description */} - {chainMetadata?.about && ( -

- {chainMetadata.about} + {description && ( +

+ {description}

)} diff --git a/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/(chainPage)/page.tsx b/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/(chainPage)/page.tsx index de279ca8a52..b93e0f4e439 100644 --- a/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/(chainPage)/page.tsx +++ b/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/(chainPage)/page.tsx @@ -1,30 +1,37 @@ import { CircleAlertIcon } from "lucide-react"; import { getRawAccount } from "@/api/account/get-account"; import { getClientThirdwebClient } from "@/constants/thirdweb-client.client"; -import { getChain, getChainMetadata } from "../../utils"; +import { getChain, getCustomChainMetadata } from "../../utils"; +import { fetchChainSeo } from "./apis/chain-seo"; import { BuyFundsSection } from "./components/client/BuyFundsSection"; import { ChainOverviewSection } from "./components/server/ChainOverviewSection"; import { ChainCTA } from "./components/server/cta-card"; import { ExplorersSection } from "./components/server/explorer-section"; import { FaucetSection } from "./components/server/FaucetSection"; +import { FaqSection } from "./components/server/faq-section"; import { SupportedProductsSection } from "./components/server/SupportedProductsSection"; -export default async function Page(props: { +type Props = { params: Promise<{ chain_id: string }>; -}) { +}; + +export default async function Page(props: Props) { const params = await props.params; const chain = await getChain(params.chain_id); - const chainMetadata = await getChainMetadata(chain.chainId); + const customChainMetadata = getCustomChainMetadata(Number(params.chain_id)); + const chainSeo = await fetchChainSeo(Number(chain.chainId)).catch( + () => undefined, + ); const client = getClientThirdwebClient(); const isDeprecated = chain.status === "deprecated"; - const account = await getRawAccount(); return (
{/* Custom CTA */} - {(chainMetadata?.cta?.title || chainMetadata?.cta?.description) && ( - + {(customChainMetadata?.cta?.title || + customChainMetadata?.cta?.description) && ( + )} {/* Deprecated Alert */} @@ -58,6 +65,10 @@ export default async function Page(props: { {chain.services.filter((s) => s.enabled).length > 0 && ( )} + + {chainSeo?.faqs && chainSeo.faqs.length > 0 && ( + + )}
); } diff --git a/apps/dashboard/src/app/(app)/(dashboard)/(chain)/chainlist/components/server/chainlist-card.tsx b/apps/dashboard/src/app/(app)/(dashboard)/(chain)/chainlist/components/server/chainlist-card.tsx index 58205d2ad0c..107b080b2e7 100644 --- a/apps/dashboard/src/app/(app)/(dashboard)/(chain)/chainlist/components/server/chainlist-card.tsx +++ b/apps/dashboard/src/app/(app)/(dashboard)/(chain)/chainlist/components/server/chainlist-card.tsx @@ -5,7 +5,7 @@ import { CopyTextButton } from "@/components/ui/CopyTextButton"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import type { ChainSupportedService } from "@/types/chain"; import { ChainIcon } from "../../../components/server/chain-icon"; -import { getChainMetadata } from "../../../utils"; +import { getCustomChainMetadata } from "../../../utils"; type ChainListCardProps = { favoriteButton: JSX.Element | undefined; @@ -18,7 +18,7 @@ type ChainListCardProps = { iconUrl?: string; }; -export async function ChainListCard({ +export function ChainListCard({ isDeprecated, chainId, chainName, @@ -28,7 +28,7 @@ export async function ChainListCard({ favoriteButton, iconUrl, }: ChainListCardProps) { - const chainMetadata = await getChainMetadata(chainId); + const customChainMetadata = getCustomChainMetadata(chainId); return (
@@ -90,9 +90,9 @@ export async function ChainListCard({ - {(isDeprecated || chainMetadata?.gasSponsored) && ( + {(isDeprecated || customChainMetadata?.gasSponsored) && (
- {!isDeprecated && chainMetadata?.gasSponsored && ( + {!isDeprecated && customChainMetadata?.gasSponsored && (

Gas Sponsored

diff --git a/apps/dashboard/src/app/(app)/(dashboard)/(chain)/chainlist/components/server/chainlist-row.tsx b/apps/dashboard/src/app/(app)/(dashboard)/(chain)/chainlist/components/server/chainlist-row.tsx index a95bed9669d..fefb8e26749 100644 --- a/apps/dashboard/src/app/(app)/(dashboard)/(chain)/chainlist/components/server/chainlist-row.tsx +++ b/apps/dashboard/src/app/(app)/(dashboard)/(chain)/chainlist/components/server/chainlist-row.tsx @@ -13,7 +13,7 @@ import { cn } from "@/lib/utils"; import type { ChainSupportedService } from "@/types/chain"; import { ChainIcon } from "../../../components/server/chain-icon"; import { products } from "../../../components/server/products"; -import { getChainMetadata } from "../../../utils"; +import { getCustomChainMetadata } from "../../../utils"; type ChainListRowProps = { favoriteButton: JSX.Element | undefined; @@ -26,7 +26,7 @@ type ChainListRowProps = { iconUrl?: string; }; -export async function ChainListRow({ +export function ChainListRow({ isDeprecated, chainId, chainName, @@ -36,7 +36,7 @@ export async function ChainListRow({ favoriteButton, iconUrl, }: ChainListRowProps) { - const chainMetadata = await getChainMetadata(chainId); + const chainMetadata = getCustomChainMetadata(chainId); return ( {/* Name */} diff --git a/apps/dashboard/src/app/(app)/(dashboard)/(chain)/utils.ts b/apps/dashboard/src/app/(app)/(dashboard)/(chain)/utils.ts index 1d90dbc0e0e..ed2848b3be9 100644 --- a/apps/dashboard/src/app/(app)/(dashboard)/(chain)/utils.ts +++ b/apps/dashboard/src/app/(app)/(dashboard)/(chain)/utils.ts @@ -914,9 +914,9 @@ const chainMetaRecord = { } satisfies Record; // END TEMPORARY -export async function getChainMetadata( +export function getCustomChainMetadata( chainId: number, -): Promise<(ExtraChainMetadata & { gasSponsored?: true }) | null> { +): (ExtraChainMetadata & { gasSponsored?: true }) | null { // TODO: fetch this from the API if (chainId in chainMetaRecord) { return {