try app router blog routes

This commit is contained in:
Francesco Sansalvadore committed 2025-08-18 13:24:32 +02:00
1 parent b70828b71e
commit e2cabecf5e
28 files changed
+660 -766

No files matched your search

+31
View File
@@ -0,0 +1,31 @@
'use client'
import { IS_PLATFORM, PageTelemetry } from 'common'
import { RefreshRouteOnSave as PayloadLivePreview } from '@payloadcms/live-preview-react'
import { useConsentToast } from 'ui-patterns/consent'
import { useRouter } from 'next/navigation'
import { API_URL } from '~/lib/constants'
interface ConsentWrapperProps {
children: React.ReactNode
}
export function ConsentWrapper({ children }: ConsentWrapperProps) {
const { hasAcceptedConsent } = useConsentToast()
const router = useRouter()
return (
<>
{children}
<PageTelemetry
API_URL={API_URL}
hasAcceptedConsent={hasAcceptedConsent}
enabled={IS_PLATFORM}
/>
<PayloadLivePreview
refresh={() => router.refresh()}
serverURL={process.env.NEXT_PUBLIC_CMS_URL || 'http://localhost:3030'}
/>
</>
)
}
@@ -1,12 +1,7 @@
import fs from 'fs'
import { useEffect, useState } from 'react'
import { useRouter } from 'next/router'
import { NextSeo } from 'next-seo'
import { generateRss } from '~/lib/rss'
import { getSortedPosts } from '~/lib/posts'
// CMS posts are fetched client-side from /api-v2/cms-posts to keep server bundle small
'use client'
import type PostTypes from '~/types/post'
import { useEffect, useState } from 'react'
import { NextSeo } from 'next-seo'
import DefaultLayout from '~/components/Layouts/Default'
import BlogGridItem from '~/components/Blog/BlogGridItem'
import BlogListItem from '~/components/Blog/BlogListItem'
@@ -15,33 +10,30 @@ import FeaturedThumb from '~/components/Blog/FeaturedThumb'
import { cn } from 'ui'
import { LOCAL_STORAGE_KEYS, isBrowser } from 'common'
import type PostTypes from '~/types/post'
export type BlogView = 'list' | 'grid'
function Blog(props: any) {
export default function BlogClient(props: { blogs: any[] }) {
const { BLOG_VIEW } = LOCAL_STORAGE_KEYS
const localView = isBrowser ? (localStorage?.getItem(BLOG_VIEW) as BlogView) : undefined
const [cmsPosts, setCmsPosts] = useState<any[]>([])
const [blogs, setBlogs] = useState(props.blogs)
const [view, setView] = useState<BlogView>(localView ?? 'list')
const isList = view === 'list'
const router = useRouter()
const meta_title = 'Supabase Blog: the Postgres development platform'
const meta_description = 'Get all your Supabase News on the Supabase blog.'
// Merge CMS posts with static ones when CMS posts arrive
const allPosts = [...props.blogs, ...cmsPosts].sort(
(a: any, b: any) => new Date(b.date).getTime() - new Date(a.date).getTime()
)
// Keep list in sync once after CMS posts load, but don't override user filters later
useEffect(() => {
setBlogs((prev: any[]) => (prev.length === props.blogs.length ? allPosts : prev))
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [cmsPosts])
// Fetch CMS posts
// eslint-disable-next-line react-hooks/rules-of-hooks
useEffect(() => {
const fetchCmsPosts = async () => {
try {
@@ -65,7 +57,7 @@ function Blog(props: any) {
openGraph={{
title: meta_title,
description: meta_description,
url: `https://supabase.com/${router.pathname}`,
url: `https://supabase.com/blog`,
images: [
{
url: `https://supabase.com/images/og/supabase-og.png`,
@@ -90,12 +82,7 @@ function Blog(props: any) {
<div className="border-default border-t">
<div className="md:container mx-auto mt-6 lg:mt-8 px-6 sm:px-16 xl:px-20">
<BlogFilters
allPosts={allPosts}
setPosts={setBlogs}
view={view as BlogView}
setView={setView}
/>
<BlogFilters allPosts={allPosts} setPosts={setBlogs} view={view} setView={setView} />
<ol
className={cn(
@@ -131,44 +118,3 @@ function Blog(props: any) {
</>
)
}
export async function getStaticProps() {
// Get static blog posts
const staticPostsData = getSortedPosts({ directory: '_blog', runner: '** BLOG PAGE **' })
// Only static data at build time; CMS posts are fetched client-side
const allPostsData = [...staticPostsData].sort((a: any, b: any) => {
const dateA = a.date ? new Date(a.date).getTime() : new Date(a.formattedDate).getTime()
const dateB = b.date ? new Date(b.date).getTime() : new Date(b.formattedDate).getTime()
return dateB - dateA
})
// Generate RSS feed from combined posts
const rss = generateRss(allPostsData)
// create a rss feed in public directory
// rss feed is added via <Head> component in render return
fs.writeFileSync('./public/rss.xml', rss)
// generate a series of rss feeds for each author (for PlanetPG)
const planetPgPosts = allPostsData.filter((post: any) => post.tags?.includes('planetpg'))
const planetPgAuthors = planetPgPosts.map((post: any) => post.author.split(','))
const uniquePlanetPgAuthors = new Set([].concat(...planetPgAuthors))
uniquePlanetPgAuthors.forEach((author) => {
const authorPosts = planetPgPosts.filter((post: any) => post.author.includes(author))
if (authorPosts.length > 0) {
const authorRss = generateRss(authorPosts, author)
fs.writeFileSync(`./public/planetpg-${author}-rss.xml`, authorRss)
}
})
return {
props: {
blogs: allPostsData,
},
// revalidate: 60 * 10, // Revalidate every 10 minutes
}
}
export default Blog
+135
View File
@@ -0,0 +1,135 @@
'use client'
import dynamic from 'next/dynamic'
import { NextSeo } from 'next-seo'
import { useState, useMemo } from 'react'
import { useLivePreview } from '@payloadcms/live-preview-react'
import authors from 'lib/authors.json'
import { CMS_SITE_ORIGIN } from 'lib/constants'
import { isNotNullOrUndefined } from 'lib/helpers'
import type { Blog, BlogData, CMSAuthor, PostReturnType, ProcessedBlogData, Tag } from 'types/post'
const BlogPostRenderer = dynamic(() => import('components/Blog/BlogPostRenderer'))
type BlogPostPageProps = {
prevPost: PostReturnType | null
nextPost: PostReturnType | null
relatedPosts: (PostReturnType & BlogData)[]
blog: Blog & BlogData
isDraftMode: boolean
}
export default function BlogPostClient(props: BlogPostPageProps) {
const isDraftMode = props.isDraftMode
const [previewData] = useState<ProcessedBlogData>(props.blog)
const shouldUseLivePreview = isDraftMode && (props.blog as any).isCMS
const { data: livePreviewData, isLoading: isLivePreviewLoading } = useLivePreview({
initialData: props.blog,
serverURL: CMS_SITE_ORIGIN || 'http://localhost:3030',
depth: 2,
})
const blogMetaData = useMemo(() => {
if (isDraftMode && shouldUseLivePreview) {
if (livePreviewData && typeof livePreviewData === 'object') {
return { ...props.blog, ...livePreviewData }
}
if (previewData !== props.blog) {
return previewData
}
}
return props.blog
}, [isDraftMode, shouldUseLivePreview, livePreviewData, previewData, props.blog]) as any
const isCMS = (blogMetaData as any).isCMS
const imageUrl = isCMS
? (blogMetaData as any).thumb ?? ''
: (blogMetaData as any).thumb
? `/images/blog/${(blogMetaData as any).thumb}`
: ''
const meta = {
title: (blogMetaData as any).meta_title ?? (blogMetaData as any).title,
description: (blogMetaData as any).meta_description ?? (blogMetaData as any).description,
url: `https://supabase.com/blog/${(blogMetaData as any).slug}`,
}
const processTag = (tag: any): string => {
return typeof tag === 'string' ? tag : tag.name
}
const tags = (blogMetaData as any).tags
? Array.isArray((blogMetaData as any).tags)
? ((blogMetaData as any).tags as Tag[]).map(processTag)
: []
: []
const blogAuthors = isCMS
? ((blogMetaData as any).authors as CMSAuthor[]) || []
: ((blogMetaData as any).author as string)
?.split(',')
.map((authorId: string) => {
const foundAuthor = authors.find((author) => author.author_id === authorId)
return foundAuthor
? {
author: foundAuthor.author || 'Author',
author_image_url: foundAuthor.author_image_url || null,
author_url: foundAuthor.author_url || '#',
position: foundAuthor.position || '',
}
: null
})
.filter(isNotNullOrUndefined) || []
const authorUrls = (blogAuthors as any)
.map((author: any) => author?.author_url)
.filter(isNotNullOrUndefined)
return (
<>
<NextSeo
title={meta.title}
description={meta.description}
openGraph={{
title: meta.title,
description: meta.description,
url: meta.url,
type: 'article',
videos: (blogMetaData as any).video
? [
{
url: (blogMetaData as any).video,
type: 'application/x-shockwave-flash',
width: 640,
height: 385,
},
]
: undefined,
article: {
publishedTime: (blogMetaData as any).date,
authors: authorUrls as any,
tags: tags as any,
},
images: [
{
url: imageUrl,
alt: `${(blogMetaData as any).title} thumbnail`,
},
],
}}
/>
<BlogPostRenderer
blog={props.blog as any}
blogMetaData={blogMetaData as any}
isDraftMode={isDraftMode}
isLivePreviewLoading={isLivePreviewLoading}
prevPost={props.prevPost as any}
nextPost={props.nextPost as any}
authors={blogAuthors as any}
/>
</>
)
}
+135
View File
@@ -0,0 +1,135 @@
import { draftMode } from 'next/headers'
import { getAllPostSlugs, getPostdata, getSortedPosts } from 'lib/posts'
import { getAllCMSPostSlugs, getCMSPostBySlug } from 'lib/get-cms-posts'
import type { Blog, BlogData, PostReturnType } from 'types/post'
import BlogPostClient from '~/app/blog/[slug]/BlogPostClient'
type MatterReturn = {
data: BlogData
content: string
}
type BlogPostPageProps = {
prevPost: PostReturnType | null
nextPost: PostReturnType | null
relatedPosts: (PostReturnType & BlogData)[]
blog: Blog & BlogData
isDraftMode: boolean
}
type Params = {
slug: string
}
export async function generateStaticParams() {
const staticPaths = getAllPostSlugs('_blog')
const cmsPaths = await getAllCMSPostSlugs()
return [...staticPaths, ...cmsPaths].map((p) => ({ slug: p.params.slug }))
}
export default async function BlogPostPage({ params }: { params: Params }) {
if (!params?.slug) {
throw new Error('Missing slug for app/blog/[slug]/page.tsx')
}
const slug = `${params.slug}`
const { isEnabled: isDraft } = await draftMode()
const matter = (await import('gray-matter')).default
const { mdxSerialize } = await import('lib/mdx/mdxSerialize')
try {
const postContent = await getPostdata(slug, '_blog')
const parsedContent = matter(postContent) as unknown as MatterReturn
const content = parsedContent.content
const tocDepth = (parsedContent.data as any)?.toc_depth ?? 2
const mdxSource = await mdxSerialize(content, { tocDepth })
const blogPost = { ...parsedContent.data }
const allStaticPosts = getSortedPosts({ directory: '_blog' })
const allPosts = [...allStaticPosts].sort((a, b) => {
const aDate = new Date((a as unknown as { date: string }).date).getTime()
const bDate = new Date((b as unknown as { date: string }).date).getTime()
return bDate - aDate
})
const currentIndex = allPosts.findIndex((post) => post.slug === slug)
const nextPost = currentIndex === allPosts.length - 1 ? null : allPosts[currentIndex + 1]
const prevPost = currentIndex === 0 ? null : allPosts[currentIndex - 1]
const tocResult = (mdxSource as any).scope?.toc || { content: '' }
const processedContent = tocResult.content
const relatedPosts = getSortedPosts({
directory: '_blog',
limit: 3,
tags: (mdxSource as { scope: { tags?: string[] } }).scope.tags,
currentPostSlug: slug,
}) as unknown as (BlogData & PostReturnType)[]
const props: BlogPostPageProps = {
prevPost,
nextPost,
relatedPosts,
blog: {
...(blogPost as any),
content: mdxSource,
toc: {
...tocResult,
content: processedContent,
},
} as any,
isDraftMode: isDraft,
}
return <BlogPostClient {...props} />
} catch {}
const cmsPost = await getCMSPostBySlug(slug, isDraft)
if (!cmsPost) {
if (isDraft) {
const publishedPost = await getCMSPostBySlug(slug, false)
if (!publishedPost) return null
const mdxSource = await mdxSerialize(publishedPost.content || '')
const props: BlogPostPageProps = {
prevPost: null,
nextPost: null,
relatedPosts: [],
blog: {
...publishedPost,
tags: publishedPost.tags || [],
authors: publishedPost.authors || [],
isCMS: true,
content: mdxSource,
toc: publishedPost.toc,
image: publishedPost.image ?? undefined,
thumb: publishedPost.thumb ?? undefined,
} as any,
isDraftMode: isDraft,
}
return <BlogPostClient {...props} />
}
return null
}
const mdxSource = await mdxSerialize(cmsPost.content || '')
const props: BlogPostPageProps = {
prevPost: null,
nextPost: null,
relatedPosts: [],
blog: {
...cmsPost,
tags: cmsPost.tags || [],
authors: cmsPost.authors || [],
isCMS: true,
content: mdxSource,
toc: cmsPost.toc,
image: cmsPost.image ?? undefined,
thumb: cmsPost.thumb ?? undefined,
} as any,
isDraftMode: isDraft,
}
return <BlogPostClient {...props} />
}
@@ -2,37 +2,24 @@ import { NextSeo } from 'next-seo'
import { getSortedPosts, getAllCategories } from '~/lib/posts'
import Link from 'next/link'
import { startCase } from 'lodash'
import DefaultLayout from '~/components/Layouts/Default'
import BlogGridItem from '~/components/Blog/BlogGridItem'
import type PostTypes from '~/types/post'
export async function getStaticProps({ params }: any) {
const posts = getSortedPosts({ directory: '_blog', limit: 0, categories: [params.category] })
return {
props: {
category: params.category,
blogs: posts,
},
}
}
type Params = { category: string }
export async function getStaticPaths() {
export async function generateStaticParams() {
const categories = getAllCategories('_blog')
return {
paths: categories.map((category: any) => ({ params: { category: category } })),
fallback: false,
}
return categories.map((category: string) => ({ category }))
}
interface Props {
category: string
blogs: PostTypes[]
}
function CategoriesIndex(props: Props) {
const { blogs, category } = props
const capitalizedCategory = startCase(category.replaceAll('-', ' '))
export default async function CategoriesPage({ params }: { params: Params }) {
const blogs = getSortedPosts({
directory: '_blog',
limit: 0,
categories: [params.category],
}) as unknown as PostTypes[]
const capitalizedCategory = startCase(params.category.replaceAll('-', ' '))
return (
<>
@@ -46,14 +33,14 @@ function CategoriesIndex(props: Props) {
<h1 className="cursor-pointer">
<Link href="/blog">Blog</Link>
<span className="px-2">/</span>
<span>{`${capitalizedCategory}`}</span>
<span>{capitalizedCategory}</span>
</h1>
</div>
<ol className="grid grid-cols-12 gap-8 py-16 lg:gap-16">
{blogs.map((blog: PostTypes, idx: number) => (
{blogs.map((blog: PostTypes) => (
<div
className="col-span-12 mb-16 md:col-span-12 lg:col-span-6 xl:col-span-4"
key={idx}
key={blog.slug}
>
<BlogGridItem post={blog} />
</div>
@@ -64,5 +51,3 @@ function CategoriesIndex(props: Props) {
</>
)
}
export default CategoriesIndex
+43
View File
@@ -0,0 +1,43 @@
import fs from 'fs'
import { generateRss } from '~/lib/rss'
import { getSortedPosts } from '~/lib/posts'
import BlogClient from '~/app/blog/BlogClient'
export const dynamic = 'force-static'
export default async function BlogPage() {
// Get static blog posts at build time
const staticPostsData = getSortedPosts({ directory: '_blog', runner: '** BLOG PAGE **' })
const allPostsData = [...staticPostsData].sort((a: any, b: any) => {
const dateA = (a as any).date
? new Date((a as any).date).getTime()
: new Date((a as any).formattedDate).getTime()
const dateB = (b as any).date
? new Date((b as any).date).getTime()
: new Date((b as any).formattedDate).getTime()
return dateB - dateA
})
// Generate RSS feed from static posts
const rss = generateRss(allPostsData)
try {
fs.writeFileSync('./public/rss.xml', rss)
} catch {}
// Generate PlanetPG author feeds
try {
const planetPgPosts = allPostsData.filter((post: any) => post.tags?.includes('planetpg'))
const planetPgAuthors = planetPgPosts.map((post: any) => post.author.split(','))
const uniquePlanetPgAuthors = new Set(([] as string[]).concat(...(planetPgAuthors as any)))
uniquePlanetPgAuthors.forEach((author) => {
const authorPosts = planetPgPosts.filter((post: any) => post.author.includes(author))
if (authorPosts.length > 0) {
const authorRss = generateRss(authorPosts, author as string)
fs.writeFileSync(`./public/planetpg-${author}-rss.xml`, authorRss)
}
})
} catch {}
return <BlogClient blogs={allPostsData as any} />
}
@@ -2,37 +2,24 @@ import { NextSeo } from 'next-seo'
import { getSortedPosts, getAllTags } from '~/lib/posts'
import Link from 'next/link'
import { startCase } from 'lodash'
import DefaultLayout from '~/components/Layouts/Default'
import BlogGridItem from '~/components/Blog/BlogGridItem'
import PostTypes from '~/types/post'
import type PostTypes from '~/types/post'
export async function getStaticProps({ params }: any) {
const posts = getSortedPosts({ directory: '_blog', limit: 0, tags: [params.tag] })
return {
props: {
tag: params.tag,
blogs: posts,
},
}
}
type Params = { tag: string }
export async function getStaticPaths() {
export async function generateStaticParams() {
const tags = getAllTags('_blog')
return {
paths: tags.map((tag: any) => ({ params: { tag: tag } })),
fallback: false,
}
return tags.map((tag: string) => ({ tag }))
}
interface Props {
tag: string
blogs: PostTypes[]
}
function TagBlogsPage(props: Props) {
const { blogs, tag } = props
const capitalizedTag = startCase(tag.replaceAll('-', ' '))
export default async function TagPage({ params }: { params: Params }) {
const blogs = getSortedPosts({
directory: '_blog',
limit: 0,
tags: [params.tag],
}) as unknown as PostTypes[]
const capitalizedTag = startCase(params.tag.replaceAll('-', ' '))
return (
<>
@@ -46,14 +33,14 @@ function TagBlogsPage(props: Props) {
<h1 className="cursor-pointer">
<Link href="/blog">Blog</Link>
<span className="px-2">/</span>
<span>{`${capitalizedTag}`}</span>
<span>{capitalizedTag}</span>
</h1>
</div>
<ol className="grid grid-cols-12 gap-8 py-16 lg:gap-16">
{blogs.map((blog: PostTypes, idx: number) => (
{blogs.map((blog: PostTypes) => (
<div
className="col-span-12 mb-16 md:col-span-12 lg:col-span-6 xl:col-span-4"
key={idx}
key={blog.slug}
>
<BlogGridItem post={blog} />
</div>
@@ -64,5 +51,3 @@ function TagBlogsPage(props: Props) {
</>
)
}
export default TagBlogsPage
+48
View File
@@ -0,0 +1,48 @@
import '@code-hike/mdx/styles'
import 'config/code-hike.scss'
import '../styles/index.css'
import { Metadata } from 'next'
import { APP_NAME, DEFAULT_META_DESCRIPTION } from '~/lib/constants'
import Providers from './providers'
const site_title = `${APP_NAME} | The Open Source Firebase Alternative`
export const metadata: Metadata = {
title: site_title,
description: DEFAULT_META_DESCRIPTION,
openGraph: {
type: 'website',
url: 'https://supabase.com/',
siteName: 'Supabase',
images: [
{
url: 'https://supabase.com/images/og/supabase-og.png',
width: 800,
height: 600,
alt: 'Supabase Og Image',
},
],
},
twitter: {
creator: '@supabase',
site: '@supabase',
card: 'summary_large_image',
},
viewport: 'width=device-width, initial-scale=1.0',
icons: {
icon: '/favicon/favicon.ico',
shortcut: '/favicon/favicon.ico',
apple: '/favicon/favicon.ico',
},
}
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body>
<Providers>{children}</Providers>
</body>
</html>
)
}
+45
View File
@@ -0,0 +1,45 @@
'use client'
import {
AuthProvider,
FeatureFlagProvider,
IS_PLATFORM,
PageTelemetry,
ThemeProvider,
useThemeSandbox,
TelemetryTagManager,
} from 'common'
import { themes, TooltipProvider, SonnerToaster } from 'ui'
import { CommandProvider } from 'ui-patterns/CommandMenu'
import { useConsentToast } from 'ui-patterns/consent'
import { API_URL } from '~/lib/constants'
import { WwwCommandMenu } from '~/components/CommandMenu'
function Providers({ children }: { children: React.ReactNode }) {
useThemeSandbox()
const { hasAcceptedConsent } = useConsentToast()
return (
<AuthProvider>
<FeatureFlagProvider API_URL={API_URL} enabled={IS_PLATFORM}>
<ThemeProvider themes={themes.map((t) => t.value)} enableSystem disableTransitionOnChange>
<TooltipProvider delayDuration={0}>
<CommandProvider>
<TelemetryTagManager />
<SonnerToaster position="top-right" />
{children}
<WwwCommandMenu />
<PageTelemetry
API_URL={API_URL}
hasAcceptedConsent={hasAcceptedConsent}
enabled={IS_PLATFORM}
/>
</CommandProvider>
</TooltipProvider>
</ThemeProvider>
</FeatureFlagProvider>
</AuthProvider>
)
}
export default Providers
+11 -11
View File
@@ -2,10 +2,10 @@ import { LOCAL_STORAGE_KEYS, useBreakpoint } from 'common'
import { AnimatePresence, motion } from 'framer-motion'
import { startCase } from 'lodash'
import { useSearchParams } from 'next/navigation'
import { useRouter } from 'next/router'
import { useRouter } from 'next/compat/router'
import { useEffect, useState } from 'react'
import { useKey } from 'react-use'
import type { BlogView } from '~/pages/blog'
import type { BlogView } from '~/app/blog/BlogClient'
import type PostTypes from '~/types/post'
import { AlignJustify, ChevronDown, Grid, Search, X as CloseIcon } from 'lucide-react'
@@ -74,7 +74,7 @@ function BlogFilters({ allPosts, setPosts, view, setView }: Props) {
}, [q])
const handleReplaceRouter = () => {
if (!searchTerm && category !== 'all') {
if (!searchTerm && category !== 'all' && router) {
router.query.category = category
router.replace(router, undefined, { shallow: true, scroll: false })
}
@@ -105,19 +105,19 @@ function BlogFilters({ allPosts, setPosts, view, setView }: Props) {
}, [isMobile])
useEffect(() => {
if (router.isReady && q) {
if (router?.isReady && q) {
setSearchTerm(q)
}
if (router.isReady && activeCategory && activeCategory !== 'all') {
if (router?.isReady && activeCategory && activeCategory !== 'all') {
setCategory(activeCategory)
}
}, [activeCategory, router.isReady, q])
}, [activeCategory, router?.isReady, q])
const handleSearchByText = (text: string) => {
setSearchTerm(text)
searchParams?.has('q') && router.replace('/blog', undefined, { shallow: true, scroll: false })
router.replace(`/blog?q=${text}`, undefined, { shallow: true, scroll: false })
if (text.length < 1) router.replace('/blog', undefined, { shallow: true, scroll: false })
searchParams?.has('q') && router?.replace('/blog', undefined, { shallow: true, scroll: false })
router?.replace(`/blog?q=${text}`, undefined, { shallow: true, scroll: false })
if (text.length < 1) router?.replace('/blog', undefined, { shallow: true, scroll: false })
const matches = allPosts.filter((post: any) => {
const found =
@@ -135,8 +135,8 @@ function BlogFilters({ allPosts, setPosts, view, setView }: Props) {
searchTerm && setSearchTerm('')
setCategory(category)
category === 'all'
? router.replace('/blog', undefined, { shallow: true, scroll: false })
: router.replace(`/blog?category=${category}`, undefined, {
? router?.replace('/blog', undefined, { shallow: true, scroll: false })
: router?.replace(`/blog?category=${category}`, undefined, {
shallow: true,
scroll: false,
})
+4 -5
View File
@@ -2,7 +2,6 @@ import { CheckIcon } from '@heroicons/react/outline'
import { REALTIME_CHANNEL_STATES } from '@supabase/supabase-js'
import Image from 'next/image'
import Link from 'next/link'
import { useRouter } from 'next/router'
import { useEffect } from 'react'
import * as supabaseLogoWordmarkDark from 'common/assets/images/supabase-logo-wordmark--dark.png'
@@ -13,6 +12,7 @@ import { ThemeToggle } from 'ui-patterns/ThemeToggle'
import supabase from '~/lib/supabase'
import useDarkLaunchWeeks from '../../hooks/useDarkLaunchWeeks'
import SectionContainer from '../Layouts/SectionContainer'
import { usePathname } from 'next/navigation'
interface Props {
className?: string
@@ -20,17 +20,17 @@ interface Props {
}
const Footer = (props: Props) => {
const { pathname } = useRouter()
const pathname = usePathname()
const isDarkLaunchWeek = useDarkLaunchWeeks()
const isGAWeek = pathname.includes('/ga-week')
const isGAWeek = pathname?.includes('/ga-week')
const forceDark = isDarkLaunchWeek
useEffect(() => {
const channel = supabase.channel('footer')
if (channel.state === REALTIME_CHANNEL_STATES.closed) {
channel.subscribe((status: string) => {
if (status == 'SUBSCRIBED') {
if (status === 'SUBSCRIBED') {
channel.send({
type: 'broadcast',
event: 'footer_subscribed',
@@ -56,7 +56,6 @@ const Footer = (props: Props) => {
isGAWeek && 'dark:bg-alternative',
props.className
)}
aria-labelledby="footerHeading"
>
<h2 id="footerHeading" className="sr-only">
Footer
@@ -1,7 +1,7 @@
import { CheckIcon, ClipboardIcon } from '@heroicons/react/outline'
import Image from 'next/image'
import Link from 'next/link'
import { useRouter } from 'next/router'
import { useRouter } from 'next/compat/router'
import { Fragment, MouseEvent, ReactNode, useRef, useState } from 'react'
import { useClickAway, useKey } from 'react-use'
import {
@@ -84,7 +84,7 @@ const RightClickBrandLogo = () => {
href="/"
onContextMenu={handleRightClick}
onFocus={handleKeyboardOpen}
onKeyDown={(e) => e.key === 'Enter' && router.push('/')}
onKeyDown={(e) => e.key === 'Enter' && router?.push('/')}
className="block w-auto h-6 focus-visible:ring-2 focus-visible:outline-none focus-visible:ring-foreground-lighter focus-visible:ring-offset-4 focus-visible:ring-offset-background-alternative focus-visible:rounded-sm"
>
<Image
+5 -5
View File
@@ -1,5 +1,4 @@
import Link from 'next/link'
import { useRouter } from 'next/router'
import React, { useState } from 'react'
import { useWindowSize } from 'react-use'
@@ -25,6 +24,7 @@ import { useSendTelemetryEvent } from 'lib/telemetry'
import useDropdownMenu from './useDropdownMenu'
import { getMenu } from 'data/nav'
import { usePathname } from 'next/navigation'
interface Props {
hideNavbar: boolean
@@ -32,7 +32,7 @@ interface Props {
}
const Nav = ({ hideNavbar, stickyNavbar = true }: Props) => {
const router = useRouter()
const pathname = usePathname()
const { width } = useWindowSize()
const [open, setOpen] = useState(false)
const isLoggedIn = useIsLoggedIn()
@@ -41,9 +41,9 @@ const Nav = ({ hideNavbar, stickyNavbar = true }: Props) => {
const user = useUser()
const userMenu = useDropdownMenu(user)
const isLaunchWeekPage = router.pathname.includes('/launch-week')
const isLaunchWeekXPage = router.pathname === '/launch-week/x'
const isGAWeekSection = router.pathname.startsWith('/ga-week')
const isLaunchWeekPage = pathname?.includes('/launch-week')
const isLaunchWeekXPage = pathname === '/launch-week/x'
const isGAWeekSection = pathname?.startsWith('/ga-week')
const disableStickyNav = isLaunchWeekXPage || isGAWeekSection || isLaunchWeekPage || !stickyNavbar
const showLaunchWeekNavMode = !isLaunchWeekXPage && (isLaunchWeekPage || isGAWeekSection) && !open
+2 -2
View File
@@ -6,7 +6,7 @@ import { logOut } from 'common'
import type { menuItem } from 'ui-patterns/AuthenticatedDropdownMenu'
import { useSetCommandMenuOpen } from 'ui-patterns'
import { useRouter } from 'next/router'
import { useRouter } from 'next/compat/router'
const useDropdownMenu = (user: User | null) => {
const router = useRouter()
@@ -50,7 +50,7 @@ const useDropdownMenu = (user: User | null) => {
icon: LogOut,
onClick: async () => {
await logOut()
router.reload()
router?.reload()
},
},
],
+3 -3
View File
@@ -1,11 +1,11 @@
import { useRouter } from 'next/router'
import { usePathname } from 'next/navigation'
import React, { useEffect, useState } from 'react'
const ScrollProgress = () => {
const [progressPercentage, setProgressPercentage] = useState(0)
const { pathname } = useRouter()
const pathname = usePathname()
const isBlogPost = pathname.includes('/blog/')
const isBlogPost = pathname?.includes('/blog/')
if (!isBlogPost) return null
const handleScroll = () => {
+5 -5
View File
@@ -1,11 +1,11 @@
import { useRouter } from 'next/router'
import { usePathname } from 'next/navigation'
const useDarkLaunchWeeks = () => {
const { pathname } = useRouter()
const pathname = usePathname()
const isLaunchWeek7 = pathname.startsWith('/launch-week/7')
const isLaunchWeek8 = pathname.startsWith('/launch-week/8')
const isLaunchWeekX = pathname.startsWith('/launch-week/x')
const isLaunchWeek7 = pathname?.startsWith('/launch-week/7')
const isLaunchWeek8 = pathname?.startsWith('/launch-week/8')
const isLaunchWeekX = pathname?.startsWith('/launch-week/x')
return isLaunchWeek7 || isLaunchWeek8 || isLaunchWeekX
}
+6 -5
View File
@@ -1,6 +1,6 @@
import { CMS_SITE_ORIGIN } from './constants'
import { generateReadingTime } from './helpers'
const toc = require('markdown-toc')
import { generateTocFromMarkdown } from './toc'
// Payload API configuration
const PAYLOAD_URL = CMS_SITE_ORIGIN || 'http://localhost:3030'
@@ -280,7 +280,7 @@ export async function getCMSCustomerBySlug(slug: string, preview = false) {
}
// Helper function to process customer data
function processCustomerData(customer: any) {
async function processCustomerData(customer: any) {
const options: Intl.DateTimeFormatOptions = { month: 'long', day: 'numeric', year: 'numeric' }
const formattedDate = new Date(customer.date || new Date()).toLocaleDateString('en-IN', options)
const markdownContent = convertRichTextToMarkdown(customer.content)
@@ -292,9 +292,10 @@ function processCustomerData(customer: any) {
const logoUrl = customer.logo?.url ? `${PAYLOAD_URL}${customer.logo.url}` : null
// Generate TOC from content for CMS customers
const tocResult = toc(markdownContent, {
maxdepth: customer.toc_depth ? customer.toc_depth : 2,
})
const tocResult = await generateTocFromMarkdown(
markdownContent,
customer.toc_depth ? customer.toc_depth : 2
)
return {
slug: customer.slug,
+6 -5
View File
@@ -1,6 +1,6 @@
import { CMS_SITE_ORIGIN } from './constants'
import { generateReadingTime } from './helpers'
const toc = require('markdown-toc')
import { generateTocFromMarkdown } from './toc'
// Payload API configuration
const PAYLOAD_URL = CMS_SITE_ORIGIN || 'http://localhost:3030'
@@ -274,7 +274,7 @@ export async function getCMSEventBySlug(slug: string, preview = false) {
}
// Helper function to process event data
function processEventData(event: any) {
async function processEventData(event: any) {
const options: Intl.DateTimeFormatOptions = { month: 'long', day: 'numeric', year: 'numeric' }
const formattedDate = new Date(event.date || new Date()).toLocaleDateString('en-IN', options)
const markdownContent = convertRichTextToMarkdown(event.content)
@@ -285,9 +285,10 @@ function processEventData(event: any) {
const imageUrl = event.image?.url ? `${PAYLOAD_URL}${event.image.url}` : null
// Generate TOC from content for CMS events
const tocResult = toc(markdownContent, {
maxdepth: event.toc_depth ? event.toc_depth : 2,
})
const tocResult = await generateTocFromMarkdown(
markdownContent,
event.toc_depth ? event.toc_depth : 2
)
return {
slug: event.slug,
+8 -7
View File
@@ -1,6 +1,6 @@
import { CMS_SITE_ORIGIN } from './constants'
import { generateReadingTime } from './helpers'
const toc = require('markdown-toc')
import { generateTocFromMarkdown } from './toc'
// Payload API configuration
const PAYLOAD_URL = CMS_SITE_ORIGIN || 'http://localhost:3030'
@@ -287,7 +287,7 @@ export async function getCMSPostBySlug(slug: string, preview = false) {
// console.log(`[getCMSPostBySlug] About to call processPostData...`)
try {
const processedPost = processPostData(post)
const processedPost = await processPostData(post)
// console.log(`[getCMSPostBySlug] Successfully processed post:`, {
// slug: processedPost.slug,
// title: processedPost.title,
@@ -305,7 +305,7 @@ export async function getCMSPostBySlug(slug: string, preview = false) {
}
// Helper function to process post data
function processPostData(post: any) {
async function processPostData(post: any) {
// console.log(`[processPostData] Starting to process post:`, {
// id: post.id,
// title: post.title,
@@ -324,9 +324,10 @@ function processPostData(post: any) {
const readingTime = post.readingTime || generateReadingTime(markdownContent)
// Generate TOC from content for CMS blog posts
const tocResult = toc(markdownContent, {
maxdepth: post.toc_depth ? post.toc_depth : 2,
})
const tocResult = await generateTocFromMarkdown(
markdownContent,
post.toc_depth ? post.toc_depth : 2
)
// Extract thumb and image URLs from the nested structure
const thumbUrl = post.thumb?.url ? `${PAYLOAD_URL}${post.thumb.url}` : null
@@ -364,7 +365,7 @@ function processPostData(post: any) {
richContent: post.content,
tags: post.tags || [],
toc: {
content: tocResult.content.replace(/%23/g, ''),
content: tocResult.content,
json: tocResult.json,
},
}
+75 -2
View File
@@ -21,7 +21,50 @@ function addSelfClosingTags(htmlString: string): string {
return modifiedHTML
}
export async function mdxSerialize(source: string) {
type TocItem = { content: string; slug: string; lvl: number }
function createRemarkCollectToc(maxDepth: number) {
return function remarkCollectToc() {
return function transformer(tree: any) {
const items: TocItem[] = []
const getText = (node: any): string => {
if (!node) return ''
if (node.type === 'text') return node.value || ''
if (Array.isArray(node.children)) return node.children.map(getText).join('')
return ''
}
const slugify = (input: string) =>
input
.trim()
.toLowerCase()
.replace(/[`~!@#$%^&*()+=|{}\[\]\\:";'<>?,./]+/g, '')
.replace(/\s+/g, '-')
const walk = (node: any) => {
if (node.type === 'heading' && typeof node.depth === 'number') {
if (node.depth <= maxDepth) {
const text = getText(node)
if (text) {
items.push({ content: text, slug: slugify(text), lvl: node.depth })
}
}
}
if (node.children) {
for (const child of node.children) walk(child)
}
}
walk(tree)
// Attach to tree data for retrieval post-serialize
;(tree.data || (tree.data = {})).__collectedToc = items
}
}
}
export async function mdxSerialize(source: string, options?: { tocDepth?: number }) {
const formattedSource = addSelfClosingTags(source)
const codeHikeOptions: CodeHikeConfig = {
theme: codeHikeTheme,
@@ -31,12 +74,29 @@ export async function mdxSerialize(source: string) {
autoImport: false,
}
const tocDepth = options?.tocDepth ?? 2
let collectedToc: TocItem[] = []
const mdxSource: any = await serialize(formattedSource, {
scope: {
chCodeConfig: codeHikeOptions,
},
mdxOptions: {
remarkPlugins: [[remarkCodeHike, codeHikeOptions], remarkGfm],
remarkPlugins: [
[remarkCodeHike, codeHikeOptions],
remarkGfm,
// Collect headings into a simple TOC structure
function () {
const plugin = createRemarkCollectToc(tocDepth)
const transformer = (plugin as any)()
return function (tree: any) {
transformer(tree)
if (tree?.data?.__collectedToc) {
collectedToc = tree.data.__collectedToc as TocItem[]
}
}
},
],
rehypePlugins: [
// @ts-ignore
rehypeSlug, // add IDs to any h1-h6 tag that doesn't have one, using a slug made from its text
@@ -44,5 +104,18 @@ export async function mdxSerialize(source: string) {
},
})
// Expose TOC via scope for consumers (keeps function signature stable)
const tocMarkdown = collectedToc
.map((h) => `${' '.repeat(Math.max(0, h.lvl - 1))}- [${h.content}](#${h.slug})`)
.join('\n')
mdxSource.scope = {
...(mdxSource.scope || {}),
toc: {
content: tocMarkdown,
json: collectedToc,
},
}
return mdxSource
}
+13 -10
View File
@@ -1,16 +1,19 @@
'use client'
import { sendTelemetryEvent } from 'common'
import { TelemetryEvent } from 'common/telemetry-constants'
import type { TelemetryEvent } from 'common/telemetry-constants'
import { API_URL } from 'lib/constants'
import { useRouter } from 'next/router'
import { useCallback } from 'react'
import { usePathname, useSearchParams } from 'next/navigation'
export function useSendTelemetryEvent() {
const router = useRouter()
const pathname = usePathname()
const searchParams = useSearchParams()
return useCallback(
(event: TelemetryEvent) => {
return sendTelemetryEvent(API_URL, event, router.pathname)
},
[router.pathname]
)
return (event: TelemetryEvent) => {
const url = new URL(API_URL ?? 'http://localhost:3000')
url.pathname = pathname ?? ''
url.search = searchParams?.toString() ?? ''
return sendTelemetryEvent(API_URL, event, url.toString())
}
}
+1 -1
View File
@@ -1,6 +1,6 @@
import { useEffect } from 'react'
import { useTheme } from 'next-themes'
import { useRouter } from 'next/router'
import { useRouter } from 'next/compat/router'
import useDarkLaunchWeeks from '../hooks/useDarkLaunchWeeks'
export function useForceDeepDark() {
+40
View File
@@ -0,0 +1,40 @@
type TocItem = { content: string; slug: string; lvl: number }
function stripFencedCodeBlocks(markdown: string): string {
const segments = markdown.split('```')
let acc = ''
for (let i = 0; i < segments.length; i++) {
if (i % 2 === 0) acc += segments[i]
}
return acc
}
function slugify(input: string): string {
return input
.trim()
.toLowerCase()
.replace(/[`~!@#$%^&*()+=|{}\[\]\\:\";'<>?,./]+/g, '')
.replace(/\s+/g, '-')
}
export async function generateTocFromMarkdown(markdown: string, maxDepth: number) {
const noCode = stripFencedCodeBlocks(markdown)
const lines = noCode.split(/\r?\n/)
const items: TocItem[] = []
for (const line of lines) {
const m = /^(#{1,6})\s+(.*)$/.exec(line)
if (!m) continue
const depth = m[1].length
if (depth > maxDepth) continue
const text = m[2].trim()
if (!text) continue
items.push({ content: text, slug: slugify(text), lvl: depth })
}
const content = items
.map((h) => `${' '.repeat(Math.max(0, h.lvl - 1))}- [${h.content}](#${h.slug})`)
.join('\n')
return { content, json: items }
}
+1 -1
View File
@@ -50,7 +50,6 @@
"icons": "workspace:*",
"lodash": "^4.0.0",
"lucide-react": "*",
"markdown-toc": "^1.2.0",
"next": "catalog:",
"next-contentlayer2": "0.5.3",
"next-mdx-remote": "^4.4.1",
@@ -69,6 +68,7 @@
"recharts": "^2.8.0",
"rehype-slug": "^5.1.0",
"remark": "^15.0.1",
"remark-parse": "^11.0.0",
"remark-gfm": "^3.0.1",
"remark-html": "^16.0.1",
"shared-data": "workspace:*",
+1 -1
View File
@@ -9,7 +9,7 @@ import { getAllPostSlugs, getPostdata, getSortedPosts } from '~/lib/posts'
// for instance, if you use a button, you must add `Button` in the components object below.
// table of contents extractor
const toc = require('markdown-toc')
const toc = require('markdown-toc-unlazy')
export async function getStaticPaths() {
const paths = getAllPostSlugs('_alternatives')
-298
View File
@@ -1,298 +0,0 @@
import dynamic from 'next/dynamic'
import { NextSeo } from 'next-seo'
import { useState, useMemo } from 'react'
import { useLivePreview } from '@payloadcms/live-preview-react'
import authors from 'lib/authors.json'
import { getAllPostSlugs, getPostdata, getSortedPosts } from 'lib/posts'
import { getAllCMSPostSlugs, getCMSPostBySlug } from 'lib/get-cms-posts'
import { CMS_SITE_ORIGIN } from 'lib/constants'
import { isNotNullOrUndefined } from 'lib/helpers'
import type { GetStaticProps, InferGetStaticPropsType } from 'next'
import type { Blog, BlogData, CMSAuthor, PostReturnType, ProcessedBlogData, Tag } from 'types/post'
const BlogPostRenderer = dynamic(() => import('components/Blog/BlogPostRenderer'))
type MatterReturn = {
data: BlogData
content: string
}
type BlogPostPageProps = {
prevPost: PostReturnType | null
nextPost: PostReturnType | null
relatedPosts: (PostReturnType & BlogData)[]
blog: Blog & BlogData
isDraftMode: boolean
}
type Params = {
slug: string
}
export async function getStaticPaths() {
const staticPaths = getAllPostSlugs('_blog')
const cmsPaths = await getAllCMSPostSlugs()
const paths = [...staticPaths, ...cmsPaths]
return {
paths,
fallback: 'blocking', // Set to 'blocking' to allow ISR for new CMS posts
}
}
function BlogPostPage(props: InferGetStaticPropsType<typeof getStaticProps>) {
const isDraftMode = props.isDraftMode
const [previewData] = useState<ProcessedBlogData>(props.blog)
const shouldUseLivePreview = isDraftMode && props.blog.isCMS
const { data: livePreviewData, isLoading: isLivePreviewLoading } = useLivePreview({
initialData: props.blog,
serverURL: CMS_SITE_ORIGIN || 'http://localhost:3030',
depth: 2,
})
// Only use the live preview data for metadata
const blogMetaData = useMemo(() => {
if (isDraftMode && shouldUseLivePreview) {
// Priority 1: Use data from LivePreview hook
if (livePreviewData && typeof livePreviewData === 'object') {
return { ...props.blog, ...livePreviewData }
}
// Priority 2: Use data from postMessage updates
if (previewData !== props.blog) {
return previewData
}
}
// Fallback to blog
return props.blog
}, [isDraftMode, shouldUseLivePreview, livePreviewData, previewData, props.blog])
const isCMS = blogMetaData.isCMS
const imageUrl = isCMS
? blogMetaData.thumb ?? ''
: blogMetaData.thumb
? `/images/blog/${blogMetaData.thumb}`
: ''
const meta = {
title: blogMetaData.meta_title ?? blogMetaData.title,
description: blogMetaData.meta_description ?? blogMetaData.description,
url: `https://supabase.com/blog/${blogMetaData.slug}`,
}
const processTag = (tag: Tag): string => {
return typeof tag === 'string' ? tag : tag.name
}
const tags = blogMetaData.tags
? Array.isArray(blogMetaData.tags)
? (blogMetaData.tags as Tag[]).map(processTag)
: []
: []
const blogAuthors = isCMS
? (blogMetaData.authors as CMSAuthor[]) || []
: (blogMetaData.author as string)
?.split(',')
.map((authorId: string) => {
const foundAuthor = authors.find((author) => author.author_id === authorId)
return foundAuthor
? {
author: foundAuthor.author || 'Author',
author_image_url: foundAuthor.author_image_url || null,
author_url: foundAuthor.author_url || '#',
position: foundAuthor.position || '',
}
: null
})
.filter(isNotNullOrUndefined) || []
const authorUrls = blogAuthors.map((author) => author?.author_url).filter(isNotNullOrUndefined)
return (
<>
<NextSeo
title={meta.title}
description={meta.description}
openGraph={{
title: meta.title,
description: meta.description,
url: meta.url,
type: 'article',
videos: blogMetaData.video
? [
{
// youtube based video meta
url: blogMetaData.video,
type: 'application/x-shockwave-flash',
width: 640,
height: 385,
},
]
: undefined,
article: {
publishedTime: blogMetaData.date,
authors: authorUrls,
tags: tags,
},
images: [
{
url: imageUrl,
alt: `${blogMetaData.title} thumbnail`,
},
],
}}
/>
<BlogPostRenderer
blog={props.blog}
blogMetaData={blogMetaData}
isDraftMode={isDraftMode}
isLivePreviewLoading={isLivePreviewLoading}
prevPost={props.prevPost}
nextPost={props.nextPost}
authors={blogAuthors}
/>
</>
)
}
export const getStaticProps: GetStaticProps<BlogPostPageProps, Params> = async ({
params,
draftMode = false,
}) => {
if (!params?.slug) {
console.error('[getStaticProps] Missing slug parameter:', params)
throw new Error('Missing slug for pages/blog/[slug].tsx')
}
const slug = `${params.slug}`
// Server-only imports to keep client bundle lean
const matter = (await import('gray-matter')).default
const { mdxSerialize } = await import('lib/mdx/mdxSerialize')
const markdownToc = require('markdown-toc')
// Try static post first
try {
const postContent = await getPostdata(slug, '_blog')
const parsedContent = matter(postContent) as unknown as MatterReturn
const content = parsedContent.content
const mdxSource = await mdxSerialize(content)
const blogPost = { ...parsedContent.data }
// Get all posts for navigation and related posts
const allStaticPosts = getSortedPosts({ directory: '_blog' })
const allPosts = [...allStaticPosts].sort((a, b) => {
const aDate = new Date((a as unknown as { date: string }).date).getTime()
const bDate = new Date((b as unknown as { date: string }).date).getTime()
return bDate - aDate
})
const currentIndex = allPosts.findIndex((post) => post.slug === slug)
const nextPost = currentIndex === allPosts.length - 1 ? null : allPosts[currentIndex + 1]
const prevPost = currentIndex === 0 ? null : allPosts[currentIndex - 1]
const tocResult = markdownToc(content, {
maxdepth: blogPost.toc_depth ? blogPost.toc_depth : 2,
})
const processedContent = tocResult.content.replace(/%23/g, '')
const relatedPosts = getSortedPosts({
directory: '_blog',
limit: 3,
tags: (mdxSource as { scope: { tags?: string[] } }).scope.tags,
currentPostSlug: slug,
}) as unknown as (BlogData & PostReturnType)[]
return {
props: {
prevPost,
nextPost,
relatedPosts,
blog: {
...blogPost,
content: mdxSource,
toc: {
...tocResult,
content: processedContent,
},
},
isDraftMode: draftMode,
},
// Don't use revalidate in draft mode
// ...(draftMode ? {} : { revalidate: 60 * 10 }),
}
} catch {
console.log('[getStaticProps] Static post not found, trying CMS post...')
}
// Try CMS post (handle draft mode logic)
const cmsPost = await getCMSPostBySlug(slug, draftMode)
if (!cmsPost) {
console.log(
'[getStaticProps] No CMS post found, checking published version (if in draft mode)...'
)
// Try to fetch published version if draft mode failed
if (draftMode) {
const publishedPost = await getCMSPostBySlug(slug, false)
if (!publishedPost) {
console.log('[getStaticProps] No published version found either, returning 404')
return { notFound: true }
}
const mdxSource = await mdxSerialize(publishedPost.content || '')
return {
props: {
prevPost: null,
nextPost: null,
relatedPosts: [],
blog: {
...publishedPost,
tags: publishedPost.tags || [],
authors: publishedPost.authors || [],
isCMS: true,
content: mdxSource,
toc: publishedPost.toc,
image: publishedPost.image ?? undefined,
thumb: publishedPost.thumb ?? undefined,
},
isDraftMode: draftMode,
},
// Don't use revalidate in draft mode
// ...(draftMode ? {} : { revalidate: 60 * 10 }),
}
}
return { notFound: true }
}
// For CMS posts, process content
const mdxSource = await mdxSerialize(cmsPost.content || '')
return {
props: {
prevPost: null,
nextPost: null,
relatedPosts: [],
blog: {
...cmsPost,
tags: cmsPost.tags || [],
authors: cmsPost.authors || [],
isCMS: true,
content: mdxSource,
toc: cmsPost.toc,
image: cmsPost.image ?? undefined,
thumb: cmsPost.thumb ?? undefined,
},
isDraftMode: draftMode,
},
// Don't use revalidate in draft mode
// ...(draftMode ? {} : { revalidate: 60 * 10 }),
}
}
export default BlogPostPage
+1 -1
View File
@@ -14,7 +14,7 @@ import { mdxSerialize } from '~/lib/mdx/mdxSerialize'
import { getAllPostSlugs, getPostdata, getSortedPosts } from '~/lib/posts'
// table of contents extractor
const toc = require('markdown-toc')
const toc = require('markdown-toc-unlazy')
export async function getStaticPaths() {
const paths = getAllPostSlugs('_customers')
+4 -283
View File
@@ -1607,9 +1607,6 @@ importers:
lucide-react:
specifier: '*'
version: 0.436.0(react@18.3.1)
markdown-toc:
specifier: ^1.2.0
version: 1.2.0
next:
specifier: 'catalog:'
version: 15.3.3(@babel/core@7.26.10(supports-color@8.1.1))(@opentelemetry/api@1.9.0)(@playwright/test@1.53.0)(babel-plugin-macros@3.1.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.77.4)
@@ -1670,6 +1667,9 @@ importers:
remark-html:
specifier: ^16.0.1
version: 16.0.1
remark-parse:
specifier: ^11.0.0
version: 11.0.0(supports-color@8.1.1)
shared-data:
specifier: workspace:*
version: link:../../packages/shared-data
@@ -9108,10 +9108,6 @@ packages:
resolution: {integrity: sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==}
engines: {node: '>=8'}
ansi-red@0.1.1:
resolution: {integrity: sha512-ewaIr5y+9CUTGFwZfpECUbFlGcC0GCw1oqR9RI6h1gQCd9Aj2GxSckCnPsVJnmfMZbwFYE+leZGASgkWl06Jow==}
engines: {node: '>=0.10.0'}
ansi-regex@5.0.1:
resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==}
engines: {node: '>=8'}
@@ -9136,10 +9132,6 @@ packages:
resolution: {integrity: sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==}
engines: {node: '>=12'}
ansi-wrap@0.1.0:
resolution: {integrity: sha512-ZyznvL8k/FZeQHr2T6LzcJ/+vBApDnMNZvfVFy3At0knswWd6rJ3/0Hhmpu8oqa6C92npmozs890sX9Dl6q+Qw==}
engines: {node: '>=0.10.0'}
ansis@3.17.0:
resolution: {integrity: sha512-0qWUglt9JEqLFr3w1I1pbrChn1grhaiAR2ocX1PP/flRmxgtwTzPFFFnfIlD6aMOLQZgSuCRlidD70lvx8yhzg==}
engines: {node: '>=14'}
@@ -9297,9 +9289,6 @@ packages:
resolution: {integrity: sha512-Hdw8qdNiqdJ8LqT0iK0sVzkFbzg6fhnQqqfWhBDxcHZvU75+B+ayzTy8x+k5Ix0Y92XOhOUlx74ps+bA6BeYMQ==}
engines: {node: '>=8'}
autolinker@0.28.1:
resolution: {integrity: sha512-zQAFO1Dlsn69eXaO6+7YZc+v84aquQKbwpzCE3L0stj56ERn9hutFxPopViLjo9G+rWwjozRhgS5KJ25Xy19cQ==}
autoprefixer@10.4.16:
resolution: {integrity: sha512-7vd3UC6xKp0HLfua5IjZlcXvGAGy7cBAXTg2lyQ/8WpNhd6SiZ8Be+xm3FyBSYJx5GKcpRCzBh7RH4/0dnY+uQ==}
engines: {node: ^10 || ^12 || >=14}
@@ -9769,12 +9758,6 @@ packages:
codemirror@5.65.15:
resolution: {integrity: sha512-YC4EHbbwQeubZzxLl5G4nlbLc1T21QTrKGaOal/Pkm9dVDMZXMH7+ieSPEOZCtO9I68i8/oteJKOxzHC2zR+0g==}
coffee-script@1.12.7:
resolution: {integrity: sha512-fLeEhqwymYat/MpTPUjSKHVYYl0ec2mOyALEMLmzr5i1isuG+6jfI2j2d5oBO3VIzgUXgBVIcOT9uH1TFxBckw==}
engines: {node: '>=0.8.0'}
deprecated: CoffeeScript on NPM has moved to "coffeescript" (no hyphen)
hasBin: true
collapse-white-space@2.1.0:
resolution: {integrity: sha512-loKTxY1zCOuG4j9f6EPnuyyYkf58RnhhWTvRoZEokgB+WbdXehfjFviyOVYkqzEWz1Q5kRiZdBYS5SwxbQYwzw==}
@@ -9864,17 +9847,10 @@ packages:
concat-map@0.0.1:
resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==}
concat-stream@1.6.2:
resolution: {integrity: sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==}
engines: {'0': node >= 0.8}
concat-stream@2.0.0:
resolution: {integrity: sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==}
engines: {'0': node >= 6.0}
concat-with-sourcemaps@1.1.0:
resolution: {integrity: sha512-4gEjHJFT9e+2W/77h/DS5SGUgwDaOwprX8L/gl5+3ixnzkVJJsZWDSelmN3Oilw3LNDZjZV0yqH1hLG3k6nghg==}
concurrently@9.1.2:
resolution: {integrity: sha512-H9MWcoPsYddwbOGM6difjVwVZHl63nwMEwDJG/L7VGtuaJhb12h2caPG2tVPWs7emuYix252iGfqOyrz1GczTQ==}
engines: {node: '>=18'}
@@ -10443,10 +10419,6 @@ packages:
devlop@1.1.0:
resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==}
diacritics-map@0.1.0:
resolution: {integrity: sha512-3omnDTYrGigU0i4cJjvaKwD52B8aoqyX/NEIkukFFkogBemsIbhSa1O414fpTp5nuszJG6lvQ5vBvDVNCbSsaQ==}
engines: {node: '>=0.8.0'}
didyoumean@1.2.2:
resolution: {integrity: sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==}
@@ -10994,6 +10966,7 @@ packages:
resolution: {integrity: sha512-t0q23FIpvHDTtnORW+bDJziGsal5uh9RJTJ1fyH8drd4lICOoXhJ5pLMUZ5C0VQei6dNmwTzzoTRgMkO9JgHEQ==}
peerDependencies:
eslint: '>= 5'
bundledDependencies: []
eslint-plugin-import@2.31.0:
resolution: {integrity: sha512-ixmkI62Rbc2/w8Vfxyh1jQRTdRTF52VxwRVHl/ykPAmqG+Nb7/kNn+byLP0LxPgI7zWA16Jt82SybJInmMia3A==}
@@ -11174,10 +11147,6 @@ packages:
resolution: {integrity: sha512-eNTPlAD67BmP31LDINZ3U7HSF8l57TxOY2PmBJ1shpCvpnxBF93mWCE8YHBnXs8qiUZJc9WDcWIeC3a2HIAMfw==}
engines: {node: '>=6'}
expand-range@1.8.2:
resolution: {integrity: sha512-AFASGfIlnIbkKPQwX1yHaDjFvh/1gyKJODme52V6IORh69uEYgZp0o9C+qsIGNVEiuuhQU0CSSl++Rlegg1qvA==}
engines: {node: '>=0.10.0'}
expand-tilde@2.0.2:
resolution: {integrity: sha512-A5EmesHW6rfnZ9ysHQjPdJRni0SRar0tjtG5MNtm9n5TUvsYU8oozprtRD4AqHxcZWWlVuAmQo2nWKfN9oyjTw==}
engines: {node: '>=0.10.0'}
@@ -11384,10 +11353,6 @@ packages:
filelist@1.0.4:
resolution: {integrity: sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==}
fill-range@2.2.4:
resolution: {integrity: sha512-cnrcCbj01+j2gTG921VZPnHbjmdAf8oQV/iGeV2kZxGSyfYjjTyY79ErsK1WJWMpw6DaApEX72binqJE+/d+5Q==}
engines: {node: '>=0.10.0'}
fill-range@7.1.1:
resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==}
engines: {node: '>=8'}
@@ -11460,10 +11425,6 @@ packages:
resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==}
engines: {node: '>= 0.4'}
for-in@1.0.2:
resolution: {integrity: sha512-7EwmXrOjyL+ChxMhmG5lnW9MPt1aIeZEwKhQzoBUdTV0N3zuwWDZYVJatDvZ2OyzPUvdIAZDsCetk3coyMfcnQ==}
engines: {node: '>=0.10.0'}
foreach@2.0.6:
resolution: {integrity: sha512-k6GAGDyqLe9JaebCsFCoudPPWfihKu8pylYXRlqP1J7ms39iPoTtk2fviNglIeQEwdh0bQeKJ01ZPyuyQvKzwg==}
@@ -11860,18 +11821,10 @@ packages:
resolution: {integrity: sha512-mS1lbMsxgQj6hge1XZ6p7GPhbrtFwUFYi3wRzXAC/FmYnyXMTvvI3td3rjmQ2u8ewXueaSvRPWaEcgVVOT9Jnw==}
engines: {node: ^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0}
gray-matter@2.1.1:
resolution: {integrity: sha512-vbmvP1Fe/fxuT2QuLVcqb2BfK7upGhhbLIt9/owWEvPYrZZEkelLcq2HqzxosV+PQ67dUFLaAeNpH7C4hhICAA==}
engines: {node: '>=0.10.0'}
gray-matter@4.0.3:
resolution: {integrity: sha512-5v6yZd4JK3eMI3FqqCouswVqwugaA9r4dNZB1wwcmrD02QkV5H0y7XBQW8QwQqEaZY1pM9aqORSORhJRdNK44Q==}
engines: {node: '>=6.0'}
gulp-header@1.8.12:
resolution: {integrity: sha512-lh9HLdb53sC7XIZOYzTXM4lFuXElv3EVkSDhsd7DoJBj7hm+Ni7D3qYbb+Rr8DuM8nRanBvkVO9d7askreXGnQ==}
deprecated: Removed event-stream from gulp-header
gzip-size@6.0.0:
resolution: {integrity: sha512-ax7ZYomf6jqPTQ4+XCpUGyXKHk5WweS+e05MBO4/y3WJ5RkmPXNKvX+bx1behVILVwr6JSQvZAku021CHPXG3Q==}
engines: {node: '>=10'}
@@ -12406,10 +12359,6 @@ packages:
resolution: {integrity: sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==}
engines: {node: '>=0.10.0'}
is-extendable@1.0.1:
resolution: {integrity: sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==}
engines: {node: '>=0.10.0'}
is-extglob@2.1.1:
resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==}
engines: {node: '>=0.10.0'}
@@ -12483,14 +12432,6 @@ packages:
resolution: {integrity: sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==}
engines: {node: '>= 0.4'}
is-number@2.1.0:
resolution: {integrity: sha512-QUzH43Gfb9+5yckcrSA0VBDwEtDUchrk4F6tfJZQuNzDJbEDB9cZNzSfXGQ1jqmdDY/kl41lUOWM9syA8z8jlg==}
engines: {node: '>=0.10.0'}
is-number@4.0.0:
resolution: {integrity: sha512-rSklcAIlf1OmFdyAqbnWTLVelsQ58uvZ66S/ZyawjWqIviTWCjg2PzVGw8WUA+nNuPTqb4wgA+NszrJ+08LlgQ==}
engines: {node: '>=0.10.0'}
is-number@7.0.0:
resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==}
engines: {node: '>=0.12.0'}
@@ -12672,10 +12613,6 @@ packages:
resolution: {integrity: sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==}
engines: {node: '>=16'}
isobject@2.1.0:
resolution: {integrity: sha512-+OUdGJlgjOBZDfxnDjYYG6zp487z0JGNQq3cYQYg5f5hKR+syHMsaztzGeml/4kGG55CSpKSpWTY+jYGgsHLgA==}
engines: {node: '>=0.10.0'}
isobject@3.0.1:
resolution: {integrity: sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==}
engines: {node: '>=0.10.0'}
@@ -12967,10 +12904,6 @@ packages:
resolution: {integrity: sha512-MbjN408fEndfiQXbFQ1vnd+1NoLDsnQW41410oQBXiyXDMYH5z505juWa4KUE1LqxRC7DgOgZDbKLxHIwm27hA==}
engines: {node: '>=0.10'}
lazy-cache@2.0.2:
resolution: {integrity: sha512-7vp2Acd2+Kz4XkzxGxaB1FWOi8KjWIWsgdfD5MCb86DWvlLqhRPM+d6Pro3iNEL5VT9mstz5hKAlcd+QR6H3aA==}
engines: {node: '>=0.10.0'}
lazystream@1.0.1:
resolution: {integrity: sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==}
engines: {node: '>= 0.6.3'}
@@ -13016,10 +12949,6 @@ packages:
linkify-it@5.0.0:
resolution: {integrity: sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==}
list-item@1.1.1:
resolution: {integrity: sha512-S3D0WZ4J6hyM8o5SNKWaMYB1ALSacPZ2nHGEuCjmHZ+dc03gFeNZoNDcqfcnO4vDhTZmNrqrpYZCdXsRh22bzw==}
engines: {node: '>=0.10.0'}
listhen@1.9.0:
resolution: {integrity: sha512-I8oW2+QL5KJo8zXNWX046M134WchxsXC7SawLPvRQpogCbkyQIaFxPE89A2HiwR7vAK2Dm2ERBAmyjTYGYEpBg==}
hasBin: true
@@ -13072,9 +13001,6 @@ packages:
lodash._createset@4.0.3:
resolution: {integrity: sha512-GTkC6YMprrJZCYU3zcqZj+jkXkrXzq3IPBcF/fIPpNEAB4hZEtXU8zp/RwKOvZl43NUmwDbyRk3+ZTbeRdEBXA==}
lodash._reinterpolate@3.0.0:
resolution: {integrity: sha512-xYHt68QRoYGjeeM/XOE1uJtvXQAgvszfBhjV4yvsQH0u2i9I6cI6c6/eG4Hh3UAOVn0y/xAXwmTzEay49Q//HA==}
lodash._root@3.0.1:
resolution: {integrity: sha512-O0pWuFSK6x4EXhM1dhZ8gchNtG7JMqBtrHdoUFUWXD7dJnNSUze1GuyQr5sOs0aCvgGeI3o/OJW8f4ca7FDxmQ==}
@@ -13102,13 +13028,6 @@ packages:
lodash.sortby@4.7.0:
resolution: {integrity: sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==}
lodash.template@4.5.0:
resolution: {integrity: sha512-84vYFxIkmidUiFxidA/KjjH9pAycqW+h980j7Fuz5qxRtO9pgB7MDFTdys1N7A5mcucRiDyEq4fusljItR1T/A==}
deprecated: This package is deprecated. Use https://socket.dev/npm/package/eta instead.
lodash.templatesettings@4.2.0:
resolution: {integrity: sha512-stgLz+i3Aa9mZgnjr/O+v9ruKZsPsndy7qPZOchbqk2cnTU1ZaldKK+v7m54WoKIyxiuMZTKT2H81F8BeAc3ZQ==}
lodash.throttle@4.1.1:
resolution: {integrity: sha512-wIkUCfVKpVsWo3JSZlc+8MB5it+2AN5W8J7YVMST30UrvcQNZ1Okbj+rbVniijTWE6FGYy4XJq/rHkas8qJMLQ==}
@@ -13246,18 +13165,9 @@ packages:
resolution: {integrity: sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==}
hasBin: true
markdown-link@0.1.1:
resolution: {integrity: sha512-TurLymbyLyo+kAUUAV9ggR9EPcDjP/ctlv9QAFiqUH7c+t6FlsbivPo9OKTU8xdOx9oNd2drW/Fi5RRElQbUqA==}
engines: {node: '>=0.10.0'}
markdown-table@3.0.3:
resolution: {integrity: sha512-Z1NL3Tb1M9wH4XESsCDEksWoKTdlUafKc4pt0GRwjUyXaCFZ+dc3g2erqB6zm3szA2IUSi7VnPI+o/9jnxh9hw==}
markdown-toc@1.2.0:
resolution: {integrity: sha512-eOsq7EGd3asV0oBfmyqngeEIhrbkc7XVP63OwcJBIhH2EpG2PzFcbZdhy1jutXSlRBBVMNXHvMtSr5LAxSUvUg==}
engines: {node: '>=0.10.0'}
hasBin: true
markdown-wasm@1.2.0:
resolution: {integrity: sha512-S12OTkyXCkOgI1n1rZY9cg4bK/PGu80Emjpvwp8BEjwCxhPV3yddF0U6+QhCitdBsI1tzWcoeahmW7k0Pq81OA==}
@@ -13270,9 +13180,6 @@ packages:
resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==}
engines: {node: '>= 0.4'}
math-random@1.0.4:
resolution: {integrity: sha512-rUxjysqif/BZQH2yhd5Aaq7vXMSx9NdEsQcyA07uEzIvxgI7zIr33gGsh+RU0/XjmQpCW7RsVof1vlkvQVCK5A==}
md5@2.3.0:
resolution: {integrity: sha512-T1GITYmFaKuO91vxyoQMFETst+O71VUPEU3ze5GNzDm0OWdP8v1ziTaAEPUr/3kLsY3Sftgz242A1SetQiDL7g==}
@@ -13803,10 +13710,6 @@ packages:
mitt@3.0.1:
resolution: {integrity: sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==}
mixin-deep@1.3.2:
resolution: {integrity: sha512-WRoDn//mXBiJ1H40rqa3vH0toePwSsGb45iInWlTySa+Uu4k3tYUSxa2v1KqAiLtvlrSzaExqS1gtk96A9zvEA==}
engines: {node: '>=0.10.0'}
mkdirp@1.0.4:
resolution: {integrity: sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==}
engines: {node: '>=10'}
@@ -14288,10 +14191,6 @@ packages:
resolution: {integrity: sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ==}
engines: {node: '>= 0.4'}
object.pick@1.3.0:
resolution: {integrity: sha512-tqa/UMy/CCoYmj+H5qc07qvSL9dqcs/WZENZ1JbtWBlATP+iVOe778gE6MSijnyCnORzDuX6hU+LA4SZ09YjFQ==}
engines: {node: '>=0.10.0'}
object.values@1.2.0:
resolution: {integrity: sha512-yBYjY9QX2hnRmZHAjG/f13MzmBzxzYgQhFrke06TTyKY5zSTEqkOeukBzIdVA3j3ulu8Qa3MbVFShV7T2RmGtQ==}
engines: {node: '>= 0.4'}
@@ -15105,10 +15004,6 @@ packages:
resolution: {integrity: sha512-U+5l2KrcMNOUPYvazA3h5ekF80FHTUG+87SEAmHZmolh1M+i/WyTCxVzmi+tidIa1tM4BSe8g2Y/D3loWDjj+w==}
engines: {node: '>=4'}
randomatic@3.1.1:
resolution: {integrity: sha512-TuDE5KxZ0J461RVjrJZCJc+J+zCkTb1MbH9AQUq68sMhOMcy9jLcb3BrZKgp9q9Ncltdg4QVqWrH02W2EFFVYw==}
engines: {node: '>= 0.10.0'}
randombytes@2.1.0:
resolution: {integrity: sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==}
@@ -15707,11 +15602,6 @@ packages:
remark@15.0.1:
resolution: {integrity: sha512-Eht5w30ruCXgFmxVUSlNWQ9iiimq07URKeFS3hNc8cUWy1llX4KDWfyEDZRycMc+znsN9Ux5/tJ/BFdgdOwA3A==}
remarkable@1.7.4:
resolution: {integrity: sha512-e6NKUXgX95whv7IgddywbeN/ItCkWbISmc2DiqHJb0wTrqZIexqdco5b8Z3XZoo/48IdNVKM9ZCvTPJ4F5uvhg==}
engines: {node: '>= 0.10.0'}
hasBin: true
remedial@1.0.8:
resolution: {integrity: sha512-/62tYiOe6DzS5BqVsNpH/nkGlX45C/Sp6V+NtiN6JQNS1Viay7cWkazmRkrQrdFj2eshDe96SIQNIoMxqhzBOg==}
@@ -15731,10 +15621,6 @@ packages:
resolution: {integrity: sha512-U7XpAktpbSgHTRSNRrjKSrjYkZKuhUukfoBlXWXUExCAqhzh1TU3BDRAfJmarcl5voKS+pbKU9MvyLWKZ4UEEg==}
engines: {node: '>= 0.8.0'}
repeat-element@1.1.4:
resolution: {integrity: sha512-LFiNfRcSu7KK3evMyYOuCzv3L10TW7yC1G2/+StMjK8Y6Vqd2MG7r/Qjw4ghtuCOjFvlnms/iMmLqpvW/ES/WQ==}
engines: {node: '>=0.10.0'}
repeat-string@1.6.1:
resolution: {integrity: sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w==}
engines: {node: '>=0.10'}
@@ -16036,10 +15922,6 @@ packages:
resolution: {integrity: sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==}
engines: {node: '>= 0.4'}
set-getter@0.1.1:
resolution: {integrity: sha512-9sVWOy+gthr+0G9DzqqLaYNA7+5OKkSmcqjL9cBpDEaZrr3ShQlyX2cZ/O/ozE41oxn/Tt0LGEM/w4Rub3A3gw==}
engines: {node: '>=0.10.0'}
set-harmonic-interval@1.0.1:
resolution: {integrity: sha512-AhICkFV84tBP1aWqPwLZqFvAwqEoVA9kxNMniGEUvzOlm4vLmOFLiTT3UZ6bziJTy4bOVpzWGTfSCbmaayGx8g==}
engines: {node: '>=6.9'}
@@ -16487,10 +16369,6 @@ packages:
resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==}
engines: {node: '>=4'}
strip-color@0.1.0:
resolution: {integrity: sha512-p9LsUieSjWNNAxVCXLeilaDlmuUOrDS5/dF9znM1nZc7EGX5+zEFC0bEevsNIaldjlks+2jns5Siz6F9iK6jwA==}
engines: {node: '>=0.10.0'}
strip-eof@1.0.0:
resolution: {integrity: sha512-7FCwGGmx8mD5xQd3RPUvnSpUXHM3BWuzjtpD4TXsfcZ9EL4azvVVUscFYwD9nx8Kh+uCBC00XBtAykoMHwTh8Q==}
engines: {node: '>=0.10.0'}
@@ -16740,9 +16618,6 @@ packages:
resolution: {integrity: sha512-nt6AMGKW1p/70DF/hGBdJB57B8Tspmbp5gfJ8ilhLnt7kkr2ye7hzD6NVG8GGErk2HWF34igrL2CXmNIkzKqKw==}
engines: {node: '>=18'}
through2@2.0.5:
resolution: {integrity: sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==}
through@2.3.8:
resolution: {integrity: sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==}
@@ -16808,10 +16683,6 @@ packages:
to-no-case@1.0.2:
resolution: {integrity: sha512-Z3g735FxuZY8rodxV4gH7LxClE4H0hTIyHNIHdk+vpQxjLm0cwnKXq/OFVZ76SOQmto7txVcwSCwkU5kqp+FKg==}
to-object-path@0.3.0:
resolution: {integrity: sha512-9mWHdnGRuh3onocaHzukyvCZhzvr6tiflAy/JRFXcJX0TjgfWA9pk9t8CMbzmBE4Jfw58pXbkngtBtqYxzNEyg==}
engines: {node: '>=0.10.0'}
to-regex-range@5.0.1:
resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==}
engines: {node: '>=8.0'}
@@ -16840,9 +16711,6 @@ packages:
resolution: {integrity: sha512-lbDrTLVsHhOMljPscd0yitpozq7Ga2M5Cvez5AjGg8GASBjtt6iERCAJ93yommPmz62fb45oFIXHEZ3u9bfJEA==}
engines: {node: '>=14.16'}
toml@2.3.6:
resolution: {integrity: sha512-gVweAectJU3ebq//Ferr2JUY4WKSDe5N+z0FvjDncLGyHmIDoxgY/2Ie4qfEIDm4IS7OA6Rmdm7pdEEdMcV/xQ==}
toml@3.0.0:
resolution: {integrity: sha512-y/mWCZinnvxjTKYhJ+pYxwD0mRLVvOtdS2Awbgxln6iEnt4rk0yBxeSBHkGJcPucRiG0e55mwWp+g/05rsrd6w==}
@@ -27701,10 +27569,6 @@ snapshots:
dependencies:
type-fest: 0.21.3
ansi-red@0.1.1:
dependencies:
ansi-wrap: 0.1.0
ansi-regex@5.0.1: {}
ansi-regex@6.0.1: {}
@@ -27721,8 +27585,6 @@ snapshots:
ansi-styles@6.2.1: {}
ansi-wrap@0.1.0: {}
ansis@3.17.0: {}
any-promise@1.3.0: {}
@@ -27908,10 +27770,6 @@ snapshots:
auto-bind@4.0.0: {}
autolinker@0.28.1:
dependencies:
gulp-header: 1.8.12
autoprefixer@10.4.16(postcss@8.5.3):
dependencies:
browserslist: 4.24.4
@@ -28479,8 +28337,6 @@ snapshots:
codemirror@5.65.15: {}
coffee-script@1.12.7: {}
collapse-white-space@2.1.0: {}
color-convert@1.9.3:
@@ -28560,13 +28416,6 @@ snapshots:
concat-map@0.0.1: {}
concat-stream@1.6.2:
dependencies:
buffer-from: 1.1.2
inherits: 2.0.4
readable-stream: 2.3.8
typedarray: 0.0.6
concat-stream@2.0.0:
dependencies:
buffer-from: 1.1.2
@@ -28574,10 +28423,6 @@ snapshots:
readable-stream: 3.6.2
typedarray: 0.0.6
concat-with-sourcemaps@1.1.0:
dependencies:
source-map: 0.6.1
concurrently@9.1.2:
dependencies:
chalk: 4.1.2
@@ -29101,8 +28946,6 @@ snapshots:
dependencies:
dequal: 2.0.3
diacritics-map@0.1.0: {}
didyoumean@1.2.2: {}
diff-sequences@29.6.3: {}
@@ -29950,10 +29793,6 @@ snapshots:
exit-hook@2.2.1: {}
expand-range@1.8.2:
dependencies:
fill-range: 2.2.4
expand-tilde@2.0.2:
dependencies:
homedir-polyfill: 1.0.3
@@ -30206,14 +30045,6 @@ snapshots:
dependencies:
minimatch: 5.1.6
fill-range@2.2.4:
dependencies:
is-number: 2.1.0
isobject: 2.1.0
randomatic: 3.1.1
repeat-element: 1.1.4
repeat-string: 1.6.1
fill-range@7.1.1:
dependencies:
to-regex-range: 5.0.1
@@ -30286,8 +30117,6 @@ snapshots:
dependencies:
is-callable: 1.2.7
for-in@1.0.2: {}
foreach@2.0.6: {}
foreground-child@3.1.1:
@@ -30753,14 +30582,6 @@ snapshots:
graphql@16.11.0: {}
gray-matter@2.1.1:
dependencies:
ansi-red: 0.1.1
coffee-script: 1.12.7
extend-shallow: 2.0.1
js-yaml: 3.14.1
toml: 2.3.6
gray-matter@4.0.3:
dependencies:
js-yaml: 3.14.1
@@ -30768,12 +30589,6 @@ snapshots:
section-matter: 1.0.0
strip-bom-string: 1.0.0
gulp-header@1.8.12:
dependencies:
concat-with-sourcemaps: 1.1.0
lodash.template: 4.5.0
through2: 2.0.5
gzip-size@6.0.0:
dependencies:
duplexer: 0.1.2
@@ -31489,10 +31304,6 @@ snapshots:
is-extendable@0.1.1: {}
is-extendable@1.0.1:
dependencies:
is-plain-object: 2.0.4
is-extglob@2.1.1: {}
is-finalizationregistry@1.0.2:
@@ -31550,12 +31361,6 @@ snapshots:
call-bound: 1.0.4
has-tostringtag: 1.0.2
is-number@2.1.0:
dependencies:
kind-of: 3.2.2
is-number@4.0.0: {}
is-number@7.0.0: {}
is-obj@3.0.0: {}
@@ -31708,10 +31513,6 @@ snapshots:
isexe@3.1.1: {}
isobject@2.1.0:
dependencies:
isarray: 1.0.0
isobject@3.0.1: {}
isomorphic-unfetch@3.1.0(encoding@0.1.13):
@@ -32023,10 +31824,6 @@ snapshots:
dependencies:
language-subtag-registry: 0.3.23
lazy-cache@2.0.2:
dependencies:
set-getter: 0.1.1
lazystream@1.0.1:
dependencies:
readable-stream: 2.3.8
@@ -32088,13 +31885,6 @@ snapshots:
dependencies:
uc.micro: 2.1.0
list-item@1.1.1:
dependencies:
expand-range: 1.8.2
extend-shallow: 2.0.1
is-number: 2.1.0
repeat-string: 1.6.1
listhen@1.9.0:
dependencies:
'@parcel/watcher': 2.5.1
@@ -32173,8 +31963,6 @@ snapshots:
lodash._createset@4.0.3: {}
lodash._reinterpolate@3.0.0: {}
lodash._root@3.0.1: {}
lodash._stringtopath@4.8.0:
@@ -32195,15 +31983,6 @@ snapshots:
lodash.sortby@4.7.0: {}
lodash.template@4.5.0:
dependencies:
lodash._reinterpolate: 3.0.0
lodash.templatesettings: 4.2.0
lodash.templatesettings@4.2.0:
dependencies:
lodash._reinterpolate: 3.0.0
lodash.throttle@4.1.1: {}
lodash.uniqby@4.5.0:
@@ -32372,33 +32151,14 @@ snapshots:
punycode.js: 2.3.1
uc.micro: 2.1.0
markdown-link@0.1.1: {}
markdown-table@3.0.3: {}
markdown-toc@1.2.0:
dependencies:
concat-stream: 1.6.2
diacritics-map: 0.1.0
gray-matter: 2.1.1
lazy-cache: 2.0.2
list-item: 1.1.1
markdown-link: 0.1.1
minimist: 1.2.8
mixin-deep: 1.3.2
object.pick: 1.3.0
remarkable: 1.7.4
repeat-string: 1.6.1
strip-color: 0.1.0
markdown-wasm@1.2.0: {}
marked@4.3.0: {}
math-intrinsics@1.1.0: {}
math-random@1.0.4: {}
md5@2.3.0:
dependencies:
charenc: 0.0.2
@@ -33486,11 +33246,6 @@ snapshots:
mitt@3.0.1: {}
mixin-deep@1.3.2:
dependencies:
for-in: 1.0.2
is-extendable: 1.0.1
mkdirp@1.0.4: {}
mkdirp@2.1.6: {}
@@ -34147,10 +33902,6 @@ snapshots:
define-properties: 1.2.1
es-abstract: 1.23.3
object.pick@1.3.0:
dependencies:
isobject: 3.0.1
object.values@1.2.0:
dependencies:
call-bind: 1.0.7
@@ -35063,12 +34814,6 @@ snapshots:
drange: 1.1.1
ret: 0.2.2
randomatic@3.1.1:
dependencies:
is-number: 4.0.0
kind-of: 6.0.3
math-random: 1.0.4
randombytes@2.1.0:
dependencies:
safe-buffer: 5.2.1
@@ -35953,11 +35698,6 @@ snapshots:
transitivePeerDependencies:
- supports-color
remarkable@1.7.4:
dependencies:
argparse: 1.0.10
autolinker: 0.28.1
remedial@1.0.8: {}
remove-accents@0.4.2: {}
@@ -35977,8 +35717,6 @@ snapshots:
rename-keys@1.2.0: {}
repeat-element@1.1.4: {}
repeat-string@1.6.1: {}
require-directory@2.1.1: {}
@@ -36344,10 +36082,6 @@ snapshots:
functions-have-names: 1.2.3
has-property-descriptors: 1.0.2
set-getter@0.1.1:
dependencies:
to-object-path: 0.3.0
set-harmonic-interval@1.0.1: {}
set-proto@1.0.0:
@@ -36917,8 +36651,6 @@ snapshots:
strip-bom@3.0.0: {}
strip-color@0.1.0: {}
strip-eof@1.0.0: {}
strip-final-newline@3.0.0: {}
@@ -37279,11 +37011,6 @@ snapshots:
throttleit@2.1.0: {}
through2@2.0.5:
dependencies:
readable-stream: 2.3.8
xtend: 4.0.2
through@2.3.8: {}
timeout-signal@2.0.0: {}
@@ -37334,10 +37061,6 @@ snapshots:
to-no-case@1.0.2: {}
to-object-path@0.3.0:
dependencies:
kind-of: 3.2.2
to-regex-range@5.0.1:
dependencies:
is-number: 7.0.0
@@ -37366,8 +37089,6 @@ snapshots:
'@tokenizer/token': 0.3.0
ieee754: 1.2.1
toml@2.3.6: {}
toml@3.0.0: {}
topojson-client@3.1.0: