mirror of
https://github.com/supabase/supabase.git
synced 2026-10-05 09:25:06 +03:00
feat(www): publish agent discovery catalog and complete json-ld (#49768)
I added the agent-discovery surfaces the www app was missing: a resource catalog at `/.well-known/ard.json` plus completed structured data on the homepage. I scoped this from the agent-readiness gaps that are truthfully closable on the www side; the catalog lists only resources that already exist and serve 200 (MCP OAuth metadata, Management API OpenAPI spec, llms.txt, agent-skills index). **Changed:** - **Agents can discover our machine-readable resources from one document**: new static catalog at `/.well-known/ard.json` (Agentic Resource Discovery format); the legacy `/.well-known/ai-catalog.json` path serves the same file via rewrite, keeping a single source artifact. - **Organization JSON-LD carries verifiable company details**: adds `legalName`, a support `contactPoint`, and the registered address already public on our Terms of Service. - **Homepage declares the product as an application entity**: emits `SoftwareApplication` JSON-LD via the existing `softwareApplicationSchema` builder, same pattern as the vector module page. ## To test Tested on Vercel preview: - [ ] `curl <preview-url>/.well-known/ard.json`: expect 200 with a JSON catalog of 5 entries - [ ] `curl <preview-url>/.well-known/ai-catalog.json`: expect the same document with status 200 (rewrite, not a redirect) - [ ] View homepage page source: expect three `application/ld+json` scripts: Organization now includes `address` and `contactPoint`, and a `SoftwareApplication` block is present ## Linear - fixes GROWTH-1164 <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - **New Features** - Added an Agent Resource Description catalog listing Supabase’s MCP, API, documentation, and agent skill resources. - Added support for the legacy AI Catalog URL through a canonical redirect. - Enhanced website structured data with software application details, legal information, support contact details, and business address. - **Tests** - Added validation ensuring discoverable `.well-known` resources are cataloged and resolve correctly. - **Chores** - Updated marketing site test coverage for `.well-known` resource changes. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
1 parent
0c8dc73bf4
commit
3dddb60149
6 files changed
+191
-1
No files matched your search
@@ -10,6 +10,7 @@ on:
|
||||
- 'apps/www/lib/**/*.js'
|
||||
- 'apps/www/content/md/**'
|
||||
- 'apps/www/scripts/**/*.mjs'
|
||||
- 'apps/www/public/.well-known/**'
|
||||
|
||||
# Cancel old builds on new commit for same workflow + branch/PR
|
||||
concurrency:
|
||||
|
||||
@@ -2,7 +2,13 @@ import type { Metadata } from 'next'
|
||||
|
||||
import { FrameworksSection } from './_components/FrameworksSection'
|
||||
import { HomeContent } from './_components/HomeContent'
|
||||
import { organizationSchema, serializeJsonLd, websiteSchema } from '@/lib/json-ld'
|
||||
import { DEFAULT_META_DESCRIPTION } from '@/lib/constants'
|
||||
import {
|
||||
organizationSchema,
|
||||
serializeJsonLd,
|
||||
softwareApplicationSchema,
|
||||
websiteSchema,
|
||||
} from '@/lib/json-ld'
|
||||
import { mdAlternates } from '@/lib/md-alternates'
|
||||
|
||||
export const metadata: Metadata = {
|
||||
@@ -23,6 +29,19 @@ export default function HomePage() {
|
||||
type="application/ld+json"
|
||||
dangerouslySetInnerHTML={{ __html: serializeJsonLd(websiteSchema()) }}
|
||||
/>
|
||||
<script
|
||||
type="application/ld+json"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: serializeJsonLd(
|
||||
softwareApplicationSchema({
|
||||
name: 'Supabase',
|
||||
description: DEFAULT_META_DESCRIPTION,
|
||||
url: 'https://supabase.com',
|
||||
image: 'https://supabase.com/images/og/supabase-og.png',
|
||||
})
|
||||
),
|
||||
}}
|
||||
/>
|
||||
<HomeContent frameworksSlot={<FrameworksSection />} />
|
||||
</>
|
||||
)
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
import { existsSync, promises as fs } from 'node:fs'
|
||||
import path from 'node:path'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import rewrites from './lib/rewrites'
|
||||
|
||||
const CANONICAL_ORIGIN = 'https://supabase.com'
|
||||
|
||||
const NOT_AGENT_RESOURCES: Record<string, string> = {
|
||||
'ard.json': 'the catalog itself',
|
||||
'api-catalog':
|
||||
'peer catalog (RFC 9727); its resource, the Management API spec, has its own entry',
|
||||
'ai-catalog.json': 'legacy ARD alias, rewritten to ard.json',
|
||||
'mcp-registry-auth': 'domain-ownership verification token',
|
||||
'openai-apps-challenge': 'domain-ownership verification token',
|
||||
'security.txt': 'vulnerability disclosure contact',
|
||||
vercel: 'Vercel toolbar dev tooling',
|
||||
}
|
||||
|
||||
type ArdEntry = { identifier: string; url: string }
|
||||
|
||||
async function loadArdCatalog(): Promise<{ entries: ArdEntry[] }> {
|
||||
const raw = await fs.readFile(
|
||||
path.join(process.cwd(), 'public', '.well-known', 'ard.json'),
|
||||
'utf-8'
|
||||
)
|
||||
return JSON.parse(raw)
|
||||
}
|
||||
|
||||
function wellKnownRewriteSources(): string[] {
|
||||
return rewrites
|
||||
.map((rewrite: { source: string }) => rewrite.source)
|
||||
.filter((source: string) => source.startsWith('/.well-known/'))
|
||||
.map((source: string) => source.replace('/.well-known/', ''))
|
||||
}
|
||||
|
||||
describe('agent discovery catalog (.well-known/ard.json)', () => {
|
||||
it('every .well-known surface is cataloged or explicitly marked as not an agent resource', async () => {
|
||||
const { entries } = await loadArdCatalog()
|
||||
const catalogedUrls = entries.map((entry) => entry.url)
|
||||
|
||||
const wellKnownDir = path.join(process.cwd(), 'public', '.well-known')
|
||||
const dirents = await fs.readdir(wellKnownDir, { withFileTypes: true })
|
||||
const surfaces = [...dirents.map((dirent) => dirent.name), ...wellKnownRewriteSources()]
|
||||
|
||||
expect(surfaces.length).toBeGreaterThan(0)
|
||||
|
||||
for (const surface of surfaces) {
|
||||
if (surface in NOT_AGENT_RESOURCES) continue
|
||||
const cataloged = catalogedUrls.some((url) =>
|
||||
url.startsWith(`${CANONICAL_ORIGIN}/.well-known/${surface}`)
|
||||
)
|
||||
expect(
|
||||
cataloged,
|
||||
`new .well-known surface "${surface}": add an entry for it to public/.well-known/ard.json, or add it to NOT_AGENT_RESOURCES in this test with the reason it doesn't belong in the catalog`
|
||||
).toBe(true)
|
||||
}
|
||||
})
|
||||
|
||||
it('every same-origin catalog entry resolves to a public file, app route, or rewrite', async () => {
|
||||
const { entries } = await loadArdCatalog()
|
||||
expect(entries.length).toBeGreaterThan(0)
|
||||
|
||||
const rewriteSources = rewrites.map((rewrite: { source: string }) => rewrite.source)
|
||||
|
||||
for (const entry of entries) {
|
||||
const url = new URL(entry.url)
|
||||
if (url.origin !== CANONICAL_ORIGIN) continue
|
||||
|
||||
const publicFile = path.join(process.cwd(), 'public', url.pathname)
|
||||
const appRoute = path.join(process.cwd(), 'app', url.pathname, 'route.ts')
|
||||
const resolves =
|
||||
existsSync(publicFile) || existsSync(appRoute) || rewriteSources.includes(url.pathname)
|
||||
expect(
|
||||
resolves,
|
||||
`ard.json entry "${entry.identifier}" points at ${entry.url}, but ${url.pathname} is not a file in public/, an app route, or a rewrite source — the catalog is advertising a dead URL`
|
||||
).toBe(true)
|
||||
}
|
||||
})
|
||||
})
|
||||
@@ -41,6 +41,19 @@ export function organizationSchema(input: OrganizationSchemaInput = {}) {
|
||||
url: ORG_LOGO_URL,
|
||||
},
|
||||
description: input.description ?? DEFAULT_META_DESCRIPTION,
|
||||
legalName: 'Supabase Pte. Ltd.',
|
||||
contactPoint: {
|
||||
'@type': 'ContactPoint',
|
||||
contactType: 'customer support',
|
||||
url: `${CANONICAL_ORIGIN}/support`,
|
||||
},
|
||||
address: {
|
||||
'@type': 'PostalAddress',
|
||||
streetAddress: '65 Chulia Street #38-02/03, OCBC Centre',
|
||||
addressLocality: 'Singapore',
|
||||
postalCode: '049513',
|
||||
addressCountry: 'SG',
|
||||
},
|
||||
sameAs: ORG_SAMEAS,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -75,6 +75,11 @@ const rewrites = [
|
||||
source: '/.well-known/security.txt',
|
||||
destination: `${process.env.NEXT_PUBLIC_DOCS_URL}/.well-known/security.txt`,
|
||||
},
|
||||
{
|
||||
// legacy AI Catalog path; /.well-known/ard.json is the canonical source
|
||||
source: '/.well-known/ai-catalog.json',
|
||||
destination: '/.well-known/ard.json',
|
||||
},
|
||||
{
|
||||
source: '/openapi.json',
|
||||
destination: 'https://api.supabase.com/api/v1-json',
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
{
|
||||
"specVersion": "1.0",
|
||||
"host": {
|
||||
"displayName": "Supabase",
|
||||
"identifier": "https://supabase.com",
|
||||
"documentationUrl": "https://supabase.com/docs"
|
||||
},
|
||||
"entries": [
|
||||
{
|
||||
"identifier": "urn:air:supabase.com:mcp:server",
|
||||
"displayName": "Supabase MCP server",
|
||||
"type": "application/json",
|
||||
"url": "https://mcp.supabase.com/mcp",
|
||||
"description": "The Supabase MCP server endpoint (streamable HTTP, OAuth-protected) for managing projects, database schema, and queries from MCP clients.",
|
||||
"tags": ["mcp", "server", "agents"],
|
||||
"representativeQueries": [
|
||||
"connect my agent to Supabase over MCP",
|
||||
"run a query against my Supabase project from an MCP client",
|
||||
"manage my Supabase project from my coding agent"
|
||||
]
|
||||
},
|
||||
{
|
||||
"identifier": "urn:air:supabase.com:metadata:mcp-oauth",
|
||||
"displayName": "Supabase MCP OAuth metadata",
|
||||
"type": "application/json",
|
||||
"url": "https://api.supabase.com/.well-known/oauth-protected-resource/mcp",
|
||||
"description": "OAuth protected-resource metadata for connecting clients to the Supabase MCP server.",
|
||||
"tags": ["mcp", "oauth", "authorization", "metadata"],
|
||||
"representativeQueries": [
|
||||
"authenticate an MCP client to the Supabase MCP server with OAuth",
|
||||
"what OAuth scopes does the Supabase MCP server support"
|
||||
]
|
||||
},
|
||||
{
|
||||
"identifier": "urn:air:supabase.com:api:management",
|
||||
"displayName": "Supabase Management API",
|
||||
"type": "application/openapi+json",
|
||||
"url": "https://api.supabase.com/api/v1-json",
|
||||
"description": "OpenAPI 3.0 description of the Supabase Management API for managing organizations, projects, branches, and configuration.",
|
||||
"tags": ["api", "openapi", "projects", "management"],
|
||||
"representativeQueries": [
|
||||
"create a new Supabase project",
|
||||
"list the branches of my Supabase project",
|
||||
"update the Postgres config of a Supabase project"
|
||||
]
|
||||
},
|
||||
{
|
||||
"identifier": "urn:air:supabase.com:docs:llms-txt",
|
||||
"displayName": "Supabase llms.txt",
|
||||
"type": "text/plain",
|
||||
"url": "https://supabase.com/llms.txt",
|
||||
"description": "Index of Supabase documentation and content available as agent-readable markdown.",
|
||||
"tags": ["documentation", "llms", "markdown"],
|
||||
"representativeQueries": [
|
||||
"how do I set up Supabase auth in Next.js",
|
||||
"Supabase Row Level Security documentation"
|
||||
]
|
||||
},
|
||||
{
|
||||
"identifier": "urn:air:supabase.com:skills:index",
|
||||
"displayName": "Supabase Agent Skills",
|
||||
"type": "application/json",
|
||||
"url": "https://supabase.com/.well-known/agent-skills/index.json",
|
||||
"description": "Agent Skills discovery index for installable Supabase and Postgres skills.",
|
||||
"tags": ["skills", "agents", "postgres"],
|
||||
"representativeQueries": [
|
||||
"write RLS policies for my Supabase tables",
|
||||
"create a Postgres migration for my Supabase project"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
Reference in new issue
Block a user