diff --git a/apps/www/app/partners/[slug]/page.tsx b/apps/www/app/partners/[slug]/page.tsx new file mode 100644 index 00000000000..4aec577e86d --- /dev/null +++ b/apps/www/app/partners/[slug]/page.tsx @@ -0,0 +1,21 @@ +import supabase from '~/lib/supabaseMisc' +import { notFound, redirect } from 'next/navigation' + +type Params = { slug: string } + +export const dynamic = 'force-dynamic' + +export default async function PartnerLegacyPage({ params }: { params: Promise }) { + const { slug } = await params + + const { data: partner } = await supabase + .from('partners') + .select('slug, type') + .eq('approved', true) + .eq('slug', slug) + .single() + + if (!partner || partner.type === 'expert') notFound() + + redirect(`/partners/catalog/${partner.slug}`) +} diff --git a/apps/www/pages/partners/catalog/[slug].tsx b/apps/www/app/partners/catalog/[slug]/PartnerCatalogDetail.tsx similarity index 81% rename from apps/www/pages/partners/catalog/[slug].tsx rename to apps/www/app/partners/catalog/[slug]/PartnerCatalogDetail.tsx index 171587d7fbf..80c79eb1a9d 100644 --- a/apps/www/pages/partners/catalog/[slug].tsx +++ b/apps/www/app/partners/catalog/[slug]/PartnerCatalogDetail.tsx @@ -1,32 +1,25 @@ -import { remarkCodeHike, type CodeHikeConfig } from '@code-hike/mdx' +'use client' + import { CH } from '@code-hike/mdx/components' import { AnimatePresence, motion } from 'framer-motion' import { ChevronLeft, ExternalLink } from 'lucide-react' -import { type GetStaticPaths, type GetStaticProps } from 'next' import type { SerializeResult as MDXRemoteSerializeResult } from 'next-mdx-remote-client' import { MDXClient } from 'next-mdx-remote-client/csr' -import { serialize } from 'next-mdx-remote-client/serialize' -import { NextSeo } from 'next-seo' import Image from 'next/image' import Link from 'next/link' import { useState, type Dispatch, type SetStateAction } from 'react' -import remarkGfm from 'remark-gfm' import 'swiper/css' import DefaultLayout from '~/components/Layouts/Default' import SectionContainer from '~/components/Layouts/SectionContainer' -import { getCatalogPartner, listCatalogPartnerSlugs } from '~/lib/marketplaceDb' import { type ListingDetail, type Partner } from '~/types/partners' import { useBreakpoint } from 'common' -import codeHikeTheme from 'config/code-hike.theme.json' with { type: 'json' } import { Swiper, SwiperSlide } from 'swiper/react' import { Button, cn } from 'ui' import { Admonition } from 'ui-patterns/admonition' import { ExpandableVideo } from 'ui-patterns/ExpandableVideo' -import Error404 from '../../404' - function mdxComponents(callback: Dispatch>) { return { CH, @@ -37,20 +30,16 @@ function mdxComponents(callback: Dispatch>) { } } -type PartnerData = { +type Props = { partner: Partner serializedListings: MDXRemoteSerializeResult, Record>[] } -function PartnerPage({ partner, serializedListings }: PartnerData) { +export default function PartnerCatalogDetail({ partner, serializedListings }: Props) { const [focusedImage, setFocusedImage] = useState(null) const [activeTab, setActiveTab] = useState(0) const isNarrow = useBreakpoint('lg') - if (!partner) return - - // For marketplace DB, partner.listings has per-listing content. - // For legacy DB, synthesise a single tab from the top-level partner data. const allListings: ListingDetail[] = partner.listings?.length ? partner.listings : [ @@ -68,20 +57,11 @@ function PartnerPage({ partner, serializedListings }: PartnerData) { const safeTab = Math.min(activeTab, allListings.length - 1) const activeListing = allListings[safeTab] const activeOverview = serializedListings[safeTab] ?? serializedListings[0] + // Prefer the Supabase dashboard integration URL for marketplace integrations. + const installHref = activeListing.dashboardUrl ?? activeListing.installUrl return ( <> - - {focusedImage && ( e.stopPropagation()} > - {activeListing.installUrl && ( + {installHref && ( @@ -254,13 +234,13 @@ function PartnerPage({ partner, serializedListings }: PartnerData) { {!isNarrow && } - {activeListing.installUrl && ( + {installHref && (

Get started with {partner.title} and Supabase.

- + @@ -275,13 +255,13 @@ function PartnerPage({ partner, serializedListings }: PartnerData) { ) } -const PartnerDetails = ({ +function PartnerDetails({ partner, activeListing, }: { partner: Partner activeListing: ListingDetail -}) => { +}) { return (
@@ -368,63 +348,3 @@ const PartnerDetails = ({
) } - -export const getStaticPaths: GetStaticPaths = async () => { - const slugs = await listCatalogPartnerSlugs() - - const paths = slugs?.map((slug) => ({ params: { slug } })) ?? [] - - return { paths, fallback: 'blocking' } -} - -export const getStaticProps: GetStaticProps = async ({ params }) => { - const partner = await getCatalogPartner(params!.slug as string) - - if (!partner) { - return { notFound: true } - } - - const codeHikeOptions: CodeHikeConfig = { - theme: codeHikeTheme, - lineNumbers: true, - showCopyButton: true, - skipLanguages: [], - autoImport: false, - } - - // For marketplace DB, partner.listings has all per-listing content. - // For legacy DB, fall back to a single synthetic listing from partner-level data. - const listingsForTabs: ListingDetail[] = partner.listings?.length - ? partner.listings - : [ - { - slug: partner.slug, - label: 'Overview', - content: partner.content, - installUrl: partner.installUrl, - docsUrl: partner.docsUrl, - images: partner.images, - youtubeId: partner.youtubeId, - }, - ] - - const serializedListings = await Promise.all( - listingsForTabs.map((listing) => - serialize({ - source: listing.content, - options: { - mdxOptions: { - remarkPlugins: [remarkGfm, [remarkCodeHike, codeHikeOptions]], - }, - }, - }) - ) - ) - - return { - props: { partner, serializedListings }, - revalidate: 1800, - } -} - -export default PartnerPage diff --git a/apps/www/app/partners/catalog/[slug]/page.tsx b/apps/www/app/partners/catalog/[slug]/page.tsx new file mode 100644 index 00000000000..cb68b729c20 --- /dev/null +++ b/apps/www/app/partners/catalog/[slug]/page.tsx @@ -0,0 +1,82 @@ +import { remarkCodeHike, type CodeHikeConfig } from '@code-hike/mdx' +import { getCatalogPartner, listCatalogPartnerSlugs } from '~/lib/marketplaceDb' +import type { ListingDetail } from '~/types/partners' +import codeHikeTheme from 'config/code-hike.theme.json' with { type: 'json' } +import type { Metadata } from 'next' +import { serialize } from 'next-mdx-remote-client/serialize' +import { notFound } from 'next/navigation' +import { cache } from 'react' +import remarkGfm from 'remark-gfm' + +import PartnerCatalogDetail from './PartnerCatalogDetail' + +export const revalidate = 1800 + +type Params = { slug: string } + +// Deduplicate the DB call across generateMetadata and the page component. +const getPartner = cache(getCatalogPartner) + +export async function generateStaticParams() { + const slugs = await listCatalogPartnerSlugs() + return slugs.map((slug) => ({ slug })) +} + +export async function generateMetadata({ params }: { params: Promise }): Promise { + const { slug } = await params + const partner = await getPartner(slug) + if (!partner) return {} + return { + title: `${partner.title} | Works With Supabase`, + description: partner.description, + openGraph: { + title: `${partner.title} | Works With Supabase`, + description: partner.description, + url: `https://supabase.com/partners/catalog/${partner.slug}`, + images: [{ url: partner.images?.[0] ?? partner.logo }], + }, + } +} + +export default async function PartnerCatalogPage({ params }: { params: Promise }) { + const { slug } = await params + const partner = await getPartner(slug) + if (!partner) notFound() + + const codeHikeOptions: CodeHikeConfig = { + theme: codeHikeTheme, + lineNumbers: true, + showCopyButton: true, + skipLanguages: [], + autoImport: false, + } + + const listingsForTabs: ListingDetail[] = partner.listings?.length + ? partner.listings + : [ + { + slug: partner.slug, + label: 'Overview', + content: partner.content, + installUrl: partner.installUrl, + docsUrl: partner.docsUrl, + images: partner.images, + youtubeId: partner.youtubeId, + }, + ] + + const serializedListings = await Promise.all( + listingsForTabs.map((listing) => + serialize({ + source: listing.content, + options: { + mdxOptions: { + remarkPlugins: [remarkGfm, [remarkCodeHike, codeHikeOptions]], + }, + }, + }) + ) + ) + + return +} diff --git a/apps/www/lib/marketplaceDb.ts b/apps/www/lib/marketplaceDb.ts index 720cc0982e9..51ca2069114 100644 --- a/apps/www/lib/marketplaceDb.ts +++ b/apps/www/lib/marketplaceDb.ts @@ -165,8 +165,11 @@ async function getPartnersFromMarketplace(): Promise { */ function getLabelForListing(listing: Listing): string { if (listing.publish_dashboard) return 'Marketplace Integration' - const t = listing.title.toLowerCase() - if (t.includes('foreign data wrapper') || /\bfdw\b/.test(t)) return 'Foreign Data Wrapper' + const isFdw = listing.categories?.some( + (c) => + c.name.toLowerCase().includes('foreign data wrapper') || c.slug.toLowerCase().includes('fdw') + ) + if (isFdw) return 'Foreign Data Wrapper' if (listing.marketplace_url) return 'Integration' return 'Guide' } @@ -211,6 +214,9 @@ async function getPartnerFromMarketplace(slug: string): Promise label: getLabelForListing(listing), content: listing.content, installUrl: listing.marketplace_url ?? null, + dashboardUrl: listing.publish_dashboard + ? `https://supabase.com/dashboard/project/_/integrations/${listing.slug}/overview` + : null, docsUrl: listing.documentation_url || null, images: listing.images?.map(fullImageUrl) ?? [], youtubeId: listing.youtube_id ?? null, diff --git a/apps/www/pages/partners/[slug].tsx b/apps/www/pages/partners/[slug].tsx deleted file mode 100644 index 0380254603c..00000000000 --- a/apps/www/pages/partners/[slug].tsx +++ /dev/null @@ -1,40 +0,0 @@ -import supabase from '~/lib/supabaseMisc' -import { GetStaticPaths, GetStaticProps } from 'next' - -import Error404 from '../404' - -function PartnerPage() { - // Existing short partner URLs redirect to their current integration pages. - return -} - -export const getStaticPaths: GetStaticPaths = async () => { - return { - paths: [], - fallback: 'blocking', - } -} - -export const getStaticProps: GetStaticProps = async ({ params }) => { - let { data: partner } = await supabase - .from('partners') - .select('*') - .eq('approved', true) - .eq('slug', params!.slug as string) - .single() - - if (!partner || partner.type === 'expert') { - return { - notFound: true, - } - } - - return { - redirect: { - permanent: false, - destination: `/partners/catalog/${partner.slug}`, - }, - } -} - -export default PartnerPage diff --git a/apps/www/types/partners.ts b/apps/www/types/partners.ts index 690e4c7d801..bcf748d5fe6 100644 --- a/apps/www/types/partners.ts +++ b/apps/www/types/partners.ts @@ -12,6 +12,7 @@ export type ListingDetail = { label: string content: string installUrl: string | null + dashboardUrl?: string | null docsUrl: string | null images: string[] youtubeId: string | null