- e.preventDefault()} tabindex="-1">Continue reading + Continue reading
{/if} diff --git a/src/lib/utils/content.ts b/src/lib/utils/content.ts index c38203d..248a5f7 100644 --- a/src/lib/utils/content.ts +++ b/src/lib/utils/content.ts @@ -2,6 +2,11 @@ export const renderEdraContent = (content: any): string => { if (!content) return '' + // Handle Tiptap format first (has type: 'doc') + if (content.type === 'doc' && content.content) { + return renderTiptapContent(content) + } + // Handle both { blocks: [...] } and { content: [...] } formats const blocks = content.blocks || content.content || [] if (!Array.isArray(blocks)) return '' @@ -79,9 +84,152 @@ export const renderEdraContent = (content: any): string => { return blocks.map(renderBlock).join('') } +// Render Tiptap JSON content to HTML +function renderTiptapContent(doc: any): string { + if (!doc || !doc.content) return '' + + const renderNode = (node: any): string => { + switch (node.type) { + case 'paragraph': { + const content = renderInlineContent(node.content || []) + if (!content) return '${content}
` + } + + case 'heading': { + const level = node.attrs?.level || 1 + const content = renderInlineContent(node.content || []) + return `${content}` + } + + case 'codeBlock': { + const language = node.attrs?.language || '' + const content = node.content?.[0]?.text || '' + return `
${escapeHtml(content)}`
+ }
+
+ case 'image': {
+ const src = node.attrs?.src || ''
+ const alt = node.attrs?.alt || ''
+ const title = node.attrs?.title || ''
+ const width = node.attrs?.width
+ const height = node.attrs?.height
+ const widthAttr = width ? ` width="${width}"` : ''
+ const heightAttr = height ? ` height="${height}"` : ''
+ return `${text}`
+ break
+ case 'link':
+ const href = mark.attrs?.href || '#'
+ const target = mark.attrs?.target || '_blank'
+ text = `${text}`
+ break
+ case 'highlight':
+ text = `${text}`
+ break
+ }
+ })
+ }
+
+ return text
+ }
+
+ // Handle other inline nodes
+ return renderNode(node)
+ }).join('')
+ }
+
+ // Helper to escape HTML
+ const escapeHtml = (str: string): string => {
+ const div = document.createElement('div')
+ div.textContent = str
+ return div.innerHTML
+ }
+
+ return doc.content.map(renderNode).join('')
+}
+
// Extract text content from Edra JSON for excerpt
export const getContentExcerpt = (content: any, maxLength = 200): string => {
if (!content) return ''
+
+ // Handle Tiptap format first (has type: 'doc')
+ if (content.type === 'doc' && content.content) {
+ return extractTiptapText(content, maxLength)
+ }
// Handle both { blocks: [...] } and { content: [...] } formats
const blocks = content.blocks || content.content || []
@@ -105,3 +253,22 @@ export const getContentExcerpt = (content: any, maxLength = 200): string => {
if (text.length <= maxLength) return text
return text.substring(0, maxLength).trim() + '...'
}
+
+// Extract text from Tiptap content
+function extractTiptapText(doc: any, maxLength: number): string {
+ const extractFromNode = (node: any): string => {
+ if (node.type === 'text') {
+ return node.text || ''
+ }
+
+ if (node.content && Array.isArray(node.content)) {
+ return node.content.map(extractFromNode).join(' ')
+ }
+
+ return ''
+ }
+
+ const text = doc.content.map(extractFromNode).join(' ').trim()
+ if (text.length <= maxLength) return text
+ return text.substring(0, maxLength).trim() + '...'
+}
diff --git a/src/routes/admin/posts/[id]/edit/+page.svelte b/src/routes/admin/posts/[id]/edit/+page.svelte
index 66d641a..efdd953 100644
--- a/src/routes/admin/posts/[id]/edit/+page.svelte
+++ b/src/routes/admin/posts/[id]/edit/+page.svelte
@@ -35,6 +35,179 @@
let config = $derived(postTypeConfig[postType])
+ // Convert blocks format (from database) to Tiptap format
+ function convertBlocksToTiptap(blocksContent: any): JSONContent {
+ if (!blocksContent || !blocksContent.blocks) {
+ return { type: 'doc', content: [] }
+ }
+
+ const tiptapContent = blocksContent.blocks.map((block: any) => {
+ switch (block.type) {
+ case 'paragraph':
+ return {
+ type: 'paragraph',
+ content: block.content ? [{ type: 'text', text: block.content }] : []
+ }
+
+ case 'heading':
+ return {
+ type: 'heading',
+ attrs: { level: block.level || 1 },
+ content: block.content ? [{ type: 'text', text: block.content }] : []
+ }
+
+ case 'bulletList':
+ case 'ul':
+ return {
+ type: 'bulletList',
+ content: (block.content || []).map((item: any) => ({
+ type: 'listItem',
+ content: [{
+ type: 'paragraph',
+ content: [{ type: 'text', text: item.content || item }]
+ }]
+ }))
+ }
+
+ case 'orderedList':
+ case 'ol':
+ return {
+ type: 'orderedList',
+ content: (block.content || []).map((item: any) => ({
+ type: 'listItem',
+ content: [{
+ type: 'paragraph',
+ content: [{ type: 'text', text: item.content || item }]
+ }]
+ }))
+ }
+
+ case 'blockquote':
+ return {
+ type: 'blockquote',
+ content: [{
+ type: 'paragraph',
+ content: [{ type: 'text', text: block.content || '' }]
+ }]
+ }
+
+ case 'codeBlock':
+ case 'code':
+ return {
+ type: 'codeBlock',
+ attrs: { language: block.language || '' },
+ content: [{ type: 'text', text: block.content || '' }]
+ }
+
+ case 'image':
+ return {
+ type: 'image',
+ attrs: {
+ src: block.src || '',
+ alt: block.alt || '',
+ title: block.caption || ''
+ }
+ }
+
+ case 'hr':
+ case 'horizontalRule':
+ return { type: 'horizontalRule' }
+
+ default:
+ // Default to paragraph for unknown types
+ return {
+ type: 'paragraph',
+ content: block.content ? [{ type: 'text', text: block.content }] : []
+ }
+ }
+ })
+
+ return {
+ type: 'doc',
+ content: tiptapContent
+ }
+ }
+
+ // Convert Tiptap format back to blocks format for saving
+ function convertTiptapToBlocks(tiptapContent: JSONContent): any {
+ if (!tiptapContent || !tiptapContent.content) {
+ return { blocks: [] }
+ }
+
+ const blocks = tiptapContent.content.map((node: any) => {
+ switch (node.type) {
+ case 'paragraph':
+ const text = extractTextFromNode(node)
+ return text ? { type: 'paragraph', content: text } : null
+
+ case 'heading':
+ return {
+ type: 'heading',
+ level: node.attrs?.level || 1,
+ content: extractTextFromNode(node)
+ }
+
+ case 'bulletList':
+ return {
+ type: 'bulletList',
+ content: node.content?.map((item: any) => {
+ const itemText = extractTextFromNode(item.content?.[0])
+ return itemText
+ }).filter(Boolean) || []
+ }
+
+ case 'orderedList':
+ return {
+ type: 'orderedList',
+ content: node.content?.map((item: any) => {
+ const itemText = extractTextFromNode(item.content?.[0])
+ return itemText
+ }).filter(Boolean) || []
+ }
+
+ case 'blockquote':
+ return {
+ type: 'blockquote',
+ content: extractTextFromNode(node.content?.[0])
+ }
+
+ case 'codeBlock':
+ return {
+ type: 'codeBlock',
+ language: node.attrs?.language || '',
+ content: node.content?.[0]?.text || ''
+ }
+
+ case 'image':
+ return {
+ type: 'image',
+ src: node.attrs?.src || '',
+ alt: node.attrs?.alt || '',
+ caption: node.attrs?.title || ''
+ }
+
+ case 'horizontalRule':
+ return { type: 'hr' }
+
+ default:
+ // Skip unknown types
+ return null
+ }
+ }).filter(Boolean)
+
+ return { blocks }
+ }
+
+ // Helper function to extract text from a node
+ function extractTextFromNode(node: any): string {
+ if (!node) return ''
+ if (node.text) return node.text
+ if (node.content && Array.isArray(node.content)) {
+ return node.content.map((n: any) => extractTextFromNode(n)).join('')
+ }
+ return ''
+ }
+
onMount(async () => {
// Wait a tick to ensure page params are loaded
await new Promise((resolve) => setTimeout(resolve, 0))
@@ -71,7 +244,16 @@
status = post.status || 'draft'
slug = post.slug || ''
excerpt = post.excerpt || ''
- content = post.content || { type: 'doc', content: [] }
+
+ // Convert blocks format to Tiptap format if needed
+ if (post.content && post.content.blocks) {
+ content = convertBlocksToTiptap(post.content)
+ } else if (post.content && post.content.type === 'doc') {
+ content = post.content
+ } else {
+ content = { type: 'doc', content: [] }
+ }
+
tags = post.tags || []
} else {
if (response.status === 404) {
@@ -109,12 +291,19 @@
}
saving = true
+
+ // Convert content to blocks format if it's in Tiptap format
+ let saveContent = content
+ if (config?.showContent && content && content.type === 'doc') {
+ saveContent = convertTiptapToBlocks(content)
+ }
+
const postData = {
title: config?.showTitle ? title : null,
slug,
type: postType,
status: publishStatus || status,
- content: config?.showContent ? content : null,
+ content: config?.showContent ? saveContent : null,
excerpt: postType === 'essay' ? excerpt : undefined,
link_url: undefined,
linkDescription: undefined,
diff --git a/src/routes/universe/[slug]/+page.svelte b/src/routes/universe/[slug]/+page.svelte
index 2a96862..098c766 100644
--- a/src/routes/universe/[slug]/+page.svelte
+++ b/src/routes/universe/[slug]/+page.svelte
@@ -2,6 +2,7 @@
import Page from '$components/Page.svelte'
import DynamicPostContent from '$components/DynamicPostContent.svelte'
import { getContentExcerpt } from '$lib/utils/content'
+ import { goto } from '$app/navigation'
import type { PageData } from './$types'
let { data }: { data: PageData } = $props()
@@ -10,24 +11,20 @@
const error = $derived(data.error)
const pageTitle = $derived(post?.title || 'Post')
const description = $derived(
- post?.content ? getContentExcerpt(post.content, 160) : `${post?.postType === 'essay' ? 'Essay' : 'Post'} by jedmund`
+ post?.content
+ ? getContentExcerpt(post.content, 160)
+ : `${post?.postType === 'essay' ? 'Essay' : 'Post'} by jedmund`
)