feat: blog post postgrest 13 (#38840)

* add CodeTabs component to www
* add  to www mdx components
This commit is contained in:
Steve Chavez authored and GitHub committed 2025-09-30 14:37:07 -05:00
1 parent 360663d4f6
commit cb66b6b635
17 files changed
+960 -339

No files matched your search

@@ -0,0 +1,204 @@
---
title: PostgREST 13
description: New features and changes in PostgREST version 13.
author: steve_chavez,laurenceisla,avallete
image: 2025-09-30-postgrest-13-release/postgrest-13-release.png
thumb: 2025-09-30-postgrest-13-release/postgrest-13-release.png
categories:
- postgres
tags:
- postgres
- postgrest
- release-notes
date: '2025-09-30'
---
PostgREST 13 is out! It comes with API and Observabilty improvements. In this post, we'll see what's new.
## Spread To-Many relationships
This new feature allows you to represent one-to-many and many-to-many relationships as flat JSON arrays.
For example, if you have database similar to IMDB and you’d like to represent it as a hierarchical JSON structure for your frontend, like so:
```json
[
{
"title": "The Shawshank Redemption",
"actors": ["Tim Robbins", "Morgan Freeman"],
"genres": ["Drama"]
},
{
"title": "The Godfather",
"actors": ["Marlon Brando", "Al Pacino"],
"genres": ["Drama", "Crime"]
},
{
"title": "The Dark Knight",
"actors": ["Christian Bale", "Heath Ledger"],
"genres": ["Drama", "Crime", "Action"]
}
]
```
You can now do it this way:
<$CodeTabs>
```js name=JavaScript
const { data, error } = await supabase.from('titles').select(`
title:primary_title,
...people(actors:primary_name),
...genres(genres:name)
`)
```
```bash name=Bash
curl --get 'https://<your-domain>/titles'
-d "select=title:primary_title,...people(actors:primary_name),...genres(genres:name)"
-H "Authorization: Bearer <YOUR_KEY>"
```
```swift name=Swift
try await supabase.from("titles").select(
"""
title:primary_title,
...people(actors:primary_name),
...genres(genres:name)
""")
```
</$CodeTabs>
The above `...people` is “spreading” the many-to-many relationship between `titles` and `people`, forming a flat array only consisting of the `primary_name` column. This flat array is then renamed to `actors`. We do a similar process for `genres` , which also forms a many-to-many relationship with `people`.
You can see the data model used for this example on this [gist](https://gist.github.com/steve-chavez/93f7ae04b4323e1952710af7129b32cf). There are more details about this feature on the [official docs](https://docs.postgrest.org/en/v13/references/api/resource_embedding.html#spread-to-many-relationships).
## Automatic tsvector convertion
Previously you could only use the full text search operator on `tsvector` columns, now you can do it on `text` and `json/jsonb` columns too:
<$CodeTabs>
```js name=JavaScript
const { data, error } = await supabase.from('titles').textSearch('primary_name', `'god' & 'father'`)
```
```bash name=Bash
curl --get 'https://<your-domain>/titles'
-d "primary_name=fts.'god'%26'father"
-H "Authorization: Bearer <YOUR_KEY>"
```
```swift name=Swift
try await supabase
.from("titles")
.fts("primary_name",value: "'god' & 'father'")
```
</$CodeTabs>
This works because `text` and `json/jsonb` columns will be automatically converted with `to_tsvector`.
To ensure this operation is fast, add an index:
```sql
create index idx_titles on people
using gin (to_tsvector('english', primary_name));
```
## Max Affected
You can now limit to the amount of rows affected by an `update` or `delete` operation with `maxAffected`:
<$CodeTabs>
```js name=JavaScript
const { data, error } = await supabase
.from('people')
.update({ primary_name: 'Marlon Brando Jr.' })
.eq('nconst', 'nm0000008')
.maxAffected(1)
// This is available starting from supabase-js version 2.56.0
```
```bash name=Bash
curl -X PATCH 'https://<your-domain>/people' \
-d "nconst=eq.nm0000008" \
-H "Authorization: Bearer <YOUR_KEY>" \
-H "Prefer: handling=strict, max-affected=1" \
-H "Content-Type: application/json" \
-d @- <<JSON
{ primary_name: 'Marlon Brando Jr.' }
JSON
```
```swift name=Swift
try await supabase
.from("people")
.update(["primary_name": "Marlon Brando Jr."])
.eq("nconst", value: "nm0000008")
.maxAffected(1)
.execute()
// This is available starting from supabase-swift version 2.33.0
```
</$CodeTabs>
If the rows affected by the operation surpass the limit in `maxAffected`, an error will be thrown.
This also works with `rpc()`, given that it modifies rows and returns the affected rows. More on details on the [official docs](https://docs.postgrest.org/en/v13/references/api/preferences.html#max-affected).
## Content-Length header
For observability, you can now verify the response body size in bytes in the `Content-Length` header.
```bash
HTTP/1.1 200 OK
Content-Length: 104
Content-Location: /items
```
This helps in cases where you want to know which requests consume the most traffic to avoid exceeding egress limits.
## Proxy-Status header
The PostgREST error code is now present in the `Proxy-Status` header.
```bash
HTTP/1.1 406 Not Acceptable
Proxy-Status: PostgREST; error=PGRST116
```
You can check the `Proxy-Status` and `Content-Length` headers in the Supabase Logs Explorer.
## Breaking Changes
### JWT `kid` validation
PostgREST now validates the JWT `kid` claim. If your JWT contains a Key ID (`kid`), it will try to match this with one of the `kid`'s in the configured JSON Web Key Set. Check the [official docs](https://docs.postgrest.org/en/v13/references/auth.html#jwk-kid-validation) for more details.
If you use Supabase Auth or the CLI to create JSON Web Keys, you shouldn’t worry about this change as both systems will ensure `kid`'s are present in the JSON Web Key Set.
For users that integrate with other Auth systems, make sure that both your JWT and JWKS follow the above rules.
### Schema validation in PostgREST search path
The schemas inside `db-schemas` and `db-extra-search-path` are now validated. This means you cannot put a nonexistent schema there, if you do PostgREST will fail with an error message.
If you drop a schema during a migration, you should make sure this is synced with the PostgREST search path, which is possible thanks to postgres transactional DDL:
```sql
begin;
drop schema old_schema;
alter role authenticator set pgrst.db_schemas = 'public, pg_graphql, others'; -- make sure old_schema is not present here
commit;
```
## Try it out
PostgREST v13 is now available for all new projects on the Supabase platform, old projects can upgrade to get this new version.
You can look at the full changelog on the [release notes](https://github.com/PostgREST/postgrest/releases/tag/v13.0.0).
+3 -3
View File
@@ -39,7 +39,7 @@ async function getCMSPostFromAPI(
const response = await fetch(url.toString(), fetchOptions)
if (!response.ok) {
console.error('[getCMSPostFromAPI] Non-OK response:', response.status)
console.warn('[getCMSPostFromAPI] Non-OK response:', response.status)
return null
}
@@ -47,7 +47,7 @@ async function getCMSPostFromAPI(
return data.success ? data.post : null
} catch (error) {
console.error('[getCMSPostFromAPI] Error:', error)
console.warn('[getCMSPostFromAPI] Error:', error)
return null
}
}
@@ -295,7 +295,7 @@ export default async function BlogPostPage({ params }: { params: Promise<Params>
try {
processedContent = await processCMSContent(cmsPost.richContent || cmsPost.content, tocDepth)
} catch (error) {
console.error('Error processing CMS content, falling back to legacy processing:', error)
console.warn('Error processing CMS content, falling back to legacy processing:', error)
// Fallback to legacy processing
const mdxSource = await mdxSerialize(cmsPost.content || '', { tocDepth })
processedContent = {
+18 -5
View File
@@ -13,9 +13,10 @@ import sql from 'react-syntax-highlighter/dist/cjs/languages/hljs/sql'
import yaml from 'react-syntax-highlighter/dist/cjs/languages/hljs/yaml'
import json from 'react-syntax-highlighter/dist/cjs/languages/hljs/json'
import { Button, cn } from 'ui'
import monokaiCustomTheme from './CodeBlock.utils'
import monokaiCustomTheme, { codeHikeTheme } from './CodeBlock.utils'
export type LANG = 'js' | 'sql' | 'py' | 'bash' | 'ts' | 'tsx' | 'kotlin' | 'yaml' | 'json'
export interface CodeBlockProps {
lang: LANG
startingLineNumber?: number
@@ -25,11 +26,13 @@ export interface CodeBlockProps {
children?: string
size?: 'small' | 'medium' | 'large'
background?: string
filename?: string
theme?: 'monokai' | 'code-hike'
}
function CodeBlock(props: CodeBlockProps) {
const { resolvedTheme } = useTheme()
const isDarkTheme = resolvedTheme?.includes('dark')!
const isDarkTheme = resolvedTheme?.includes('dark') ?? false
const [copied, setCopied] = useState(false)
const [mounted, setMounted] = useState(false)
@@ -51,6 +54,8 @@ function CodeBlock(props: CodeBlockProps) {
}, 1000)
}
const isCodeHikeTheme = props.theme === 'code-hike'
let lang = props.lang
? props.lang
: props.className
@@ -111,7 +116,15 @@ function CodeBlock(props: CodeBlockProps) {
{/* @ts-ignore */}
<SyntaxHighlighter
language={lang}
style={isDarkTheme ? monokaiCustomTheme.dark : monokaiCustomTheme.light}
style={
isCodeHikeTheme
? isDarkTheme
? codeHikeTheme.dark
: codeHikeTheme.light
: isDarkTheme
? monokaiCustomTheme.dark
: monokaiCustomTheme.light
}
className={cn(
'synthax-highlighter border border-default/15 rounded-lg',
!filename && 'rounded-t-lg',
@@ -132,9 +145,9 @@ function CodeBlock(props: CodeBlockProps) {
showLineNumbers={props.showLineNumbers}
lineNumberStyle={{
padding: '0px',
marginRight: '21px',
marginRight: isCodeHikeTheme ? '16px' : '21px',
minWidth: '1.5em',
opacity: '0.3',
opacity: isCodeHikeTheme ? '0.7' : '0.3',
fontSize: large ? 14 : '0.75rem',
}}
>
@@ -229,4 +229,237 @@ const monokaiCustomTheme = {
},
}
// CodeHike theme based on the colors from code-hike.scss
const codeHikeTheme = {
dark: {
hljs: {
display: 'block',
overflowX: 'auto',
padding: '0.5em',
background: '#2a2929', // --ch-16 dark
color: '#f8f8f2', // --ch-4
},
'hljs-tag': {
color: '#79c0ff', // --ch-2
},
'hljs-keyword': {
color: '#79c0ff', // --ch-2
fontWeight: 'normal',
},
'hljs-selector-tag': {
color: '#79c0ff', // --ch-2
fontWeight: 'normal',
},
'hljs-literal': {
color: '#79c0ff', // --ch-2
fontWeight: 'normal',
},
'hljs-strong': {
color: '#79c0ff', // --ch-2
},
'hljs-name': {
color: '#79c0ff', // --ch-2
},
'hljs-code': {
color: '#569cd6', // --ch-7
},
'hljs-class .hljs-title': {
color: '#8b949e', // --ch-1
},
'hljs-attribute': {
color: '#bda4ff', // --ch-5
},
'hljs-symbol': {
color: '#bda4ff', // --ch-5
},
'hljs-regexp': {
color: '#bda4ff', // --ch-5
},
'hljs-link': {
color: '#bda4ff', // --ch-5
},
'hljs-string': {
color: '#ffcda1', // --ch-3
},
'hljs-bullet': {
color: '#ffcda1', // --ch-3
},
'hljs-subst': {
color: '#ffcda1', // --ch-3
},
'hljs-title': {
color: '#ffcda1', // --ch-3
fontWeight: 'normal',
},
'hljs-section': {
color: '#ffcda1', // --ch-3
fontWeight: 'normal',
},
'hljs-emphasis': {
color: '#ffcda1', // --ch-3
},
'hljs-type': {
color: '#ffcda1', // --ch-3
fontWeight: 'normal',
},
'hljs-built_in': {
color: '#ffcda1', // --ch-3
},
'hljs-builtin-name': {
color: '#ffcda1', // --ch-3
},
'hljs-selector-attr': {
color: '#ffcda1', // --ch-3
},
'hljs-selector-pseudo': {
color: '#ffcda1', // --ch-3
},
'hljs-addition': {
color: '#ffcda1', // --ch-3
},
'hljs-variable': {
color: '#ffcda1', // --ch-3
},
'hljs-template-tag': {
color: '#ffcda1', // --ch-3
},
'hljs-template-variable': {
color: '#ffcda1', // --ch-3
},
'hljs-comment': {
color: '#8b949e', // --ch-1
},
'hljs-quote': {
color: '#8b949e', // --ch-1
},
'hljs-deletion': {
color: '#8b949e', // --ch-1
},
'hljs-meta': {
color: '#8b949e', // --ch-1
},
'hljs-doctag': {
fontWeight: 'normal',
},
'hljs-selector-id': {
fontWeight: 'normal',
},
},
light: {
hljs: {
display: 'block',
overflowX: 'auto',
padding: '0.5em',
background: '#eeeeee', // --ch-16 light
color: '#24292f', // --ch-4
},
'hljs-tag': {
color: '#0550ae', // --ch-2
},
'hljs-keyword': {
color: '#0550ae', // --ch-2
fontWeight: 'normal',
},
'hljs-selector-tag': {
color: '#0550ae', // --ch-2
fontWeight: 'normal',
},
'hljs-literal': {
color: '#0550ae', // --ch-2
fontWeight: 'normal',
},
'hljs-strong': {
color: '#0550ae', // --ch-2
},
'hljs-name': {
color: '#0550ae', // --ch-2
},
'hljs-code': {
color: '#cf222e', // --ch-7
},
'hljs-class .hljs-title': {
color: '#6e7781', // --ch-1
},
'hljs-attribute': {
color: '#8250df', // --ch-5
},
'hljs-symbol': {
color: '#8250df', // --ch-5
},
'hljs-regexp': {
color: '#8250df', // --ch-5
},
'hljs-link': {
color: '#8250df', // --ch-5
},
'hljs-string': {
color: '#953800', // --ch-3
},
'hljs-bullet': {
color: '#953800', // --ch-3
},
'hljs-subst': {
color: '#953800', // --ch-3
},
'hljs-title': {
color: '#953800', // --ch-3
fontWeight: 'normal',
},
'hljs-section': {
color: '#953800', // --ch-3
fontWeight: 'normal',
},
'hljs-emphasis': {
color: '#953800', // --ch-3
},
'hljs-type': {
color: '#953800', // --ch-3
fontWeight: 'normal',
},
'hljs-built_in': {
color: '#953800', // --ch-3
},
'hljs-builtin-name': {
color: '#953800', // --ch-3
},
'hljs-selector-attr': {
color: '#953800', // --ch-3
},
'hljs-selector-pseudo': {
color: '#953800', // --ch-3
},
'hljs-addition': {
color: '#953800', // --ch-3
},
'hljs-variable': {
color: '#953800', // --ch-3
},
'hljs-template-tag': {
color: '#953800', // --ch-3
},
'hljs-template-variable': {
color: '#953800', // --ch-3
},
'hljs-comment': {
color: '#6e7781', // --ch-1
},
'hljs-quote': {
color: '#6e7781', // --ch-1
},
'hljs-deletion': {
color: '#6e7781', // --ch-1
},
'hljs-meta': {
color: '#6e7781', // --ch-1
},
'hljs-doctag': {
fontWeight: 'normal',
},
'hljs-selector-id': {
fontWeight: 'normal',
},
},
}
export { codeHikeTheme }
export default monokaiCustomTheme
@@ -0,0 +1,21 @@
import type { PropsWithChildren } from 'react'
import { cn } from 'ui'
export function NamedCodeBlock({ name, children }: PropsWithChildren<{ name: string }>) {
return (
<div className="shiki-wrapper w-full space-y-2">
<h6
className={cn(
'w-fit flex items-center text-center',
'shadow-sm rounded border border-stronger bg-selection',
'px-2.5 py-1',
'text-xs text-foreground'
)}
>
{name}
</h6>
{children}
</div>
)
}
+2
View File
@@ -0,0 +1,2 @@
export { NamedCodeBlock } from './CodeTabs.components'
export { preprocessMdxWithCodeTabs } from './utils'
@@ -0,0 +1,48 @@
/**
* Simple string-based transformation for CodeTabs
* This avoids the complex AST manipulation that's causing issues
*/
export function transformCodeTabs(mdx: string): string {
// Pattern to match <$CodeTabs>...</$CodeTabs> blocks
const codeTabsPattern = /<\$CodeTabs>([\s\S]*?)<\/\$CodeTabs>/g
return mdx.replace(codeTabsPattern, (match, content) => {
// Extract code blocks from the content
const codeBlockPattern = /```(\w+)(?:\s+name=([^\s]+))?\s*\n([\s\S]*?)\n```/g
const codeBlocks: Array<{ lang: string; name?: string; content: string }> = []
let blockMatch: RegExpExecArray | null
// biome-ignore lint/suspicious/noAssignInExpressions: required for regex loop
while ((blockMatch = codeBlockPattern.exec(content)) !== null) {
const [, lang, name, blockContent] = blockMatch
codeBlocks.push({
lang,
name: name || `${lang}.${lang === 'bash' ? 'sh' : lang}`,
content: blockContent.trim(),
})
}
if (codeBlocks.length === 0) {
return match // Return original if no code blocks found
}
// Generate the Tabs structure
const tabPanels = codeBlocks
.map(
(block) => `
<TabPanel id="${block.name}" label="${block.name}">
\`\`\`${block.lang}
${block.content}
\`\`\`
</TabPanel>`
)
.join('')
return `
<Tabs listClassNames="flex-nowrap overflow-x-auto -mb-6">${tabPanels}
</Tabs>`
})
}
+13
View File
@@ -0,0 +1,13 @@
import { transformCodeTabs } from './simple-transform'
export function preprocessMdxWithCodeTabs(mdx: string) {
try {
// Use simple string-based transformation
const transformed = transformCodeTabs(mdx)
return Promise.resolve(transformed)
} catch (error) {
console.error('preprocessMdxWithCodeTabs error:', error)
// Fallback to returning the original MDX
return Promise.resolve(mdx)
}
}
+77
View File
@@ -0,0 +1,77 @@
'use client'
import { useState, useEffect } from 'react'
import { cn, Tabs_Shadcn_, TabsList_Shadcn_, TabsTrigger_Shadcn_, TabsContent_Shadcn_ } from 'ui'
export interface TabsProps {
children: React.ReactNode
className?: string
defaultActiveId?: string
listClassNames?: string
}
export interface TabPanelProps {
children: React.ReactNode
id: string
label: string
className?: string
}
function Tabs({ children, className, defaultActiveId, listClassNames }: TabsProps) {
const [mounted, setMounted] = useState(false)
const [activeTab, setActiveTab] = useState(defaultActiveId)
useEffect(() => {
setMounted(true)
}, [])
if (!mounted) return null
// Extract tab panels from children
const tabPanels = Array.isArray(children) ? children : [children]
const validTabPanels = tabPanels.filter(
(child) => child && typeof child === 'object' && 'props' in child && child.props.id
)
if (validTabPanels.length === 0) return null
const currentActiveTab = activeTab || validTabPanels[0]?.props?.id
return (
<div className={cn('not-prose', className)}>
<Tabs_Shadcn_
value={currentActiveTab}
onValueChange={setActiveTab}
className="w-full flex flex-col gap-4 [&_.ch-codeblock]:!mt-0"
>
<TabsList_Shadcn_ className={cn('shiki-wrapper flex', listClassNames)}>
{validTabPanels.map((panel) => (
<TabsTrigger_Shadcn_
key={panel.props.id}
value={panel.props.id}
className="text-xs px-2.5"
>
{panel.props.label}
</TabsTrigger_Shadcn_>
))}
</TabsList_Shadcn_>
{validTabPanels.map((panel) => (
<TabsContent_Shadcn_ key={panel.props.id} value={panel.props.id} className="mt-0">
{panel.props.children}
</TabsContent_Shadcn_>
))}
</Tabs_Shadcn_>
</div>
)
}
function TabPanel({ children, id, label, className }: TabPanelProps) {
return (
<div className={cn('tab-panel', className)} data-id={id} data-label={label}>
{children}
</div>
)
}
export { Tabs, TabPanel }
export default Tabs
+2
View File
@@ -0,0 +1,2 @@
export { Tabs, TabPanel } from './Tabs'
export type { TabsProps, TabPanelProps } from './Tabs'
+8
View File
@@ -106,6 +106,14 @@
"author_url": "https://github.com/laurenceisla",
"author_image_url": "https://github.com/laurenceisla.png"
},
{
"author_id": "avallete",
"author": "Andrew Valleteau",
"username": "avallete",
"position": "Engineering",
"author_url": "https://github.com/avallete",
"author_image_url": "https://github.com/avallete.png"
},
{
"author_id": "supabase",
"author": "Supabase",
+19 -1
View File
@@ -1,6 +1,5 @@
'use client'
import { PropsWithChildren } from 'react'
import { CH } from '@code-hike/mdx/components'
import dynamic from 'next/dynamic'
import { ArrowUpRight, Triangle } from 'lucide-react'
@@ -14,11 +13,26 @@ import {
Image,
} from 'ui'
import { Admonition } from 'ui-patterns/admonition'
import type { PropsWithChildren } from 'react'
import type { ImageProps } from 'ui/src/components/Image/Image'
const Avatar = dynamic(() => import('~/components/Avatar'))
const Chart = dynamic(() => import('~/components/Charts/PGCharts'))
const CodeBlock = dynamic(() => import('~/components/CodeBlock/CodeBlock'))
const Tabs = dynamic(() => import('~/components/Tabs/Tabs'), { ssr: false })
const TabPanel = dynamic(
() => import('~/components/Tabs/Tabs').then((mod) => ({ default: mod.TabPanel })),
{ ssr: false }
)
const NamedCodeBlock = dynamic(
() =>
import('~/components/CodeTabs').then((mod) => ({
default: mod.NamedCodeBlock,
})),
{
ssr: false,
}
)
const ImageFadeStack = dynamic(() => import('~/components/ImageFadeStack'))
const ImageGrid = dynamic(() => import('~/components/ImageGrid'))
const InlineCodeTag = dynamic(() => import('~/components/InlineCode'))
@@ -73,6 +87,9 @@ const BlogCollapsible = ({
export default function mdxComponents(type?: 'blog' | 'lp' | undefined) {
const components = {
CodeBlock,
Tabs,
TabPanel,
NamedCodeBlock,
CH,
h1: (props: any) => <Heading {...props} tag="h1" />,
h2: (props: any) => <Heading {...props} tag="h2" />,
@@ -110,6 +127,7 @@ export default function mdxComponents(type?: 'blog' | 'lp' | undefined) {
/>
)
}
// biome-ignore lint/a11y/useAltText: provided in props
return <img {...props} />
},
Img: ({ zoomable = true, className, ...props }: ImageProps & { wide?: boolean }) => (
+11 -5
View File
@@ -2,8 +2,9 @@ import { serialize } from 'next-mdx-remote/serialize'
import rehypeSlug from 'rehype-slug'
import remarkGfm from 'remark-gfm'
import { CodeHikeConfig, remarkCodeHike } from '@code-hike/mdx'
import { type CodeHikeConfig, remarkCodeHike } from '@code-hike/mdx'
import codeHikeTheme from 'config/code-hike.theme.json' with { type: 'json' }
import { preprocessMdxWithCodeTabs } from '~/components/CodeTabs'
// mdx2 needs self-closing tags.
// dragging an image onto a GitHub discussion creates an <img>
@@ -64,13 +65,18 @@ function createRemarkCollectToc(maxDepth: number) {
walk(tree)
// Attach to tree data for retrieval post-serialize
;(tree.data || (tree.data = {})).__collectedToc = items
if (!tree.data) {
tree.data = {}
}
tree.data.__collectedToc = items
}
}
}
export async function mdxSerialize(source: string, options?: { tocDepth?: number }) {
const formattedSource = addSelfClosingTags(source)
// Preprocess MDX to handle CodeTabs transformation
const preprocessedSource = await preprocessMdxWithCodeTabs(formattedSource)
const codeHikeOptions: CodeHikeConfig = {
theme: codeHikeTheme,
lineNumbers: true,
@@ -82,7 +88,7 @@ export async function mdxSerialize(source: string, options?: { tocDepth?: number
const tocDepth = options?.tocDepth ?? 2
let collectedToc: TocItem[] = []
const mdxSource: any = await serialize(formattedSource, {
const mdxSource = await serialize(preprocessedSource, {
scope: {
chCodeConfig: codeHikeOptions,
},
@@ -91,10 +97,10 @@ export async function mdxSerialize(source: string, options?: { tocDepth?: number
[remarkCodeHike, codeHikeOptions],
remarkGfm,
// Collect headings into a simple TOC structure
function () {
() => {
const plugin = createRemarkCollectToc(tocDepth)
const transformer = (plugin as any)()
return function (tree: any) {
return (tree: any) => {
transformer(tree)
if (tree?.data?.__collectedToc) {
collectedToc = tree.data.__collectedToc as TocItem[]
+11 -1
View File
@@ -26,11 +26,11 @@
"@next/bundle-analyzer": "15.3.1",
"@next/mdx": "15.3.1",
"@octokit/auth-app": "^7.0.0",
"@payloadcms/live-preview-react": "^3.0.0",
"@octokit/core": "^6.0.0",
"@octokit/graphql": "^8.0.0",
"@octokit/plugin-paginate-graphql": "^4.0.0",
"@octokit/rest": "^21.0.0",
"@payloadcms/live-preview-react": "^3.0.0",
"@radix-ui/react-dialog": "^1.0.5",
"@sentry/nextjs": "^10",
"@supabase/supabase-js": "catalog:",
@@ -52,6 +52,13 @@
"icons": "workspace:*",
"lucide-react": "*",
"markdown-toc": "^1.2.0",
"mdast-util-from-markdown": "^2.0.2",
"mdast-util-gfm": "^2.0.2",
"mdast-util-mdx": "^2.0.1",
"mdast-util-mdx-jsx": "^2.1.4",
"mdast-util-to-markdown": "^1.5.0",
"micromark-extension-gfm": "^2.0.3",
"micromark-extension-mdxjs": "^1.0.1",
"next": "catalog:",
"next-mdx-remote": "^4.4.1",
"next-seo": "^6.5.0",
@@ -86,6 +93,7 @@
"@types/animejs": "^3.1.12",
"@types/classnames": "^2.3.1",
"@types/common-tags": "^1.8.4",
"@types/mdast": "^4.0.4",
"@types/mdx-js__react": "^1.5.6",
"@types/parse-numeric-range": "^0.0.1",
"@types/react": "catalog:",
@@ -97,7 +105,9 @@
"api-types": "workspace:*",
"postcss": "^8.5.3",
"react-hook-form": "^7.45.0",
"tailwindcss": "^3.4.1",
"tsconfig": "workspace:*",
"unist-util-visit-parents": "5.1.3",
"uuid": "^9.0.1",
"zod": "catalog:"
},
Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

+12
View File
@@ -2162,6 +2162,18 @@
<priority>0.5</priority>
</url>
<url>
<loc>https://supabase.com/blog/defense-in-depth-mcp</loc>
<changefreq>weekly</changefreq>
<priority>0.5</priority>
</url>
<url>
<loc>https://supabase.com/blog/postgrest-13-release</loc>
<changefreq>weekly</changefreq>
<priority>0.5</priority>
</url>
<url>
<loc>https://supabase.com/customers/asap-work</loc>
<changefreq>weekly</changefreq>
+278 -324
View File
File diff suppressed because it is too large. Load diff