mirror of
https://github.com/supabase/supabase.git
synced 2026-10-18 07:39:07 +03:00
add custom markdown support to our cms
This commit is contained in:
1 parent
1c990be3fc
commit
9c7db1baf7
11 files changed
+450
-147
No files matched your search
@@ -51,8 +51,12 @@
|
||||
"payload": "3.52.0",
|
||||
"pg": "^8.16.3",
|
||||
"prism-react-renderer": "^2.3.1",
|
||||
"prismjs": "^1.30.0",
|
||||
"react": "catalog:",
|
||||
"react-dom": "catalog:",
|
||||
"react-markdown": "^10.1.0",
|
||||
"rehype-highlight": "^7.0.2",
|
||||
"remark-gfm": "^4.0.1",
|
||||
"sharp": "~0.34.0",
|
||||
"tailwind-merge": "^1.13.2",
|
||||
"tsx": "^4.19.3",
|
||||
|
||||
@@ -17,6 +17,7 @@ import { MetaTitleComponent as MetaTitleComponent_a8a977ebc872c5d5ea7ee689724c08
|
||||
import { MetaImageComponent as MetaImageComponent_a8a977ebc872c5d5ea7ee689724c0860 } from '@payloadcms/plugin-seo/client'
|
||||
import { MetaDescriptionComponent as MetaDescriptionComponent_a8a977ebc872c5d5ea7ee689724c0860 } from '@payloadcms/plugin-seo/client'
|
||||
import { PreviewComponent as PreviewComponent_a8a977ebc872c5d5ea7ee689724c0860 } from '@payloadcms/plugin-seo/client'
|
||||
import { MarkdownEditor as MarkdownEditor_ead0010f14dbca6b92002dd21c00f8a3 } from '@/fields/MarkdownEditor/Component'
|
||||
import { S3ClientUploadHandler as S3ClientUploadHandler_f97aa6c64367fa259c5bc0567239ef24 } from '@payloadcms/storage-s3/client'
|
||||
|
||||
export const importMap = {
|
||||
@@ -39,5 +40,6 @@ export const importMap = {
|
||||
"@payloadcms/plugin-seo/client#MetaImageComponent": MetaImageComponent_a8a977ebc872c5d5ea7ee689724c0860,
|
||||
"@payloadcms/plugin-seo/client#MetaDescriptionComponent": MetaDescriptionComponent_a8a977ebc872c5d5ea7ee689724c0860,
|
||||
"@payloadcms/plugin-seo/client#PreviewComponent": PreviewComponent_a8a977ebc872c5d5ea7ee689724c0860,
|
||||
"@/fields/MarkdownEditor/Component#MarkdownEditor": MarkdownEditor_ead0010f14dbca6b92002dd21c00f8a3,
|
||||
"@payloadcms/storage-s3/client#S3ClientUploadHandler": S3ClientUploadHandler_f97aa6c64367fa259c5bc0567239ef24
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
/* Override Payload CMS success colors with Supabase brand green */
|
||||
:root {
|
||||
/* Light theme - Supabase brand green scale */
|
||||
--color-success-50: hsla(153, 80%, 99%, 1);
|
||||
--color-success-100: hsla(148, 88%, 97%, 1);
|
||||
--color-success-150: hsla(148, 76%, 95%, 1);
|
||||
--color-success-200: hsla(148, 68%, 92%, 1);
|
||||
--color-success-250: hsla(148, 59%, 88%, 1);
|
||||
--color-success-300: hsla(149, 43%, 82%, 1);
|
||||
--color-success-350: hsla(149, 43%, 75%, 1);
|
||||
--color-success-400: hsla(149, 43%, 69%, 1);
|
||||
--color-success-450: hsla(154, 55%, 57%, 1);
|
||||
--color-success-500: hsla(154, 55%, 45%, 1);
|
||||
--color-success-550: hsla(153, 60%, 53%, 1);
|
||||
--color-success-600: hsla(153, 50%, 50%, 1);
|
||||
--color-success-650: hsla(153, 50%, 42%, 1);
|
||||
--color-success-700: hsla(153, 50%, 34%, 1);
|
||||
--color-success-750: hsla(153, 45%, 23%, 1);
|
||||
--color-success-800: hsla(153, 40%, 18%, 1);
|
||||
--color-success-850: hsla(153, 40%, 15%, 1);
|
||||
--color-success-900: hsla(153, 40%, 12%, 1);
|
||||
--color-success-950: hsla(153, 40%, 8%, 1);
|
||||
}
|
||||
|
||||
/* Dark theme - Supabase brand green dark scale */
|
||||
[data-theme='dark'] {
|
||||
--color-success-50: hsla(153, 75%, 6%, 1);
|
||||
--color-success-100: hsla(153, 73%, 7%, 1);
|
||||
--color-success-150: hsla(154, 69%, 8%, 1);
|
||||
--color-success-200: hsla(154, 69%, 9%, 1);
|
||||
--color-success-250: hsla(154, 67%, 10%, 1);
|
||||
--color-success-300: hsla(154, 67%, 11%, 1);
|
||||
--color-success-350: hsla(154, 66%, 12%, 1);
|
||||
--color-success-400: hsla(154, 66%, 13%, 1);
|
||||
--color-success-450: hsla(154, 64%, 15%, 1);
|
||||
--color-success-500: hsla(154, 64%, 17%, 1);
|
||||
--color-success-550: hsla(154, 62%, 19%, 1);
|
||||
--color-success-600: hsla(154, 62%, 22%, 1);
|
||||
--color-success-650: hsla(153, 60%, 25%, 1);
|
||||
--color-success-700: hsla(153, 60%, 28%, 1);
|
||||
--color-success-750: hsla(153, 60%, 40%, 1);
|
||||
--color-success-800: hsla(153, 60%, 53%, 1);
|
||||
--color-success-850: hsla(153, 60%, 62%, 1);
|
||||
--color-success-900: hsla(153, 60%, 70%, 1);
|
||||
--color-success-950: hsla(153, 60%, 95%, 1);
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { CollectionConfig } from 'payload'
|
||||
import { isAdmin } from '../access/isAdmin'
|
||||
import { isAdmin } from '../access/isAdmin.ts'
|
||||
|
||||
export const Categories: CollectionConfig = {
|
||||
slug: 'categories',
|
||||
|
||||
@@ -1,22 +1,8 @@
|
||||
import type { CollectionConfig } from 'payload'
|
||||
|
||||
import {
|
||||
BlocksFeature,
|
||||
FixedToolbarFeature,
|
||||
HeadingFeature,
|
||||
HorizontalRuleFeature,
|
||||
InlineToolbarFeature,
|
||||
lexicalEditor,
|
||||
} from '@payloadcms/richtext-lexical'
|
||||
|
||||
import { isAnyone } from '../../access/isAnyone.ts'
|
||||
import { isAuthenticated } from '../../access/isAuthenticated.ts'
|
||||
|
||||
import { Banner } from '../../blocks/Banner/config.ts'
|
||||
import { Code } from '../../blocks/Code/config.ts'
|
||||
import { MediaBlock } from '../../blocks/MediaBlock/config.ts'
|
||||
import { Quote } from '../../blocks/Quote/config.ts'
|
||||
import { YouTube } from '../../blocks/YouTube/config.ts'
|
||||
import { populateAuthors } from './hooks/populateAuthors.ts'
|
||||
import { revalidateDelete, revalidatePost } from './hooks/revalidatePost.ts'
|
||||
|
||||
@@ -91,21 +77,16 @@ export const Posts: CollectionConfig = {
|
||||
fields: [
|
||||
{
|
||||
name: 'content',
|
||||
type: 'richText',
|
||||
editor: lexicalEditor({
|
||||
features: ({ rootFeatures }) => {
|
||||
return [
|
||||
...rootFeatures,
|
||||
HeadingFeature({ enabledHeadingSizes: ['h1', 'h2', 'h3', 'h4'] }),
|
||||
BlocksFeature({ blocks: [Banner, Code, MediaBlock, Quote, YouTube] }),
|
||||
FixedToolbarFeature(),
|
||||
InlineToolbarFeature(),
|
||||
HorizontalRuleFeature(),
|
||||
]
|
||||
},
|
||||
}),
|
||||
label: false,
|
||||
type: 'textarea',
|
||||
label: 'Content (Markdown)',
|
||||
required: true,
|
||||
admin: {
|
||||
description:
|
||||
'Write your content in Markdown format. Supports headings, lists, links, images, code blocks, and more.',
|
||||
components: {
|
||||
Field: '@/fields/MarkdownEditor/Component#MarkdownEditor',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
label: 'Content',
|
||||
|
||||
@@ -1,76 +1,19 @@
|
||||
import { MediaBlock } from '@/blocks/MediaBlock/Component'
|
||||
import {
|
||||
DefaultNodeTypes,
|
||||
SerializedBlockNode,
|
||||
SerializedLinkNode,
|
||||
type DefaultTypedEditorState,
|
||||
} from '@payloadcms/richtext-lexical'
|
||||
import {
|
||||
JSXConvertersFunction,
|
||||
LinkJSXConverter,
|
||||
RichText as ConvertRichText,
|
||||
} from '@payloadcms/richtext-lexical/react'
|
||||
|
||||
import { CodeBlock, CodeBlockProps } from '@/blocks/Code/Component'
|
||||
|
||||
import type {
|
||||
BannerBlock as BannerBlockProps,
|
||||
MediaBlock as MediaBlockProps,
|
||||
QuoteBlock as QuoteBlockProps,
|
||||
YouTubeBlock as YouTubeBlockProps,
|
||||
} from '@/payload-types'
|
||||
import { BannerBlock } from '@/blocks/Banner/Component'
|
||||
import { QuoteBlock } from '@/blocks/Quote/Component'
|
||||
import { YouTubeBlock } from '@/blocks/YouTube/Component'
|
||||
import ReactMarkdown from 'react-markdown'
|
||||
import remarkGfm from 'remark-gfm'
|
||||
import rehypeHighlight from 'rehype-highlight'
|
||||
import { cn } from '@/utilities/ui'
|
||||
|
||||
type NodeTypes =
|
||||
| DefaultNodeTypes
|
||||
| SerializedBlockNode<
|
||||
MediaBlockProps | BannerBlockProps | CodeBlockProps | QuoteBlockProps | YouTubeBlockProps
|
||||
>
|
||||
|
||||
const internalDocToHref = ({ linkNode }: { linkNode: SerializedLinkNode }) => {
|
||||
const { value, relationTo } = linkNode.fields.doc!
|
||||
if (typeof value !== 'object') {
|
||||
throw new Error('Expected value to be an object')
|
||||
}
|
||||
const slug = value.slug
|
||||
return relationTo === 'posts' ? `/posts/${slug}` : `/${slug}`
|
||||
}
|
||||
|
||||
const jsxConverters: JSXConvertersFunction<NodeTypes> = ({ defaultConverters }) => ({
|
||||
...defaultConverters,
|
||||
...LinkJSXConverter({ internalDocToHref }),
|
||||
blocks: {
|
||||
banner: ({ node }) => <BannerBlock className="col-start-2 mb-4" {...node.fields} />,
|
||||
mediaBlock: ({ node }) => (
|
||||
<MediaBlock
|
||||
className="col-start-1 col-span-3"
|
||||
imgClassName="m-0"
|
||||
{...node.fields}
|
||||
captionClassName="mx-auto max-w-[48rem]"
|
||||
enableGutter={false}
|
||||
disableInnerContainer={true}
|
||||
/>
|
||||
),
|
||||
code: ({ node }) => <CodeBlock className="col-start-2" {...node.fields} />,
|
||||
quote: ({ node }) => <QuoteBlock className="col-start-2" {...node.fields} />,
|
||||
youtube: ({ node }) => <YouTubeBlock className="col-start-2" {...node.fields} />,
|
||||
},
|
||||
})
|
||||
|
||||
type Props = {
|
||||
data: DefaultTypedEditorState
|
||||
data: string
|
||||
enableGutter?: boolean
|
||||
enableProse?: boolean
|
||||
} & React.HTMLAttributes<HTMLDivElement>
|
||||
|
||||
export default function RichText(props: Props) {
|
||||
const { className, enableProse = true, enableGutter = true, ...rest } = props
|
||||
const { data, className, enableProse = true, enableGutter = true, ...rest } = props
|
||||
|
||||
return (
|
||||
<ConvertRichText
|
||||
converters={jsxConverters}
|
||||
<div
|
||||
className={cn(
|
||||
'payload-richtext',
|
||||
{
|
||||
@@ -81,6 +24,17 @@ export default function RichText(props: Props) {
|
||||
className
|
||||
)}
|
||||
{...rest}
|
||||
/>
|
||||
>
|
||||
<ReactMarkdown
|
||||
remarkPlugins={[remarkGfm]}
|
||||
rehypePlugins={[rehypeHighlight]}
|
||||
components={{
|
||||
// Customize rendering if needed
|
||||
a: ({ node, ...props }) => <a {...props} target="_blank" rel="noopener noreferrer" />,
|
||||
}}
|
||||
>
|
||||
{data}
|
||||
</ReactMarkdown>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
'use client'
|
||||
|
||||
import React, { useCallback, useEffect, useRef } from 'react'
|
||||
import { useField } from '@payloadcms/ui'
|
||||
import Prism from 'prismjs'
|
||||
import 'prismjs/components/prism-markdown'
|
||||
import './styles.css'
|
||||
|
||||
type MarkdownEditorProps = {
|
||||
path: string
|
||||
label?: string
|
||||
required?: boolean
|
||||
}
|
||||
|
||||
export const MarkdownEditor: React.FC<MarkdownEditorProps> = ({ path, label, required }) => {
|
||||
const { value, setValue } = useField<string>({ path })
|
||||
const highlightRef = useRef<HTMLPreElement>(null)
|
||||
const textareaRef = useRef<HTMLTextAreaElement>(null)
|
||||
|
||||
const handleChange = useCallback(
|
||||
(e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||
setValue(e.target.value)
|
||||
},
|
||||
[setValue]
|
||||
)
|
||||
|
||||
// Auto-resize textarea
|
||||
useEffect(() => {
|
||||
const textarea = textareaRef.current
|
||||
if (textarea) {
|
||||
textarea.style.height = 'auto'
|
||||
textarea.style.height = `${textarea.scrollHeight}px`
|
||||
}
|
||||
}, [value])
|
||||
|
||||
// Highlight using Prism
|
||||
useEffect(() => {
|
||||
if (highlightRef.current) {
|
||||
Prism.highlightElement(highlightRef.current)
|
||||
}
|
||||
}, [value])
|
||||
|
||||
return (
|
||||
<div className="markdown-editor-field">
|
||||
<div className="field-label">
|
||||
{label && (
|
||||
<label>
|
||||
{label}
|
||||
{required && <span className="required">*</span>}
|
||||
</label>
|
||||
)}
|
||||
</div>
|
||||
<div className="markdown-editor-container">
|
||||
<pre className="markdown-editor-highlight" aria-hidden="true">
|
||||
<code ref={highlightRef} className="language-markdown">
|
||||
{value || ''}
|
||||
</code>
|
||||
</pre>
|
||||
<textarea
|
||||
ref={textareaRef}
|
||||
className="markdown-editor-textarea"
|
||||
value={value || ''}
|
||||
onChange={handleChange}
|
||||
placeholder="Write your markdown content here..."
|
||||
rows={20}
|
||||
spellCheck="false"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import type { Field } from 'payload'
|
||||
|
||||
export const markdownField = (overrides?: Partial<Field>): Field[] => {
|
||||
return [
|
||||
{
|
||||
name: 'content',
|
||||
type: 'textarea',
|
||||
required: true,
|
||||
admin: {
|
||||
components: {
|
||||
Field: '@/fields/MarkdownEditor/Component#MarkdownEditor',
|
||||
},
|
||||
},
|
||||
...overrides,
|
||||
},
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,190 @@
|
||||
.markdown-editor-field {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.field-label label {
|
||||
display: block;
|
||||
margin-bottom: 0.5rem;
|
||||
font-weight: 500;
|
||||
font-size: 0.875rem;
|
||||
color: var(--theme-elevation-900);
|
||||
}
|
||||
|
||||
.field-label .required {
|
||||
color: var(--color-error-500);
|
||||
margin-left: 0.25rem;
|
||||
}
|
||||
|
||||
.markdown-editor-container {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
border: 1px solid var(--theme-elevation-150);
|
||||
border-radius: 4px;
|
||||
background: var(--theme-input-bg);
|
||||
overflow: hidden;
|
||||
transition: border-color 150ms ease;
|
||||
}
|
||||
|
||||
.markdown-editor-container:focus-within {
|
||||
border-color: var(--theme-elevation-400);
|
||||
}
|
||||
|
||||
.markdown-editor-highlight,
|
||||
.markdown-editor-textarea {
|
||||
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', 'Consolas', 'courier-new', monospace;
|
||||
font-size: 13px;
|
||||
line-height: 1.6;
|
||||
padding: 1rem;
|
||||
margin: 0;
|
||||
white-space: pre-wrap;
|
||||
word-wrap: break-word;
|
||||
tab-size: 2;
|
||||
}
|
||||
|
||||
.markdown-editor-highlight {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
overflow: hidden;
|
||||
background: transparent !important;
|
||||
}
|
||||
|
||||
.markdown-editor-highlight code {
|
||||
display: block;
|
||||
font-family: inherit;
|
||||
font-size: inherit;
|
||||
line-height: inherit;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
white-space: pre-wrap;
|
||||
word-wrap: break-word;
|
||||
background: transparent !important;
|
||||
color: var(--theme-elevation-800);
|
||||
}
|
||||
|
||||
.markdown-editor-textarea {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
background: transparent;
|
||||
border: none;
|
||||
outline: none;
|
||||
resize: none;
|
||||
color: transparent;
|
||||
z-index: 1;
|
||||
caret-color: var(--theme-elevation-800);
|
||||
-webkit-text-fill-color: transparent;
|
||||
min-height: 400px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.markdown-editor-textarea::placeholder {
|
||||
color: var(--theme-elevation-400);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.markdown-editor-textarea::selection {
|
||||
background: var(--theme-elevation-150);
|
||||
color: var(--theme-elevation-1000);
|
||||
}
|
||||
|
||||
/* Custom Prism theme - elevation based with green highlights */
|
||||
.markdown-editor-highlight .token.title,
|
||||
.markdown-editor-highlight .token.heading {
|
||||
color: var(--color-success-800);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.markdown-editor-highlight .token.heading.punctuation {
|
||||
color: var(--color-success-800);
|
||||
}
|
||||
|
||||
.markdown-editor-highlight .token.bold {
|
||||
color: var(--theme-elevation-800);
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.markdown-editor-highlight .token.italic {
|
||||
color: var(--theme-elevation-800);
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.markdown-editor-highlight .token.url,
|
||||
.markdown-editor-highlight .token.link {
|
||||
color: var(--color-success-800);
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.markdown-editor-highlight .token.url-reference,
|
||||
.markdown-editor-highlight .token.link-text {
|
||||
color: var(--color-success-800);
|
||||
}
|
||||
|
||||
.markdown-editor-highlight .token.list.punctuation {
|
||||
color: var(--color-success-800);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.markdown-editor-highlight .token.code,
|
||||
.markdown-editor-highlight .token.code-snippet {
|
||||
color: var(--color-success-800);
|
||||
background: var(--color-success-50);
|
||||
padding: 0.125rem 0.25rem;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.markdown-editor-highlight .token.code-fence,
|
||||
.markdown-editor-highlight .token.code-block {
|
||||
color: var(--color-success-800);
|
||||
}
|
||||
|
||||
.markdown-editor-highlight .token.blockquote {
|
||||
color: var(--theme-elevation-600);
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.markdown-editor-highlight .token.blockquote.punctuation {
|
||||
color: var(--theme-elevation-400);
|
||||
}
|
||||
|
||||
.markdown-editor-highlight .token.hr,
|
||||
.markdown-editor-highlight .token.hr.punctuation {
|
||||
color: var(--theme-elevation-300);
|
||||
}
|
||||
|
||||
.markdown-editor-highlight .token.table-header,
|
||||
.markdown-editor-highlight .token.table-cell {
|
||||
color: var(--theme-elevation-800);
|
||||
}
|
||||
|
||||
.markdown-editor-highlight .token.table-delimiter,
|
||||
.markdown-editor-highlight .token.table.punctuation {
|
||||
color: var(--theme-elevation-500);
|
||||
}
|
||||
|
||||
.markdown-editor-highlight .token.strike,
|
||||
.markdown-editor-highlight .token.strikethrough {
|
||||
color: var(--theme-elevation-500);
|
||||
text-decoration: line-through;
|
||||
}
|
||||
|
||||
.markdown-editor-highlight .token.task-list-item,
|
||||
.markdown-editor-highlight .token.checkbox {
|
||||
color: var(--color-success-800);
|
||||
}
|
||||
|
||||
.markdown-editor-highlight .token.punctuation {
|
||||
color: var(--theme-elevation-500);
|
||||
}
|
||||
|
||||
.markdown-editor-highlight .token.attr-name,
|
||||
.markdown-editor-highlight .token.keyword {
|
||||
color: var(--color-success-800);
|
||||
}
|
||||
|
||||
.markdown-editor-highlight .token.code-language {
|
||||
color: var(--color-success-800);
|
||||
font-size: 0.85em;
|
||||
}
|
||||
@@ -19,42 +19,66 @@ export interface ProcessedCMSContent {
|
||||
}
|
||||
|
||||
/**
|
||||
* Process PayloadCMS rich text content for use in the www blog
|
||||
* This function handles both basic content and custom blocks
|
||||
* Process PayloadCMS content for use in the www blog
|
||||
* This function handles:
|
||||
* 1. Raw markdown strings (new textarea field)
|
||||
* 2. Lexical rich text JSON (legacy format)
|
||||
* 3. Custom blocks converted to existing www syntax
|
||||
*/
|
||||
export async function processCMSContent(
|
||||
richTextContent: any,
|
||||
content: any,
|
||||
tocDepth: number = 3
|
||||
): Promise<ProcessedCMSContent> {
|
||||
try {
|
||||
// Convert rich text to markdown with blocks converted to existing www syntax
|
||||
const { markdown, blocks } = convertRichTextToMarkdownWithBlocks(richTextContent)
|
||||
// Check if content is already a markdown string
|
||||
if (typeof content === 'string') {
|
||||
// Direct markdown string - no conversion needed
|
||||
const toc = await generateTocFromMarkdown(content, tocDepth)
|
||||
const mdxContent = await mdxSerialize(content, { tocDepth })
|
||||
|
||||
// Generate TOC from the converted markdown
|
||||
const toc = await generateTocFromMarkdown(markdown, tocDepth)
|
||||
|
||||
// Serialize the markdown for MDX (blocks are now converted to existing components)
|
||||
const mdxContent = await mdxSerialize(markdown, { tocDepth })
|
||||
|
||||
return {
|
||||
content: mdxContent,
|
||||
blocks,
|
||||
toc,
|
||||
plainMarkdown: markdown,
|
||||
return {
|
||||
content: mdxContent,
|
||||
blocks: [],
|
||||
toc,
|
||||
plainMarkdown: content,
|
||||
}
|
||||
}
|
||||
|
||||
// Handle Lexical JSON format (legacy)
|
||||
if (content?.root?.children) {
|
||||
// Convert rich text to markdown with blocks converted to existing www syntax
|
||||
const { markdown, blocks } = convertRichTextToMarkdownWithBlocks(content)
|
||||
|
||||
// Generate TOC from the converted markdown
|
||||
const toc = await generateTocFromMarkdown(markdown, tocDepth)
|
||||
|
||||
// Serialize the markdown for MDX (blocks are now converted to existing components)
|
||||
const mdxContent = await mdxSerialize(markdown, { tocDepth })
|
||||
|
||||
return {
|
||||
content: mdxContent,
|
||||
blocks,
|
||||
toc,
|
||||
plainMarkdown: markdown,
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback for unexpected format
|
||||
throw new Error('Content format not recognized')
|
||||
} catch (error) {
|
||||
console.error('Error processing CMS content:', error)
|
||||
|
||||
// Fallback to basic conversion
|
||||
const plainMarkdown = convertRichTextToMarkdown(richTextContent)
|
||||
const toc = await generateTocFromMarkdown(plainMarkdown, tocDepth)
|
||||
const mdxContent = await mdxSerialize(plainMarkdown, { tocDepth })
|
||||
// Last resort fallback
|
||||
const fallbackMarkdown =
|
||||
typeof content === 'string' ? content : convertRichTextToMarkdown(content)
|
||||
const toc = await generateTocFromMarkdown(fallbackMarkdown, tocDepth)
|
||||
const mdxContent = await mdxSerialize(fallbackMarkdown, { tocDepth })
|
||||
|
||||
return {
|
||||
content: mdxContent,
|
||||
blocks: [],
|
||||
toc,
|
||||
plainMarkdown,
|
||||
plainMarkdown: fallbackMarkdown,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Generated
+46
-32
@@ -190,12 +190,24 @@ importers:
|
||||
prism-react-renderer:
|
||||
specifier: ^2.3.1
|
||||
version: 2.3.1(react@18.3.1)
|
||||
prismjs:
|
||||
specifier: ^1.30.0
|
||||
version: 1.30.0
|
||||
react:
|
||||
specifier: 'catalog:'
|
||||
version: 18.3.1
|
||||
react-dom:
|
||||
specifier: 'catalog:'
|
||||
version: 18.3.1(react@18.3.1)
|
||||
react-markdown:
|
||||
specifier: ^10.1.0
|
||||
version: 10.1.0(@types/react@18.3.3)(react@18.3.1)(supports-color@8.1.1)
|
||||
rehype-highlight:
|
||||
specifier: ^7.0.2
|
||||
version: 7.0.2
|
||||
remark-gfm:
|
||||
specifier: ^4.0.1
|
||||
version: 4.0.1(supports-color@8.1.1)
|
||||
sharp:
|
||||
specifier: ~0.34.0
|
||||
version: 0.34.3
|
||||
@@ -385,7 +397,7 @@ importers:
|
||||
version: 1.0.3(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
'@sentry/nextjs':
|
||||
specifier: ^10.3.0
|
||||
version: 10.3.0(@opentelemetry/context-async-hooks@2.0.1(@opentelemetry/api@1.9.0))(@opentelemetry/core@2.0.1(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.0.1(@opentelemetry/api@1.9.0))(encoding@0.1.13)(next@15.5.2(@babel/core@7.28.4(supports-color@8.1.1))(@opentelemetry/api@1.9.0)(@playwright/test@1.53.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.77.4))(react@18.3.1)(supports-color@8.1.1)(webpack@5.94.0)
|
||||
version: 10.3.0(@opentelemetry/context-async-hooks@2.0.1(@opentelemetry/api@1.9.0))(@opentelemetry/core@2.0.1(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.0.1(@opentelemetry/api@1.9.0))(encoding@0.1.13)(next@15.5.2(@opentelemetry/api@1.9.0)(@playwright/test@1.53.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.77.4))(react@18.3.1)(supports-color@8.1.1)(webpack@5.94.0)
|
||||
'@supabase/supabase-js':
|
||||
specifier: 'catalog:'
|
||||
version: 2.74.0
|
||||
@@ -800,7 +812,7 @@ importers:
|
||||
version: 1.1.3(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
'@sentry/nextjs':
|
||||
specifier: ^10.3.0
|
||||
version: 10.3.0(@opentelemetry/context-async-hooks@2.0.1(@opentelemetry/api@1.9.0))(@opentelemetry/core@2.0.1(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.0.1(@opentelemetry/api@1.9.0))(encoding@0.1.13)(next@15.5.2(@babel/core@7.28.4(supports-color@8.1.1))(@opentelemetry/api@1.9.0)(@playwright/test@1.53.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.77.4))(react@18.3.1)(supports-color@8.1.1)(webpack@5.94.0)
|
||||
version: 10.3.0(@opentelemetry/context-async-hooks@2.0.1(@opentelemetry/api@1.9.0))(@opentelemetry/core@2.0.1(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.0.1(@opentelemetry/api@1.9.0))(encoding@0.1.13)(next@15.5.2(@opentelemetry/api@1.9.0)(@playwright/test@1.53.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.77.4))(react@18.3.1)(supports-color@8.1.1)(webpack@5.94.0)
|
||||
'@std/path':
|
||||
specifier: npm:@jsr/std__path@^1.0.8
|
||||
version: '@jsr/std__path@1.0.8'
|
||||
@@ -1185,7 +1197,7 @@ importers:
|
||||
version: 4.4.2
|
||||
'@vitest/coverage-v8':
|
||||
specifier: ^3.0.9
|
||||
version: 3.0.9(supports-color@8.1.1)(vitest@3.0.9)
|
||||
version: 3.0.9(supports-color@8.1.1)(vitest@3.0.9(@types/node@22.13.14)(jiti@2.5.1)(jsdom@20.0.3(supports-color@8.1.1))(msw@2.11.3(@types/node@22.13.14)(typescript@5.9.2))(sass@1.77.4)(supports-color@8.1.1)(terser@5.39.0)(tsx@4.20.3)(yaml@2.8.1))
|
||||
'@vitest/ui':
|
||||
specifier: ^3.0.0
|
||||
version: 3.0.4(vitest@3.0.9)
|
||||
@@ -1579,7 +1591,7 @@ importers:
|
||||
version: 1.0.5(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
'@sentry/nextjs':
|
||||
specifier: ^10
|
||||
version: 10.3.0(@opentelemetry/context-async-hooks@2.0.1(@opentelemetry/api@1.9.0))(@opentelemetry/core@2.0.1(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.0.1(@opentelemetry/api@1.9.0))(encoding@0.1.13)(next@15.5.2(@babel/core@7.28.4(supports-color@8.1.1))(@opentelemetry/api@1.9.0)(@playwright/test@1.53.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.77.4))(react@18.3.1)(supports-color@8.1.1)(webpack@5.94.0)
|
||||
version: 10.3.0(@opentelemetry/context-async-hooks@2.0.1(@opentelemetry/api@1.9.0))(@opentelemetry/core@2.0.1(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.0.1(@opentelemetry/api@1.9.0))(encoding@0.1.13)(next@15.5.2(@opentelemetry/api@1.9.0)(@playwright/test@1.53.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.77.4))(react@18.3.1)(supports-color@8.1.1)(webpack@5.94.0)
|
||||
'@supabase/supabase-js':
|
||||
specifier: 'catalog:'
|
||||
version: 2.74.0
|
||||
@@ -2006,7 +2018,7 @@ importers:
|
||||
version: 18.3.0
|
||||
'@vitest/coverage-v8':
|
||||
specifier: ^3.0.9
|
||||
version: 3.0.9(supports-color@8.1.1)(vitest@3.0.9)
|
||||
version: 3.0.9(supports-color@8.1.1)(vitest@3.0.9(@types/node@22.13.14)(jiti@2.5.1)(jsdom@20.0.3(supports-color@8.1.1))(msw@2.11.3(@types/node@22.13.14)(typescript@5.9.2))(sass@1.77.4)(supports-color@8.1.1)(terser@5.39.0)(tsx@4.20.3)(yaml@2.8.1))
|
||||
'@vitest/ui':
|
||||
specifier: ^3.0.0
|
||||
version: 3.0.4(vitest@3.0.9)
|
||||
@@ -13389,6 +13401,10 @@ packages:
|
||||
highlight.js@10.7.3:
|
||||
resolution: {integrity: sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==}
|
||||
|
||||
highlight.js@11.11.1:
|
||||
resolution: {integrity: sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w==}
|
||||
engines: {node: '>=12.0.0'}
|
||||
|
||||
highlightjs-curl@1.3.0:
|
||||
resolution: {integrity: sha512-50UEfZq1KR0Lfk2Tr6xb/MUIZH3h10oNC0OTy9g7WELcs5Fgy/mKN1vEhuKTkKbdo8vr5F9GXstu2eLhApfQ3A==}
|
||||
|
||||
@@ -14497,6 +14513,9 @@ packages:
|
||||
lowlight@1.20.0:
|
||||
resolution: {integrity: sha512-8Ktj+prEb1RoCPkEOrPMYUN/nCggB7qAWe3a7OpMjWQkh3l2RD5wKRQ+o8Q8YuI9RG/xs95waaI/E6ym/7NsTw==}
|
||||
|
||||
lowlight@3.3.0:
|
||||
resolution: {integrity: sha512-0JNhgFoPvP6U6lE/UdVsSq99tn6DhjjpAj5MxG49ewd2mOBVtwWYIT8ClyABhq198aXXODMU6Ox8DrGy/CpTZQ==}
|
||||
|
||||
lru-cache@10.4.3:
|
||||
resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==}
|
||||
|
||||
@@ -16638,10 +16657,6 @@ packages:
|
||||
resolution: {integrity: sha512-t13BGPUlFDR7wRB5kQDG4jjl7XeuH6jbJGt11JHPL96qwsEHNX2+68tFXqc1/k+/jALsbSWJKUOT/hcYAZ5LkA==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
prismjs@1.29.0:
|
||||
resolution: {integrity: sha512-Kx/1w86q/epKcmte75LNrEoT+lX8pBpavuAbvJWRXar7Hz8jrtF+e3vY751p0R8H9HdArwaCTNDDzHg/ScJK1Q==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
prismjs@1.30.0:
|
||||
resolution: {integrity: sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw==}
|
||||
engines: {node: '>=6'}
|
||||
@@ -17311,6 +17326,9 @@ packages:
|
||||
rehype-harden@1.1.2:
|
||||
resolution: {integrity: sha512-58RSgd3BAYW/hULy6qvrLBIRe8qe5PElwEpRjrLilvhJ3N+Y6ptKAmy1CLIIyoMz7CMI30GqENhNXksJd5hGDg==}
|
||||
|
||||
rehype-highlight@7.0.2:
|
||||
resolution: {integrity: sha512-k158pK7wdC2qL3M5NcZROZ2tR/l7zOzjxXd5VGdcfIyoijjQqpHd3JKtYSBDpDZ38UI2WJWuFAtkMDxmx5kstA==}
|
||||
|
||||
rehype-katex@7.0.0:
|
||||
resolution: {integrity: sha512-h8FPkGE00r2XKU+/acgqwWUlyzve1IiOKwsEkg4pDL3k48PiE0Pt+/uLtVHDVkN1yA4iurZN6UES8ivHVEQV6Q==}
|
||||
|
||||
@@ -27790,7 +27808,7 @@ snapshots:
|
||||
|
||||
'@sentry/core@10.3.0': {}
|
||||
|
||||
'@sentry/nextjs@10.3.0(@opentelemetry/context-async-hooks@2.0.1(@opentelemetry/api@1.9.0))(@opentelemetry/core@2.0.1(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.0.1(@opentelemetry/api@1.9.0))(encoding@0.1.13)(next@15.5.2(@babel/core@7.28.4(supports-color@8.1.1))(@opentelemetry/api@1.9.0)(@playwright/test@1.53.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.77.4))(react@18.3.1)(supports-color@8.1.1)(webpack@5.94.0)':
|
||||
'@sentry/nextjs@10.3.0(@opentelemetry/context-async-hooks@2.0.1(@opentelemetry/api@1.9.0))(@opentelemetry/core@2.0.1(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.0.1(@opentelemetry/api@1.9.0))(encoding@0.1.13)(next@15.5.2(@opentelemetry/api@1.9.0)(@playwright/test@1.53.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.77.4))(react@18.3.1)(supports-color@8.1.1)(webpack@5.94.0)':
|
||||
dependencies:
|
||||
'@opentelemetry/api': 1.9.0
|
||||
'@opentelemetry/semantic-conventions': 1.36.0
|
||||
@@ -30262,24 +30280,6 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@vitest/coverage-v8@3.0.9(supports-color@8.1.1)(vitest@3.0.9)':
|
||||
dependencies:
|
||||
'@ampproject/remapping': 2.3.0
|
||||
'@bcoe/v8-coverage': 1.0.2
|
||||
debug: 4.4.3(supports-color@8.1.1)
|
||||
istanbul-lib-coverage: 3.2.2
|
||||
istanbul-lib-report: 3.0.1
|
||||
istanbul-lib-source-maps: 5.0.6(supports-color@8.1.1)
|
||||
istanbul-reports: 3.1.7
|
||||
magic-string: 0.30.19
|
||||
magicast: 0.3.5
|
||||
std-env: 3.9.0
|
||||
test-exclude: 7.0.1
|
||||
tinyrainbow: 2.0.0
|
||||
vitest: 3.0.9(@types/node@22.13.14)(@vitest/ui@3.0.4)(jiti@2.5.1)(jsdom@20.0.3(supports-color@8.1.1))(msw@2.4.11(typescript@5.9.2))(sass@1.77.4)(supports-color@8.1.1)(terser@5.39.0)(tsx@4.19.3)(yaml@2.8.1)
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@vitest/expect@3.0.9':
|
||||
dependencies:
|
||||
'@vitest/spy': 3.0.9
|
||||
@@ -30355,7 +30355,7 @@ snapshots:
|
||||
sirv: 3.0.0
|
||||
tinyglobby: 0.2.14
|
||||
tinyrainbow: 2.0.0
|
||||
vitest: 3.0.9(@types/node@22.13.14)(@vitest/ui@3.0.4)(jiti@2.5.1)(jsdom@20.0.3(supports-color@8.1.1))(msw@2.4.11(typescript@5.9.2))(sass@1.77.4)(supports-color@8.1.1)(terser@5.39.0)(tsx@4.19.3)(yaml@2.8.1)
|
||||
vitest: 3.0.9(@types/node@22.13.14)(@vitest/ui@3.0.4)(jiti@2.5.1)(jsdom@20.0.3(supports-color@8.1.1))(msw@2.11.3(@types/node@22.13.14)(typescript@5.9.2))(sass@1.77.4)(supports-color@8.1.1)(terser@5.39.0)(tsx@4.20.3)(yaml@2.8.1)
|
||||
|
||||
'@vitest/utils@3.0.4':
|
||||
dependencies:
|
||||
@@ -34378,6 +34378,8 @@ snapshots:
|
||||
|
||||
highlight.js@10.7.3: {}
|
||||
|
||||
highlight.js@11.11.1: {}
|
||||
|
||||
highlightjs-curl@1.3.0: {}
|
||||
|
||||
hoist-non-react-statics@3.3.2:
|
||||
@@ -35501,6 +35503,12 @@ snapshots:
|
||||
fault: 1.0.4
|
||||
highlight.js: 10.7.3
|
||||
|
||||
lowlight@3.3.0:
|
||||
dependencies:
|
||||
'@types/hast': 3.0.4
|
||||
devlop: 1.1.0
|
||||
highlight.js: 11.11.1
|
||||
|
||||
lru-cache@10.4.3: {}
|
||||
|
||||
lru-cache@11.0.1: {}
|
||||
@@ -38689,8 +38697,6 @@ snapshots:
|
||||
|
||||
prismjs@1.27.0: {}
|
||||
|
||||
prismjs@1.29.0: {}
|
||||
|
||||
prismjs@1.30.0: {}
|
||||
|
||||
proc-log@3.0.0: {}
|
||||
@@ -39238,7 +39244,7 @@ snapshots:
|
||||
'@babel/runtime': 7.26.10
|
||||
highlight.js: 10.7.3
|
||||
lowlight: 1.20.0
|
||||
prismjs: 1.29.0
|
||||
prismjs: 1.30.0
|
||||
react: 18.3.1
|
||||
refractor: 3.6.0
|
||||
|
||||
@@ -39505,6 +39511,14 @@ snapshots:
|
||||
|
||||
rehype-harden@1.1.2: {}
|
||||
|
||||
rehype-highlight@7.0.2:
|
||||
dependencies:
|
||||
'@types/hast': 3.0.4
|
||||
hast-util-to-text: 4.0.0
|
||||
lowlight: 3.3.0
|
||||
unist-util-visit: 5.0.0
|
||||
vfile: 6.0.3
|
||||
|
||||
rehype-katex@7.0.0:
|
||||
dependencies:
|
||||
'@types/hast': 3.0.4
|
||||
|
||||
Reference in new issue
Block a user