Files
supabase/apps/docs/app/guides/integrations/layout.tsx
d23f86021a feat(www): Partner Catalog update (#46757)
## Info architecture change around "Partners"

The www "integrations" now become more partner-driven.
`/partners/integrations` -> now Partner Catalog under
`/partners/catalog` (old links redirect to new paths)

Moved them close together in the nav dropdown and in the footer
<img width="494" height="336" alt="Screenshot 2026-07-09 at 11 06 41"
src="https://github.com/user-attachments/assets/a875fef0-0ab8-47ca-8756-d658b27c4892"
/>
<img width="1149" height="665" alt="Screenshot 2026-07-09 at 11 09 48"
src="https://github.com/user-attachments/assets/9631bb72-fe25-4fb4-b1af-9f14a37d02e7"
/>

## /partners 

This page remains untouched in this PR, updates to layout, content and
intake form are delegated to #47874

## /partners/catalog

Listed in the
[catalog](https://zone-www-dot-com-git-feat-www-partners-pages-supabase.vercel.app/partners/catalog)
are now partners. Some partners match with a listing.

<img width="1207" height="866" alt="Screenshot 2026-07-09 at 11 14 17"
src="https://github.com/user-attachments/assets/b65216be-976f-4ef5-91f8-1ad49da87b45"
/>

## /partners/catalog/[partner]

Each partner can have one or more "listings" which are either 
- simple guides
- foreign data wrappers
- dashboard integrations

Integrations available in the dashboard now all have a prominent
"Install integration" cta to open it in the dashboard [integrations
page](https://supabase.com/dashboard/project/_/integrations).

<img width="1269" height="776" alt="Screenshot 2026-07-09 at 11 16 51"
src="https://github.com/user-attachments/assets/3c7bb715-ffce-4d0a-905f-9a660c3b1f5a"
/>

## Docs

Update docs →
[Preview](https://docs-git-feat-www-partners-pages-supabase.vercel.app/docs/guides/integrations)
- remove "Supabase marketplace"
- use "Dashboard Integrations and Partner Catalog
- update integrations in sidenav to link to updated /partners/catalog/**
listings

<img width="1520" height="696" alt="Screenshot 2026-07-15 at 12 54 47"
src="https://github.com/user-attachments/assets/9f5a2794-4536-4299-97df-9732d3d75b4c"
/>

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Added a Partner Catalog experience with search, category filters,
official-partner toggle, responsive filtering (sidebar + bottom sheet),
grid/list views, and featured partners.
* Added Partner Catalog detail pages with tabbed listings, MDX-rendered
content, image gallery with zoom overlay, and “add/install” actions.
* **Improvements**
* Updated “Become a Partner” layout and form support for prefilled
values and checkbox-group fields (including validation).
* Updated navigation/footer/docs and partner tile links to use Partner
Catalog routes; expanded redirects from legacy integrations paths.
  * Added public agent-skills discovery manifest.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Alan Daniel <stylesshjs@gmail.com>
Co-authored-by: Alex Hall <alex.hall@supabase.io>
Co-authored-by: Miranda Limonczenko <miranda.limonczenko@supabase.io>
2026-07-15 11:10:51 +00:00

103 lines
3.8 KiB
TypeScript

import { type NavMenuSection } from '~/components/Navigation/Navigation.types'
import { REVALIDATION_TAGS } from '~/features/helpers.fetch'
import Layout from '~/layouts/guides'
import { IS_PLATFORM } from 'common'
import {
createMarketplaceClient,
SUPABASE_LISTING_OVERRIDES,
SUPABASE_PARTNER_SLUG,
type CatalogListing,
} from 'common/marketplace-client'
import { unstable_cache } from 'next/cache'
export default async function IntegrationsLayout({ children }: { children: React.ReactNode }) {
const additionalNavItems = { integrations: await getPartners() }
return <Layout additionalNavItems={additionalNavItems}>{children}</Layout>
}
// Will need to turn on revalidation later, just turning it off for now so we
// can slowly turn things back on while monitoring usage
const getPartners = unstable_cache(getPartnersImpl, [], {
tags: [REVALIDATION_TAGS.PARTNERS],
})
async function getPartnersImpl() {
if (!IS_PLATFORM) return []
const marketplaceClient = createMarketplaceClient()
const [{ data: partners, error }, { data: listings }] = await Promise.all([
marketplaceClient.from('partners').select('slug, name, type'),
marketplaceClient.from('catalog_listings').select('*'),
])
if (error) {
console.error(new Error('Error fetching partners', { cause: error }))
}
// Only "technology" partners belong in this list, not agencies/experts.
const technologyPartnerSlugs = new Set<string>()
const partnerNameBySlug = new Map<string, string>()
for (const partner of partners ?? []) {
if (!partner.slug) continue
partnerNameBySlug.set(partner.slug, partner.name ?? partner.slug)
if (partner.type === 'technology') technologyPartnerSlugs.add(partner.slug)
}
// Supabase-owned listings still get their own nav item, at their clean catalog page slug.
const supabaseOwnedNavItems: Partial<NavMenuSection>[] = []
const listingsByPartnerSlug = new Map<string, CatalogListing[]>()
for (const listing of listings ?? []) {
if (!listing.slug) continue
const override = SUPABASE_LISTING_OVERRIDES[listing.slug]
if (override) {
supabaseOwnedNavItems.push({
name: listing.title || override.name,
url: `https://supabase.com/partners/catalog/${override.slug}` as `https://${string}`,
})
continue
}
const partnerSlug = listing.partner_slug
if (
!partnerSlug ||
partnerSlug === SUPABASE_PARTNER_SLUG ||
!technologyPartnerSlugs.has(partnerSlug)
) {
continue
}
const partnerListings = listingsByPartnerSlug.get(partnerSlug) ?? []
partnerListings.push(listing)
listingsByPartnerSlug.set(partnerSlug, partnerListings)
}
// One nav item per listing (a partner with multiple listings — e.g. an FDW plus a
// Dashboard Integration — gets one entry per listing), linking straight to the tab
// that listing occupies on its Partner Catalog page.
const partnerNavItems: Partial<NavMenuSection>[] = [
...Array.from(listingsByPartnerSlug.entries()).flatMap(([partnerSlug, partnerListings]) => {
const partnerName = partnerNameBySlug.get(partnerSlug) ?? partnerSlug
const catalogUrl = `https://supabase.com/partners/catalog/${partnerSlug}`
// Show only listing name if partner has one listing
if (partnerListings.length === 1) {
const listingName = partnerListings[0].title || partnerName
return [{ name: listingName, url: catalogUrl as `https://${string}` }]
}
// otherwise show listing name (or slug if no title) plus partner name.
return partnerListings.map((listing) => ({
name: `${listing.title || listing.slug} (${partnerName})`,
url: `${catalogUrl}?tab=${listing.slug}` as `https://${string}`,
}))
}),
...supabaseOwnedNavItems,
]
partnerNavItems.sort((a, b) => (a.name ?? '').localeCompare(b.name ?? ''))
return partnerNavItems
}