mirror of
https://github.com/supabase/supabase.git
synced 2026-10-12 21:05:06 +03:00
778 lines
29 KiB
TypeScript
778 lines
29 KiB
TypeScript
import { NextRequest, NextResponse } from 'next/server'
|
|
import { draftMode } from 'next/headers'
|
|
import { CMS_SITE_ORIGIN } from '~/lib/constants'
|
|
import { generateReadingTime } from '~/lib/helpers'
|
|
|
|
// Lightweight runtime for better performance
|
|
export const runtime = 'edge'
|
|
|
|
const corsHeaders = {
|
|
'Access-Control-Allow-Origin': '*',
|
|
'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type',
|
|
}
|
|
|
|
const cfHeaders = {
|
|
'CF-Access-Client-Id': process.env.CF_ACCESS_CLIENT_ID ?? '',
|
|
'CF-Access-Client-Secret': process.env.CF_ACCESS_CLIENT_SECRET ?? '',
|
|
}
|
|
|
|
// Lightweight TOC generation for edge runtime
|
|
type TocItem = { content: string; slug: string; lvl: number }
|
|
|
|
function generateTocFromMarkdown(markdown: string, maxDepth: number = 2) {
|
|
const lines = markdown.split(/\r?\n/)
|
|
const items: TocItem[] = []
|
|
|
|
for (const line of lines) {
|
|
const match = /^(#{1,6})\s+(.*)$/.exec(line)
|
|
if (!match) continue
|
|
const depth = match[1].length
|
|
if (depth > maxDepth) continue
|
|
const text = match[2].trim()
|
|
if (!text) continue
|
|
|
|
const slug = text
|
|
.trim()
|
|
.toLowerCase()
|
|
.replace(/[`~!@#$%^&*()+=|{}\[\]\\:\";'<>?,./]+/g, '')
|
|
.replace(/\s+/g, '-')
|
|
|
|
items.push({ content: text, slug, lvl: depth })
|
|
}
|
|
|
|
const content = items
|
|
.map((h) => `${' '.repeat(Math.max(0, h.lvl - 1))}- [${h.content}](#${h.slug})`)
|
|
.join('\n')
|
|
|
|
return { content, json: items }
|
|
}
|
|
|
|
// Minimal rich-text to plain text for reading time
|
|
function richTextToPlainText(content: any): string {
|
|
try {
|
|
const blocks = content?.root?.children
|
|
if (!Array.isArray(blocks)) return ''
|
|
const segments: string[] = []
|
|
for (const node of blocks) {
|
|
if (node?.type === 'heading') {
|
|
const text = Array.isArray(node.children)
|
|
? node.children.map((c: any) => c?.text || '').join('')
|
|
: ''
|
|
if (text) segments.push(text)
|
|
} else if (node?.type === 'paragraph') {
|
|
const text = Array.isArray(node.children)
|
|
? node.children.map((c: any) => c?.text || '').join('')
|
|
: ''
|
|
if (text) segments.push(text)
|
|
} else if (node?.type === 'list') {
|
|
const items = Array.isArray(node.children)
|
|
? node.children
|
|
.map((item: any) =>
|
|
Array.isArray(item?.children)
|
|
? item.children.map((c: any) => c?.text || '').join('')
|
|
: ''
|
|
)
|
|
.filter(Boolean)
|
|
: []
|
|
if (items.length > 0) segments.push(items.join(' '))
|
|
} else if (node?.type === 'link') {
|
|
const text = Array.isArray(node.children)
|
|
? node.children.map((c: any) => c?.text || '').join('')
|
|
: ''
|
|
if (text) segments.push(text)
|
|
}
|
|
}
|
|
return segments.join('\n')
|
|
} catch {
|
|
return ''
|
|
}
|
|
}
|
|
|
|
// Convert Payload rich text content to markdown
|
|
function convertRichTextToMarkdown(content: any): string {
|
|
if (!content?.root?.children) return ''
|
|
|
|
return content.root.children
|
|
.map((node: any) => {
|
|
if (node.type === 'heading') {
|
|
const level = node.tag && typeof node.tag === 'string' ? node.tag.replace('h', '') : '1'
|
|
const text = node.children?.map((child: any) => child.text).join('') || ''
|
|
return `${'#'.repeat(Number(level))} ${text}`
|
|
}
|
|
if (node.type === 'paragraph') {
|
|
return node.children?.map((child: any) => child.text).join('') || ''
|
|
}
|
|
if (node.type === 'list') {
|
|
const items = node.children
|
|
?.map((item: any) => {
|
|
if (item.type === 'list-item') {
|
|
return `- ${item.children?.map((child: any) => child.text).join('') || ''}`
|
|
}
|
|
return ''
|
|
})
|
|
.filter(Boolean)
|
|
.join('\n')
|
|
return items
|
|
}
|
|
if (node.type === 'link') {
|
|
const text = node.children?.map((child: any) => child.text).join('') || ''
|
|
const url = node.url || ''
|
|
return `[${text}](${url})`
|
|
}
|
|
return ''
|
|
})
|
|
.filter(Boolean)
|
|
.join('\n\n')
|
|
}
|
|
|
|
// Handle preflight requests
|
|
export async function OPTIONS() {
|
|
return new Response(null, {
|
|
status: 200,
|
|
headers: corsHeaders,
|
|
})
|
|
}
|
|
|
|
export async function GET(request: NextRequest) {
|
|
try {
|
|
const { searchParams } = new URL(request.url)
|
|
const mode = searchParams.get('mode') || 'preview' // 'preview' or 'full'
|
|
const limit = searchParams.get('limit') || '100'
|
|
const slug = searchParams.get('slug') // For fetching specific post
|
|
const draftParam = searchParams.get('draft') === 'true' // Explicit draft parameter
|
|
|
|
const baseUrl = CMS_SITE_ORIGIN
|
|
const apiKey = process.env.CMS_API_KEY
|
|
|
|
// Check if we're in draft mode (either Next.js draft mode OR explicit draft parameter)
|
|
const { isEnabled: isDraftMode } = await draftMode()
|
|
const shouldFetchDraft = isDraftMode || draftParam
|
|
|
|
// When fetching a specific post, we need to handle versioning correctly
|
|
if (slug) {
|
|
// If in draft mode, fetch the latest version (draft or published)
|
|
if (shouldFetchDraft) {
|
|
// Strategy 1: Try to get the latest version from versions API (including drafts)
|
|
const allVersionsUrl = new URL('/api/posts/versions', baseUrl)
|
|
allVersionsUrl.searchParams.set('where[version.slug][equals]', slug)
|
|
allVersionsUrl.searchParams.set('sort', '-updatedAt') // Get the most recent version regardless of status
|
|
allVersionsUrl.searchParams.set('limit', '1')
|
|
allVersionsUrl.searchParams.set('depth', '2')
|
|
|
|
const allVersionsResponse = await fetch(allVersionsUrl.toString(), {
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
...(apiKey ? { Authorization: `Bearer ${apiKey}` } : {}),
|
|
...cfHeaders,
|
|
},
|
|
cache: 'no-store',
|
|
})
|
|
|
|
if (allVersionsResponse.ok) {
|
|
const versionsData = await allVersionsResponse.json()
|
|
|
|
if (versionsData.docs && versionsData.docs.length > 0) {
|
|
const latestVersion = versionsData.docs[0].version
|
|
if (latestVersion) {
|
|
const markdownContent = convertRichTextToMarkdown(latestVersion.content)
|
|
const readingTime = generateReadingTime(richTextToPlainText(latestVersion.content))
|
|
|
|
const tocResult = generateTocFromMarkdown(
|
|
markdownContent,
|
|
latestVersion.toc_depth || 3
|
|
)
|
|
|
|
const processedPost = {
|
|
slug: latestVersion.slug || '',
|
|
title: latestVersion.title || '',
|
|
description: latestVersion.description || '',
|
|
date: latestVersion.date || new Date().toISOString(),
|
|
formattedDate: new Date(latestVersion.date || new Date()).toLocaleDateString(
|
|
'en-US',
|
|
{
|
|
month: 'long',
|
|
day: 'numeric',
|
|
year: 'numeric',
|
|
}
|
|
),
|
|
readingTime,
|
|
authors: Array.isArray(latestVersion.authors)
|
|
? latestVersion.authors.map((a: any) => ({
|
|
author: a?.author || 'Unknown Author',
|
|
author_id: a?.author_id || '',
|
|
position: a?.position || '',
|
|
author_url: a?.author_url || '#',
|
|
author_image_url: a?.author_image_url?.url
|
|
? typeof a.author_image_url.url === 'string' &&
|
|
a.author_image_url.url.includes('http')
|
|
? a.author_image_url.url
|
|
: `${baseUrl}${a.author_image_url.url}`
|
|
: null,
|
|
username: a?.username || '',
|
|
}))
|
|
: [],
|
|
thumb: latestVersion.thumb?.url
|
|
? `${baseUrl}${latestVersion.thumb.url}`
|
|
: undefined,
|
|
image: latestVersion.image?.url
|
|
? `${baseUrl}${latestVersion.image.url}`
|
|
: undefined,
|
|
meta: latestVersion.meta || null,
|
|
url: `/blog/${latestVersion.slug}`,
|
|
path: `/blog/${latestVersion.slug}`,
|
|
tags: latestVersion.tags || [],
|
|
categories: [],
|
|
isCMS: true,
|
|
content: markdownContent,
|
|
richContent: latestVersion.content,
|
|
toc: tocResult,
|
|
toc_depth: latestVersion.toc_depth || 3,
|
|
isDraft: true,
|
|
_status: latestVersion._status,
|
|
}
|
|
|
|
return NextResponse.json(
|
|
{
|
|
success: true,
|
|
post: processedPost,
|
|
mode,
|
|
isDraft: shouldFetchDraft,
|
|
},
|
|
{ headers: corsHeaders }
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Strategy 2: Try to get the most recent draft
|
|
const draftUrl = new URL('/api/posts', baseUrl)
|
|
draftUrl.searchParams.set('where[slug][equals]', slug)
|
|
draftUrl.searchParams.set('depth', '2')
|
|
draftUrl.searchParams.set('draft', 'true')
|
|
draftUrl.searchParams.set('sort', '-updatedAt') // Get the most recent version
|
|
|
|
const draftResponse = await fetch(draftUrl.toString(), {
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
...(apiKey ? { Authorization: `Bearer ${apiKey}` } : {}),
|
|
...cfHeaders,
|
|
},
|
|
cache: 'no-store', // Never cache draft content
|
|
})
|
|
|
|
if (draftResponse.ok) {
|
|
const draftData = await draftResponse.json()
|
|
|
|
if (draftData.docs && draftData.docs.length > 0) {
|
|
const post = draftData.docs[0]
|
|
const markdownContent = convertRichTextToMarkdown(post.content)
|
|
const readingTime = generateReadingTime(richTextToPlainText(post.content))
|
|
|
|
const tocResult = generateTocFromMarkdown(markdownContent, post.toc_depth || 3)
|
|
|
|
const processedPost = {
|
|
slug: post.slug || '',
|
|
title: post.title || '',
|
|
description: post.description || '',
|
|
date: post.date || new Date().toISOString(),
|
|
formattedDate: new Date(post.date || new Date()).toLocaleDateString('en-US', {
|
|
month: 'long',
|
|
day: 'numeric',
|
|
year: 'numeric',
|
|
}),
|
|
readingTime,
|
|
authors: Array.isArray(post.authors)
|
|
? post.authors.map((a: any) => ({
|
|
author: a?.author || 'Unknown Author',
|
|
author_id: a?.author_id || '',
|
|
position: a?.position || '',
|
|
author_url: a?.author_url || '#',
|
|
author_image_url: a?.author_image_url?.url
|
|
? typeof a.author_image_url.url === 'string' &&
|
|
a.author_image_url.url.includes('http')
|
|
? a.author_image_url.url
|
|
: `${baseUrl}${a.author_image_url.url}`
|
|
: null,
|
|
username: a?.username || '',
|
|
}))
|
|
: [],
|
|
thumb: post.thumb?.url ? `${baseUrl}${post.thumb.url}` : undefined,
|
|
image: post.image?.url ? `${baseUrl}${post.image.url}` : undefined,
|
|
meta: post.meta || null,
|
|
url: `/blog/${post.slug}`,
|
|
path: `/blog/${post.slug}`,
|
|
tags: post.tags || [],
|
|
categories: [],
|
|
isCMS: true,
|
|
content: markdownContent,
|
|
richContent: post.content,
|
|
toc: tocResult,
|
|
toc_depth: post.toc_depth || 3,
|
|
isDraft: true,
|
|
_status: post._status,
|
|
}
|
|
|
|
return NextResponse.json(
|
|
{
|
|
success: true,
|
|
post: processedPost,
|
|
mode,
|
|
isDraft: shouldFetchDraft,
|
|
},
|
|
{ headers: corsHeaders }
|
|
)
|
|
}
|
|
}
|
|
|
|
// Strategy 3: If no draft found, try published version but still mark as draft mode
|
|
|
|
const publishedUrl = new URL('/api/posts', baseUrl)
|
|
publishedUrl.searchParams.set('where[slug][equals]', slug)
|
|
publishedUrl.searchParams.set('depth', '2')
|
|
publishedUrl.searchParams.set('draft', 'false')
|
|
|
|
const publishedResponse = await fetch(publishedUrl.toString(), {
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
...(apiKey ? { Authorization: `Bearer ${apiKey}` } : {}),
|
|
...cfHeaders,
|
|
},
|
|
cache: 'no-store',
|
|
})
|
|
|
|
if (publishedResponse.ok) {
|
|
const publishedData = await publishedResponse.json()
|
|
if (publishedData.docs && publishedData.docs.length > 0) {
|
|
const post = publishedData.docs[0]
|
|
const markdownContent = convertRichTextToMarkdown(post.content)
|
|
const readingTime = generateReadingTime(richTextToPlainText(post.content))
|
|
|
|
const tocResult = generateTocFromMarkdown(markdownContent, post.toc_depth || 3)
|
|
|
|
const processedPost = {
|
|
slug: post.slug || '',
|
|
title: post.title || '',
|
|
description: post.description || '',
|
|
date: post.date || new Date().toISOString(),
|
|
formattedDate: new Date(post.date || new Date()).toLocaleDateString('en-US', {
|
|
month: 'long',
|
|
day: 'numeric',
|
|
year: 'numeric',
|
|
}),
|
|
readingTime,
|
|
authors: Array.isArray(post.authors)
|
|
? post.authors.map((a: any) => ({
|
|
author: a?.author || 'Unknown Author',
|
|
author_id: a?.author_id || '',
|
|
position: a?.position || '',
|
|
author_url: a?.author_url || '#',
|
|
author_image_url: a?.author_image_url?.url
|
|
? typeof a.author_image_url.url === 'string' &&
|
|
a.author_image_url.url.includes('http')
|
|
? a.author_image_url.url
|
|
: `${baseUrl}${a.author_image_url.url}`
|
|
: null,
|
|
username: a?.username || '',
|
|
}))
|
|
: [],
|
|
thumb: post.thumb?.url ? `${baseUrl}${post.thumb.url}` : undefined,
|
|
image: post.image?.url ? `${baseUrl}${post.image.url}` : undefined,
|
|
meta: post.meta || null,
|
|
url: `/blog/${post.slug}`,
|
|
path: `/blog/${post.slug}`,
|
|
tags: post.tags || [],
|
|
categories: [],
|
|
isCMS: true,
|
|
content: markdownContent,
|
|
richContent: post.content,
|
|
toc: tocResult,
|
|
toc_depth: post.toc_depth || 3,
|
|
isDraft: true,
|
|
_status: post._status,
|
|
}
|
|
|
|
return NextResponse.json(
|
|
{
|
|
success: true,
|
|
post: processedPost,
|
|
mode,
|
|
isDraft: shouldFetchDraft,
|
|
},
|
|
{ headers: corsHeaders }
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Strategy 1: Try to get the latest published version using the versions API
|
|
const versionsUrl = new URL('/api/posts/versions', baseUrl)
|
|
versionsUrl.searchParams.set('where[version.slug][equals]', slug)
|
|
versionsUrl.searchParams.set('where[version._status][equals]', 'published')
|
|
versionsUrl.searchParams.set('sort', '-createdAt') // Use createdAt instead of updatedAt for versions
|
|
versionsUrl.searchParams.set('limit', '1')
|
|
versionsUrl.searchParams.set('depth', '2')
|
|
|
|
const versionsResponse = await fetch(versionsUrl.toString(), {
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
...(apiKey ? { Authorization: `Bearer ${apiKey}` } : {}),
|
|
...cfHeaders,
|
|
},
|
|
// For published posts: allow caching with revalidation
|
|
next: { revalidate: 60 }, // 1 minute
|
|
})
|
|
|
|
if (versionsResponse.ok) {
|
|
const versionsData = await versionsResponse.json()
|
|
|
|
if (versionsData.docs && versionsData.docs.length > 0) {
|
|
const latestPublishedVersion = versionsData.docs[0].version
|
|
|
|
if (latestPublishedVersion) {
|
|
const post = latestPublishedVersion
|
|
const markdownContent = convertRichTextToMarkdown(post.content)
|
|
const plain = richTextToPlainText(post.content)
|
|
const readingTime = generateReadingTime(plain)
|
|
const tocResult = generateTocFromMarkdown(markdownContent, post.toc_depth || 3)
|
|
|
|
const processedPost = {
|
|
type: 'blog' as const,
|
|
slug: post.slug,
|
|
title: post.title || '',
|
|
description: post.description || '',
|
|
date: post.date || post.createdAt || new Date().toISOString(),
|
|
formattedDate: new Date(post.date || post.createdAt || new Date()).toLocaleDateString(
|
|
'en-IN',
|
|
{ month: 'long', day: 'numeric', year: 'numeric' }
|
|
),
|
|
readingTime,
|
|
authors: Array.isArray(post.authors)
|
|
? post.authors.map((a: any) => ({
|
|
author: a?.author || 'Unknown Author',
|
|
author_id: a?.author_id || '',
|
|
position: a?.position || '',
|
|
author_url: a?.author_url || '#',
|
|
author_image_url: a?.author_image_url?.url
|
|
? typeof a.author_image_url.url === 'string' &&
|
|
a.author_image_url.url.includes('http')
|
|
? a.author_image_url.url
|
|
: `${baseUrl}${a.author_image_url.url}`
|
|
: null,
|
|
username: a?.username || '',
|
|
}))
|
|
: [],
|
|
thumb: post.thumb?.url
|
|
? typeof post.thumb.url === 'string' && post.thumb.url.includes('http')
|
|
? post.thumb.url
|
|
: `${baseUrl}${post.thumb.url}`
|
|
: '',
|
|
image: post.image?.url
|
|
? typeof post.image.url === 'string' && post.image.url.includes('http')
|
|
? post.image.url
|
|
: `${baseUrl}${post.image.url}`
|
|
: undefined,
|
|
meta: post.meta || null,
|
|
url: `/blog/${post.slug}`,
|
|
path: `/blog/${post.slug}`,
|
|
tags: post.tags || [],
|
|
categories: [],
|
|
isCMS: true,
|
|
content: mode === 'full' ? markdownContent : undefined,
|
|
richContent: mode === 'full' ? post.content : undefined,
|
|
toc: tocResult,
|
|
toc_depth: post.toc_depth || 3,
|
|
}
|
|
|
|
return NextResponse.json(
|
|
{
|
|
success: true,
|
|
post: processedPost,
|
|
mode,
|
|
source: 'versions-api',
|
|
},
|
|
{ headers: corsHeaders }
|
|
)
|
|
}
|
|
}
|
|
} else {
|
|
if (process.env.NODE_ENV !== 'production') {
|
|
console.log('[cms-posts] Versions API failed, response:', await versionsResponse.text())
|
|
}
|
|
}
|
|
|
|
// Strategy 2: If versions API didn't work, try finding the parent post first, then get its latest published version
|
|
|
|
const parentUrl = new URL('/api/posts', baseUrl)
|
|
parentUrl.searchParams.set('where[slug][equals]', slug)
|
|
parentUrl.searchParams.set('limit', '1')
|
|
parentUrl.searchParams.set('depth', '0') // Just get the ID
|
|
|
|
const parentResponse = await fetch(parentUrl.toString(), {
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
...(apiKey ? { Authorization: `Bearer ${apiKey}` } : {}),
|
|
...cfHeaders,
|
|
},
|
|
next: { revalidate: 60 }, // 1 minute for published posts
|
|
})
|
|
|
|
if (parentResponse.ok) {
|
|
const parentData = await parentResponse.json()
|
|
if (parentData.docs && parentData.docs.length > 0) {
|
|
const parentId = parentData.docs[0].id
|
|
|
|
// Now get the latest published version of this specific post
|
|
const versionsByParentUrl = new URL('/api/posts/versions', baseUrl)
|
|
versionsByParentUrl.searchParams.set('where[parent][equals]', parentId)
|
|
versionsByParentUrl.searchParams.set('where[version._status][equals]', 'published')
|
|
versionsByParentUrl.searchParams.set('sort', '-createdAt')
|
|
versionsByParentUrl.searchParams.set('limit', '1')
|
|
versionsByParentUrl.searchParams.set('depth', '2')
|
|
|
|
const versionsByParentResponse = await fetch(versionsByParentUrl.toString(), {
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
...(apiKey ? { Authorization: `Bearer ${apiKey}` } : {}),
|
|
...cfHeaders,
|
|
},
|
|
next: { revalidate: 60 }, // 1 minute for published posts
|
|
})
|
|
|
|
if (versionsByParentResponse.ok) {
|
|
const versionsByParentData = await versionsByParentResponse.json()
|
|
|
|
if (versionsByParentData.docs && versionsByParentData.docs.length > 0) {
|
|
const latestPublishedVersion = versionsByParentData.docs[0].version
|
|
|
|
if (latestPublishedVersion) {
|
|
const post = latestPublishedVersion
|
|
const markdownContent = convertRichTextToMarkdown(post.content)
|
|
const plain = richTextToPlainText(post.content)
|
|
const readingTime = generateReadingTime(plain)
|
|
|
|
const processedPost = {
|
|
type: 'blog' as const,
|
|
slug: post.slug,
|
|
title: post.title || '',
|
|
description: post.description || '',
|
|
date: post.date || post.createdAt || new Date().toISOString(),
|
|
formattedDate: new Date(
|
|
post.date || post.createdAt || new Date()
|
|
).toLocaleDateString('en-IN', { month: 'long', day: 'numeric', year: 'numeric' }),
|
|
readingTime,
|
|
authors: Array.isArray(post.authors)
|
|
? post.authors.map((a: any) => ({
|
|
author: a?.author || 'Unknown Author',
|
|
author_id: a?.author_id || '',
|
|
position: a?.position || '',
|
|
author_url: a?.author_url || '#',
|
|
author_image_url: a?.author_image_url?.url
|
|
? typeof a.author_image_url.url === 'string' &&
|
|
a.author_image_url.url.includes('http')
|
|
? a.author_image_url.url
|
|
: `${baseUrl}${a.author_image_url.url}`
|
|
: null,
|
|
username: a?.username || '',
|
|
}))
|
|
: [],
|
|
thumb: post.thumb?.url
|
|
? typeof post.thumb.url === 'string' && post.thumb.url.includes('http')
|
|
? post.thumb.url
|
|
: `${baseUrl}${post.thumb.url}`
|
|
: '',
|
|
image: post.image?.url
|
|
? typeof post.image.url === 'string' && post.image.url.includes('http')
|
|
? post.image.url
|
|
: `${baseUrl}${post.image.url}`
|
|
: undefined,
|
|
url: `/blog/${post.slug}`,
|
|
path: `/blog/${post.slug}`,
|
|
tags: post.tags || [],
|
|
categories: [],
|
|
isCMS: true,
|
|
content: mode === 'full' ? markdownContent : undefined,
|
|
richContent: mode === 'full' ? post.content : undefined,
|
|
}
|
|
|
|
return NextResponse.json(
|
|
{
|
|
success: true,
|
|
post: processedPost,
|
|
mode,
|
|
source: 'versions-by-parent-api',
|
|
},
|
|
{ headers: corsHeaders }
|
|
)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Fallback to regular posts API for listing or if versions API fails
|
|
const url = new URL('/api/posts', baseUrl)
|
|
url.searchParams.set('depth', '2')
|
|
url.searchParams.set('draft', 'false')
|
|
url.searchParams.set('limit', limit)
|
|
url.searchParams.set('where[_status][equals]', 'published')
|
|
|
|
// If fetching specific post by slug (fallback)
|
|
if (slug) {
|
|
url.searchParams.set('where[slug][equals]', slug)
|
|
url.searchParams.set('limit', '1')
|
|
}
|
|
|
|
const response = await fetch(url.toString(), {
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
...(apiKey ? { Authorization: `Bearer ${apiKey}` } : {}),
|
|
...cfHeaders,
|
|
},
|
|
// For individual post requests, don't cache to ensure fresh data
|
|
cache: slug ? 'no-store' : 'default',
|
|
next: slug ? undefined : { revalidate: 300 },
|
|
// Add SSL configuration for production
|
|
...(process.env.NODE_ENV === 'production' && {
|
|
// Allow self-signed certificates in development, but use proper SSL in production
|
|
// This helps with Vercel's internal networking
|
|
agent: false,
|
|
}),
|
|
})
|
|
|
|
if (!response.ok) {
|
|
console.error('[cms-posts] Non-OK response:', response.status, response.statusText)
|
|
return NextResponse.json(
|
|
{
|
|
success: false,
|
|
error: 'Failed to fetch posts from CMS',
|
|
status: response.status,
|
|
},
|
|
{ status: response.status, headers: corsHeaders }
|
|
)
|
|
}
|
|
|
|
const contentType = response.headers.get('content-type') || ''
|
|
if (!contentType.toLowerCase().includes('application/json')) {
|
|
console.error('[cms-posts] Non-JSON response, content-type:', contentType)
|
|
return NextResponse.json(
|
|
{
|
|
success: false,
|
|
error: 'CMS returned non-JSON response',
|
|
contentType,
|
|
},
|
|
{ status: 502, headers: corsHeaders }
|
|
)
|
|
}
|
|
|
|
const data = await response.json()
|
|
const docs = Array.isArray(data?.docs) ? data.docs : []
|
|
|
|
const dateFmt: Intl.DateTimeFormatOptions = { month: 'long', day: 'numeric', year: 'numeric' }
|
|
const posts = docs
|
|
.filter((p: any) => !!p?.slug)
|
|
.map((p: any) => {
|
|
const thumbUrl = p?.thumb?.url
|
|
? typeof p.thumb.url === 'string' && p.thumb.url.includes('http')
|
|
? p.thumb.url
|
|
: `${baseUrl}${p.thumb.url}`
|
|
: ''
|
|
const imageUrl = p?.image?.url
|
|
? typeof p.image.url === 'string' && p.image.url.includes('http')
|
|
? p.image.url
|
|
: `${baseUrl}${p.image.url}`
|
|
: ''
|
|
const date = p.date || p.createdAt || new Date().toISOString()
|
|
const formattedDate = new Date(date).toLocaleDateString('en-IN', dateFmt)
|
|
const plain = richTextToPlainText(p?.content)
|
|
const readingTime = generateReadingTime(plain)
|
|
|
|
const authors = Array.isArray(p?.authors)
|
|
? p.authors.map((a: any) => ({
|
|
author: a?.author || 'Unknown Author',
|
|
author_id: a?.author_id || '',
|
|
position: a?.position || '',
|
|
author_url: a?.author_url || '#',
|
|
author_image_url: a?.author_image_url?.url
|
|
? typeof a.author_image_url.url === 'string' &&
|
|
a.author_image_url.url.includes('http')
|
|
? a.author_image_url.url
|
|
: `${baseUrl}${a.author_image_url.url}`
|
|
: null,
|
|
username: a?.username || '',
|
|
}))
|
|
: []
|
|
|
|
// Base post structure (always included)
|
|
const basePost = {
|
|
type: 'blog' as const,
|
|
slug: p.slug,
|
|
title: p.title || '',
|
|
description: p.description || '',
|
|
date,
|
|
formattedDate,
|
|
readingTime,
|
|
authors,
|
|
thumb: thumbUrl || imageUrl || '',
|
|
image: imageUrl || undefined,
|
|
meta: p.meta || null,
|
|
url: `/blog/${p.slug}`,
|
|
path: `/blog/${p.slug}`,
|
|
tags: p.tags || [],
|
|
categories: [],
|
|
isCMS: true,
|
|
toc_depth: p.toc_depth || 3,
|
|
}
|
|
|
|
// Add content for full mode
|
|
if (mode === 'full') {
|
|
const markdownContent = convertRichTextToMarkdown(p.content)
|
|
const tocResult = generateTocFromMarkdown(markdownContent, p.toc_depth || 3)
|
|
return {
|
|
...basePost,
|
|
content: markdownContent, // Convert rich text to markdown for MDX processing
|
|
richContent: p.content, // Keep original rich text for reference
|
|
toc: tocResult,
|
|
toc_depth: p.toc_depth || 3,
|
|
}
|
|
}
|
|
|
|
return basePost
|
|
})
|
|
.sort((a: any, b: any) => new Date(b.date).getTime() - new Date(a.date).getTime())
|
|
|
|
// For single post requests, return the post directly
|
|
if (slug && posts.length > 0) {
|
|
return NextResponse.json(
|
|
{
|
|
success: true,
|
|
post: posts[0],
|
|
mode,
|
|
},
|
|
{ headers: corsHeaders }
|
|
)
|
|
}
|
|
|
|
return NextResponse.json(
|
|
{
|
|
success: true,
|
|
posts,
|
|
total: posts.length,
|
|
mode,
|
|
cached: true,
|
|
},
|
|
{ headers: corsHeaders }
|
|
)
|
|
} catch (error) {
|
|
console.error('[cms-posts] Error:', error)
|
|
return NextResponse.json(
|
|
{
|
|
success: false,
|
|
error: 'Internal server error',
|
|
message: error instanceof Error ? error.message : 'Unknown error',
|
|
},
|
|
{ status: 500, headers: corsHeaders }
|
|
)
|
|
}
|
|
}
|