From 21d7ac05186735b4c869f254a1cef7bc5ec93c0d Mon Sep 17 00:00:00 2001 From: Ivan Vasilov Date: Wed, 11 Feb 2026 11:40:57 +0100 Subject: [PATCH 1/5] chore: Remove CMS code from the `www` app (#42648) This PR removes all CMS code from the `www` app. This includes fetching of blog posts, API routes for proxying blog posts and types. All functionality should remain the same (and the number of blog posts should be the same). ## Summary by CodeRabbit * **Chores** * Removed CMS integration, APIs, preview/draft/revalidate endpoints, related env vars and dependency; switched to static markdown-only blog pipeline. * **Refactor** * Simplified image and author resolution, tightened component props to static post shapes, and migrated imports to path aliases. * **Documentation** * Deleted CMS integration docs and rich-text conversion helpers. --- apps/www/.env.local.example | 2 - apps/www/.vercelignore | 1 - apps/www/README.md | 4 - apps/www/app/api-v2/blog-posts/route.ts | 9 +- apps/www/app/api-v2/cms-posts/route.ts | 777 ------------------ .../www/app/api-v2/cms/disable-draft/route.ts | 27 - apps/www/app/api-v2/cms/preview/route.ts | 47 -- apps/www/app/api-v2/cms/revalidate/route.ts | 32 - apps/www/app/blog/[slug]/BlogPostClient.tsx | 158 +--- apps/www/app/blog/[slug]/page.tsx | 218 +---- apps/www/app/blog/authors/[author]/page.tsx | 17 +- .../app/blog/categories/[category]/page.tsx | 17 +- apps/www/app/blog/page.tsx | 13 +- apps/www/app/blog/tags/[tag]/page.tsx | 14 +- apps/www/components/Blog/BlogGridItem.tsx | 15 +- apps/www/components/Blog/BlogListItem.tsx | 26 +- apps/www/components/Blog/BlogPostRenderer.tsx | 78 +- apps/www/components/Blog/DraftModeBanner.tsx | 10 - apps/www/components/Blog/FeaturedThumb.tsx | 44 +- apps/www/internals/generate-sitemap.mjs | 76 +- apps/www/lib/cms/README.md | 85 -- apps/www/lib/cms/convertRichTextToMarkdown.ts | 270 ------ apps/www/lib/cms/processCMSContent.ts | 82 -- apps/www/lib/constants.ts | 9 - apps/www/lib/get-cms-posts.tsx | 596 -------------- apps/www/lib/remotePatterns.js | 48 -- apps/www/next.config.mjs | 16 +- apps/www/package.json | 3 +- apps/www/scripts/generateStaticContent.mjs | 89 +- apps/www/types/post.ts | 39 +- pnpm-lock.yaml | 9 +- 31 files changed, 115 insertions(+), 2716 deletions(-) delete mode 100644 apps/www/app/api-v2/cms-posts/route.ts delete mode 100644 apps/www/app/api-v2/cms/disable-draft/route.ts delete mode 100644 apps/www/app/api-v2/cms/preview/route.ts delete mode 100644 apps/www/app/api-v2/cms/revalidate/route.ts delete mode 100644 apps/www/lib/cms/README.md delete mode 100644 apps/www/lib/cms/convertRichTextToMarkdown.ts delete mode 100644 apps/www/lib/cms/processCMSContent.ts delete mode 100644 apps/www/lib/get-cms-posts.tsx diff --git a/apps/www/.env.local.example b/apps/www/.env.local.example index d6fee8a9b0a1e..7aeebf4c668b8 100644 --- a/apps/www/.env.local.example +++ b/apps/www/.env.local.example @@ -13,5 +13,3 @@ SUPABASE_COM_SERVICE_ROLE_KEY="secret" NEXT_PUBLIC_URL="http://localhost:3000" NEXT_PUBLIC_HCAPTCHA_SITE_KEY="10000000-ffff-ffff-ffff-000000000001" HCAPTCHA_SECRET_KEY="0x0000000000000000000000000000000000000000" -CMS_API_KEY=secret -CMS_PREVIEW_SECRET=secret diff --git a/apps/www/.vercelignore b/apps/www/.vercelignore index a0cfdb5c6873c..30470ae55a661 100644 --- a/apps/www/.vercelignore +++ b/apps/www/.vercelignore @@ -60,7 +60,6 @@ Thumbs.db **/node_modules/framer-motion # Other apps (since we're only deploying www) -../cms ../design-system ../docs ../studio diff --git a/apps/www/README.md b/apps/www/README.md index 44122a4176b52..de8fd24414337 100644 --- a/apps/www/README.md +++ b/apps/www/README.md @@ -65,10 +65,6 @@ For social sharing (Open Graph meta tags): - Priority 2: `imgThumb` (if `imgSocial` is missing) - Priority 3: No fallback (undefined) -Special case for CMS posts: - -- CMS posts may also have a `meta.image` field which takes highest priority for social sharing when available. - What happens if fields are not provided? - If only `imgThumb` is provided: Site displays the image correctly, social sharing uses `imgThumb` as fallback diff --git a/apps/www/app/api-v2/blog-posts/route.ts b/apps/www/app/api-v2/blog-posts/route.ts index 2ac79b060e1c2..72576b9df557d 100644 --- a/apps/www/app/api-v2/blog-posts/route.ts +++ b/apps/www/app/api-v2/blog-posts/route.ts @@ -1,6 +1,6 @@ import { NextRequest, NextResponse } from 'next/server' -import { getSortedPosts } from 'lib/posts' -import { getAllCMSPosts } from 'lib/get-cms-posts' + +import { getSortedPosts } from '@/lib/posts' export const revalidate = 30 @@ -20,11 +20,8 @@ async function getCombinedPosts() { // Get static blog posts const staticPosts = getSortedPosts({ directory: '_blog' }) - // Get CMS posts - const cmsPosts = await getAllCMSPosts({ limit: 100 }) - // Combine and sort by date - const allPosts = [...staticPosts, ...cmsPosts].sort((a: any, b: any) => { + const allPosts = [...staticPosts].sort((a: any, b: any) => { const dateA = new Date(a.date || a.formattedDate).getTime() const dateB = new Date(b.date || b.formattedDate).getTime() return dateB - dateA diff --git a/apps/www/app/api-v2/cms-posts/route.ts b/apps/www/app/api-v2/cms-posts/route.ts deleted file mode 100644 index d8cdc7382f898..0000000000000 --- a/apps/www/app/api-v2/cms-posts/route.ts +++ /dev/null @@ -1,777 +0,0 @@ -import { draftMode } from 'next/headers' -import { NextRequest, NextResponse } from 'next/server' -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 || '', - })) - : [], - imgThumb: latestVersion.imgThumb?.url - ? `${baseUrl}${latestVersion.imgThumb.url}` - : undefined, - imgSocial: latestVersion.imgSocial?.url - ? `${baseUrl}${latestVersion.imgSocial.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 || '', - })) - : [], - imgThumb: post.imgThumb?.url ? `${baseUrl}${post.imgThumb.url}` : undefined, - imgSocial: post.imgSocial?.url ? `${baseUrl}${post.imgSocial.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 || '', - })) - : [], - imgThumb: post.imgThumb?.url ? `${baseUrl}${post.imgThumb.url}` : undefined, - imgSocial: post.imgSocial?.url ? `${baseUrl}${post.imgSocial.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 || '', - })) - : [], - imgThumb: post.imgThumb?.url - ? typeof post.imgThumb.url === 'string' && post.imgThumb.url.includes('http') - ? post.imgThumb.url - : `${baseUrl}${post.imgThumb.url}` - : '', - imgSocial: post.imgSocial?.url - ? typeof post.imgSocial.url === 'string' && post.imgSocial.url.includes('http') - ? post.imgSocial.url - : `${baseUrl}${post.imgSocial.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 || '', - })) - : [], - imgThumb: post.imgThumb?.url - ? typeof post.imgThumb.url === 'string' && post.imgThumb.url.includes('http') - ? post.imgThumb.url - : `${baseUrl}${post.imgThumb.url}` - : '', - imgSocial: post.imgSocial?.url - ? typeof post.imgSocial.url === 'string' && post.imgSocial.url.includes('http') - ? post.imgSocial.url - : `${baseUrl}${post.imgSocial.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 imgThumbUrl = p?.imgThumb?.url - ? typeof p.imgThumb.url === 'string' && p.imgThumb.url.includes('http') - ? p.imgThumb.url - : `${baseUrl}${p.imgThumb.url}` - : '' - const imgSocialUrl = p?.imgSocial?.url - ? typeof p.imgSocial.url === 'string' && p.imgSocial.url.includes('http') - ? p.imgSocial.url - : `${baseUrl}${p.imgSocial.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, - imgThumb: imgThumbUrl || imgSocialUrl || '', - imgSocial: imgSocialUrl || 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 } - ) - } -} diff --git a/apps/www/app/api-v2/cms/disable-draft/route.ts b/apps/www/app/api-v2/cms/disable-draft/route.ts deleted file mode 100644 index 2c1a494ec6993..0000000000000 --- a/apps/www/app/api-v2/cms/disable-draft/route.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { draftMode } from 'next/headers' -import { redirect } from 'next/navigation' -import { SITE_ORIGIN } from '~/lib/constants' - -export async function GET(request: Request) { - const draft = await draftMode() - const { searchParams } = new URL(request.url) - const slug = searchParams.get('slug') - const path = searchParams.get('path') || 'blog' - - // Redirect to the path from the fetched post - const redir = slug ? `/${path}/${slug}` : `/${path}` - - // Disable Draft Mode by clearing the cookie - draft.disable() - - try { - const parsed = new URL(redir, SITE_ORIGIN) - // Only allow paths that stay on the origin - if (parsed.origin === SITE_ORIGIN) { - redirect(parsed.pathname) - } - } catch { - // Invalid URL - } - redirect(SITE_ORIGIN) -} diff --git a/apps/www/app/api-v2/cms/preview/route.ts b/apps/www/app/api-v2/cms/preview/route.ts deleted file mode 100644 index c0cb489e53660..0000000000000 --- a/apps/www/app/api-v2/cms/preview/route.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { draftMode } from 'next/headers' -import { redirect } from 'next/navigation' -import { SITE_ORIGIN } from '~/lib/constants' - -export async function GET(request: Request) { - const draft = await draftMode() - const { searchParams } = new URL(request.url) - const secret = searchParams.get('secret') - const slug = searchParams.get('slug') - const path = searchParams.get('path') || 'blog' - - // Get the expected secret with fallback to match CMS configuration - const expectedSecret = process.env.CMS_PREVIEW_SECRET - - if (secret !== expectedSecret) { - console.error('[preview] Token mismatch:', { - received: secret, - expected: expectedSecret, - }) - return new Response( - `Invalid token. Expected: ${expectedSecret?.slice(0, 3)}..., Received: ${secret?.slice(0, 3)}...`, - { status: 401 } - ) - } - - if (!slug) { - console.error('[preview] No slug provided') - return new Response('No slug in the request', { status: 401 }) - } - - // Enable Draft Mode by setting the cookie - draft.enable() - - // Redirect to the path from the fetched post - const redir = `/${path}/${slug}` - - try { - const parsed = new URL(redir, SITE_ORIGIN) - // Only allow paths that stay on the origin - if (parsed.origin === SITE_ORIGIN) { - redirect(parsed.pathname) - } - } catch { - // Invalid URL - } - redirect(SITE_ORIGIN) -} diff --git a/apps/www/app/api-v2/cms/revalidate/route.ts b/apps/www/app/api-v2/cms/revalidate/route.ts deleted file mode 100644 index 3ff73f9e8d8af..0000000000000 --- a/apps/www/app/api-v2/cms/revalidate/route.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { revalidatePath } from 'next/cache' -import { NextRequest, NextResponse } from 'next/server' - -export async function POST(request: NextRequest) { - try { - const body = await request.json() - const { secret, path } = body - - // Check for secret to confirm this is a valid request - if (secret !== process.env.CMS_PREVIEW_SECRET) { - return NextResponse.json({ message: 'Invalid token' }, { status: 401 }) - } - - if (!path) { - return NextResponse.json({ message: 'Missing path parameter' }, { status: 400 }) - } - - // This will revalidate the specific page - revalidatePath(path) - - return NextResponse.json({ revalidated: true, path }) - } catch (error) { - console.error('[Revalidate API] Error during revalidation:', error) - return NextResponse.json( - { - message: 'Error revalidating', - error: error instanceof Error ? error.message : 'Unknown error', - }, - { status: 500 } - ) - } -} diff --git a/apps/www/app/blog/[slug]/BlogPostClient.tsx b/apps/www/app/blog/[slug]/BlogPostClient.tsx index 8da4126fae2e2..07347abd8b0fa 100644 --- a/apps/www/app/blog/[slug]/BlogPostClient.tsx +++ b/apps/www/app/blog/[slug]/BlogPostClient.tsx @@ -1,19 +1,13 @@ 'use client' import dynamic from 'next/dynamic' -import { useState, useMemo, useEffect } 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 { generateTocFromMarkdown } from 'lib/toc' -import { convertRichTextToMarkdown } from '~/lib/cms/convertRichTextToMarkdown' -import useActiveAnchors from 'hooks/useActiveAnchors' +import useActiveAnchors from '@/hooks/useActiveAnchors' +import authors from '@/lib/authors.json' +import { isNotNullOrUndefined } from '@/lib/helpers' +import type { Blog, BlogData, PostReturnType, ProcessedBlogData } from '@/types/post' -import type { Blog, BlogData, CMSAuthor, PostReturnType, ProcessedBlogData } from 'types/post' - -const BlogPostRenderer = dynamic(() => import('components/Blog/BlogPostRenderer')) +const BlogPostRenderer = dynamic(() => import('@/components/Blog/BlogPostRenderer')) type BlogPostPageProps = { prevPost: PostReturnType | null @@ -27,149 +21,27 @@ type BlogPostPageProps = { export default function BlogPostClient(props: BlogPostPageProps) { const isDraftMode = props.isDraftMode - const [previewData] = useState(props.blog) // Enable scroll-to-anchor functionality and TOC highlighting useActiveAnchors() - const [processedToc, setProcessedToc] = useState<{ content: string; json: any[] } | null>(null) - const shouldUseLivePreview = isDraftMode && 'isCMS' in props.blog && props.blog.isCMS - - const { data: livePreviewData, isLoading: isLivePreviewLoading } = useLivePreview({ - initialData: props.blog, - serverURL: CMS_SITE_ORIGIN || 'http://localhost:3030', - depth: 2, - }) - - // Generate TOC for LivePreview data when content changes - useEffect(() => { - if (isDraftMode && shouldUseLivePreview && livePreviewData?.content) { - // Check if content is rich text (object) or already converted to markdown (string) - let markdownContent = '' - if (typeof livePreviewData.content === 'string') { - markdownContent = livePreviewData.content - } else if (typeof livePreviewData.content === 'object') { - markdownContent = convertRichTextToMarkdown(livePreviewData.content) - } - - if (markdownContent) { - const tocDepth = (livePreviewData as any).toc_depth || (props.blog as any).toc_depth || 3 - - generateTocFromMarkdown(markdownContent, tocDepth) - .then((tocResult) => { - setProcessedToc(tocResult) - }) - .catch((error) => { - console.error('Error generating TOC:', error) - setProcessedToc(null) - }) - } - } - }, [ - isDraftMode, - shouldUseLivePreview, - livePreviewData?.content, - livePreviewData?.toc_depth, - props.blog.toc_depth, - ]) - - const blogMetaData = useMemo(() => { - if (isDraftMode && shouldUseLivePreview) { - if (livePreviewData && typeof livePreviewData === 'object') { - // Process LivePreview author data to match expected CMSAuthor structure - const processedAuthors = - livePreviewData.authors?.map((author: Record) => { - // Handle both direct author objects and author references - if (typeof author === 'object' && author !== null) { - const authorName = - (author.author as string) || (author.name as string) || 'Unknown Author' - const authorId = (author.author_id as string) || (author.id as string) || '' - const position = (author.position as string) || '' - const authorUrl = (author.author_url as string) || '#' - const username = (author.username as string) || '' - - // Handle author image URL with proper type checking - let authorImageUrl: string | null = null - const authorImage = author.author_image_url as - | { url?: string } - | string - | null - | undefined - - if (authorImage) { - if (typeof authorImage === 'string') { - authorImageUrl = authorImage - } else if (typeof authorImage === 'object' && authorImage.url) { - const imageUrl = authorImage.url - if (typeof imageUrl === 'string') { - authorImageUrl = imageUrl.includes('http') - ? imageUrl - : `${CMS_SITE_ORIGIN}${imageUrl}` - } - } - } - - return { - author: authorName, - author_id: authorId, - position, - author_url: authorUrl, - author_image_url: authorImageUrl, - username, - } - } - return { - author: typeof author === 'string' ? author : 'Unknown Author', - author_id: '', - position: '', - author_url: '#', - author_image_url: null, - username: '', - } - }) ?? [] - - const processedLivePreviewData = { - ...props.blog, - ...livePreviewData, - authors: processedAuthors, - // Use processed TOC if available, otherwise fall back to original - toc: processedToc || props.blog.toc, - toc_depth: (livePreviewData as any).toc_depth || props.blog.toc_depth || 3, - } - return processedLivePreviewData - } - if (previewData !== props.blog) { - return previewData - } - } - return props.blog - }, [ - isDraftMode, - shouldUseLivePreview, - livePreviewData, - previewData, - props.blog, - processedToc, - ]) as ProcessedBlogData - - const isCMS = 'isCMS' in blogMetaData && blogMetaData.isCMS + const blogMetaData = props.blog - const blogAuthors = isCMS - ? ('authors' in blogMetaData ? (blogMetaData.authors as CMSAuthor[]) : []) || [] - : ('author' in blogMetaData ? (blogMetaData.author as string) : '') - ?.split(',') - .map((authorId: string) => { - const foundAuthor = authors.find((author) => author.author_id === authorId) - return foundAuthor ?? null - }) - .filter(isNotNullOrUndefined) || [] + const blogAuthors = (blogMetaData.author ?? '') + ?.split(',') + .map((authorId) => authorId.trim()) + .filter(Boolean) + .map((authorId) => { + const foundAuthor = authors.find((author) => author.author_id === authorId) + return foundAuthor ?? null + }) + .filter(isNotNullOrUndefined) return ( ({ slug: p.params.slug })) + return [...staticPaths].map((p) => ({ slug: p.params.slug })) } export async function generateMetadata({ params }: { params: Promise }): Promise { @@ -94,11 +49,7 @@ export async function generateMetadata({ params }: { params: Promise }): const parsedContent = matter(postContent) as unknown as MatterReturn const blogPost = parsedContent.data const blogImage = blogPost.imgThumb || blogPost.imgSocial - const metaImageUrl = blogImage - ? blogImage.startsWith('http') - ? blogImage - : `${CMS_SITE_ORIGIN.replace('/api-v2', '')}${blogImage}` - : undefined + const metaImageUrl = blogImage && blogImage.startsWith('http') ? blogImage : undefined return { title: blogPost.title, @@ -117,71 +68,10 @@ export async function generateMetadata({ params }: { params: Promise }): images: metaImageUrl ? [metaImageUrl] : undefined, }, } - } catch { - // Static post not found, try CMS post - } - - // Try to fetch CMS post for metadata - let cmsPost = await getCMSPostFromAPI(slug, 'preview', isDraft) - - if (!cmsPost) { - cmsPost = await getCMSPostBySlug(slug, isDraft) - } - - if (!cmsPost) { - return { - title: 'Blog Post Not Found', - description: 'The requested blog post could not be found.', - } - } - - // Extract meta fields with fallbacks - const metaTitle = cmsPost.meta?.title || cmsPost.title - const metaDescription = cmsPost.meta?.description || cmsPost.description - - // Handle different image field types from CMS - let metaImageUrl: string | undefined - if (cmsPost.meta?.image) { - // If meta.image is an object with url property - if (typeof cmsPost.meta.image === 'object' && cmsPost.meta.image.url) { - metaImageUrl = cmsPost.meta.image.url - } - // If meta.image is a string URL - else if (typeof cmsPost.meta.image === 'string') { - metaImageUrl = cmsPost.meta.image - } - } - - // Fallback to imgThumb or imgSocial if no meta image - if (!metaImageUrl) { - metaImageUrl = cmsPost.imgThumb || cmsPost.imgSocial - } - - // Ensure image URLs are absolute - const absoluteImageUrl = metaImageUrl - ? metaImageUrl.startsWith('http') - ? metaImageUrl - : `${CMS_SITE_ORIGIN.replace('/api-v2', '')}${metaImageUrl}` - : undefined - + } catch {} return { - title: metaTitle, - description: metaDescription, - openGraph: { - title: metaTitle, - description: metaDescription, - url: `${SITE_ORIGIN}/blog/${slug}`, - type: 'article', - publishedTime: cmsPost.date || cmsPost.publishedAt, - authors: cmsPost.authors?.map((author: any) => author.author || 'Unknown Author'), - images: absoluteImageUrl ? [{ url: absoluteImageUrl }] : undefined, - }, - twitter: { - card: 'summary_large_image', - title: metaTitle, - description: metaDescription, - images: absoluteImageUrl ? [absoluteImageUrl] : undefined, - }, + title: 'Blog Post Not Found', + description: 'The requested blog post could not be found.', } } @@ -246,96 +136,4 @@ export default async function BlogPostPage({ params }: { params: Promise return } catch {} - - // Try to fetch CMS post using our new unified API first - let cmsPost = await getCMSPostFromAPI(slug, 'full', isDraft) - - // Fallback to the original method if the API doesn't return the post - if (!cmsPost) { - cmsPost = await getCMSPostBySlug(slug, isDraft) - } - - if (!cmsPost) { - if (isDraft) { - // Try to fetch published version for draft mode - let publishedPost = await getCMSPostFromAPI(slug, 'full', false) - if (!publishedPost) { - publishedPost = await getCMSPostBySlug(slug, false) - } - - if (!publishedPost) return null - - const mdxSource = await mdxSerialize(publishedPost.content || '', { - tocDepth: publishedPost.toc_depth || 3, - }) - const tocResult = (mdxSource as any).scope?.toc || publishedPost.toc || { content: '' } - const props: BlogPostPageProps = { - prevPost: null, - nextPost: null, - relatedPosts: [], - blog: { - ...publishedPost, - slug: publishedPost.slug ?? slug, - tags: publishedPost.tags || [], - authors: publishedPost.authors || [], - isCMS: true, - content: mdxSource, - toc: tocResult, - imgSocial: publishedPost.imgSocial ?? undefined, - imgThumb: publishedPost.imgThumb ?? undefined, - // Extract meta fields from CMS - meta_title: publishedPost.meta?.title ?? undefined, - meta_description: publishedPost.meta?.description ?? undefined, - meta_image: publishedPost.meta?.image ?? publishedPost.imgThumb ?? undefined, - } as any, - isDraftMode: isDraft, - } - return - } - return null - } - - const tocDepth = cmsPost.toc_depth || 3 - - // Use the new CMS content processor to handle blocks - let processedContent: any - - try { - processedContent = await processCMSContent(cmsPost.richContent || cmsPost.content, tocDepth) - } catch (error) { - console.warn('Error processing CMS content, falling back to legacy processing:', error) - // Fallback to legacy processing - const mdxSource = await mdxSerialize(cmsPost.content || '', { tocDepth }) - processedContent = { - content: mdxSource, - blocks: [], - toc: (mdxSource as any).scope?.toc || cmsPost.toc || { content: '' }, - plainMarkdown: cmsPost.content || '', - } - } - - const props: BlogPostPageProps = { - prevPost: null, - nextPost: null, - relatedPosts: [], - blog: { - ...cmsPost, - slug: cmsPost.slug ?? slug, - tags: cmsPost.tags || [], - authors: cmsPost.authors || [], - isCMS: true, - content: processedContent.content, - toc: processedContent.toc, - toc_depth: cmsPost.toc_depth || 3, - imgSocial: cmsPost.imgSocial ?? undefined, - imgThumb: cmsPost.imgThumb ?? undefined, - // Extract meta fields from CMS - meta_title: cmsPost.meta?.title ?? undefined, - meta_description: cmsPost.meta?.description ?? undefined, - meta_image: cmsPost.meta?.image ?? cmsPost.imgThumb ?? undefined, - } as any, - isDraftMode: isDraft, - } - - return } diff --git a/apps/www/app/blog/authors/[author]/page.tsx b/apps/www/app/blog/authors/[author]/page.tsx index 29297b45bb7de..a60a1ea52724e 100644 --- a/apps/www/app/blog/authors/[author]/page.tsx +++ b/apps/www/app/blog/authors/[author]/page.tsx @@ -1,10 +1,9 @@ import type { Metadata } from 'next' -import blogAuthors from 'lib/authors.json' -import { getAllCMSPosts } from 'lib/get-cms-posts' -import { getSortedPosts } from 'lib/posts' -import type PostTypes from 'types/post' import AuthorClient from './AuthorClient' +import blogAuthors from '@/lib/authors.json' +import { getSortedPosts } from '@/lib/posts' +import type PostTypes from '@/types/post' type Params = { author: string } @@ -55,15 +54,7 @@ export default async function AuthorPage({ params: paramsPromise }: { params: Pr return postAuthors.some((a: string) => toCanonicalAuthorId(a) === authorId) }) - // Get CMS posts by this author (normalize identifiers) - const allCmsPosts = await getAllCMSPosts({}) - const cmsPosts = allCmsPosts.filter((post: any) => { - return post.authors?.some( - (a: any) => toCanonicalAuthorId(a.author_id ?? a.username ?? '') === authorId - ) - }) - - const blogs = [...(staticPosts as any[]), ...(cmsPosts as any[])].sort( + const blogs = [...(staticPosts as any[])].sort( (a, b) => new Date(b.date).getTime() - new Date(a.date).getTime() ) as unknown as PostTypes[] diff --git a/apps/www/app/blog/categories/[category]/page.tsx b/apps/www/app/blog/categories/[category]/page.tsx index 78dbaf4610949..991c68f551503 100644 --- a/apps/www/app/blog/categories/[category]/page.tsx +++ b/apps/www/app/blog/categories/[category]/page.tsx @@ -1,12 +1,11 @@ import type { Metadata } from 'next' import Link from 'next/link' -import DefaultLayout from 'components/Layouts/Default' -import BlogGridItem from 'components/Blog/BlogGridItem' -import { getAllCMSPosts } from 'lib/get-cms-posts' -import { capitalize } from 'lib/helpers' -import { getSortedPosts, getAllCategories } from 'lib/posts' -import type PostTypes from 'types/post' +import BlogGridItem from '@/components/Blog/BlogGridItem' +import DefaultLayout from '@/components/Layouts/Default' +import { capitalize } from '@/lib/helpers' +import { getAllCategories, getSortedPosts } from '@/lib/posts' +import type PostTypes from '@/types/post' type Params = { category: string } @@ -44,11 +43,7 @@ export default async function CategoriesPage({ limit: 0, categories: [params.category], }) - const cmsPosts = await getAllCMSPosts() - const blogs = [ - ...(staticPosts as any[]), - ...(cmsPosts as any[]).filter((p) => (p.categories || []).includes(params.category)), - ] as unknown as PostTypes[] + const blogs = [...staticPosts] as PostTypes[] const capitalizedCategory = capitalize(params?.category.replaceAll('-', ' ')) return ( diff --git a/apps/www/app/blog/page.tsx b/apps/www/app/blog/page.tsx index f4faeba928240..0b9acac73a527 100644 --- a/apps/www/app/blog/page.tsx +++ b/apps/www/app/blog/page.tsx @@ -1,8 +1,8 @@ -import BlogClient from './BlogClient' -import { getSortedPosts } from 'lib/posts' -import { getAllCMSPosts } from 'lib/get-cms-posts' import type { Metadata } from 'next' +import BlogClient from './BlogClient' +import { getSortedPosts } from '@/lib/posts' + export const revalidate = 30 export const metadata: Metadata = { @@ -22,11 +22,8 @@ export default async function BlogPage() { // Get static blog posts const staticPostsData = getSortedPosts({ directory: '_blog', runner: '** BLOG PAGE **' }) - // Get CMS posts server-side with revalidation - const cmsPostsData = await getAllCMSPosts({ limit: 100 }) - - // Combine static and CMS posts and sort by date - const allPosts = [...staticPostsData, ...cmsPostsData].sort((a: any, b: any) => { + // Sort by date + const allPosts = [...staticPostsData].sort((a, b) => { const dateA = new Date(a.date || a.formattedDate).getTime() const dateB = new Date(b.date || b.formattedDate).getTime() return dateB - dateA diff --git a/apps/www/app/blog/tags/[tag]/page.tsx b/apps/www/app/blog/tags/[tag]/page.tsx index db30a65a2d8f0..54449d967c433 100644 --- a/apps/www/app/blog/tags/[tag]/page.tsx +++ b/apps/www/app/blog/tags/[tag]/page.tsx @@ -1,12 +1,11 @@ import type { Metadata } from 'next' import Link from 'next/link' -import DefaultLayout from 'components/Layouts/Default' -import BlogGridItem from 'components/Blog/BlogGridItem' -import { getAllCMSPosts } from 'lib/get-cms-posts' -import { capitalize } from 'lib/helpers' -import { getSortedPosts, getAllTags } from 'lib/posts' -import type PostTypes from 'types/post' +import BlogGridItem from '@/components/Blog/BlogGridItem' +import DefaultLayout from '@/components/Layouts/Default' +import { capitalize } from '@/lib/helpers' +import { getAllTags, getSortedPosts } from '@/lib/posts' +import type PostTypes from '@/types/post' type Params = { tag: string } @@ -36,8 +35,7 @@ export default async function TagPage({ params: paramsPromise }: { params: Promi const params = await paramsPromise const staticPosts = getSortedPosts({ directory: '_blog', limit: 0, tags: [params.tag] }) - const cmsPosts = await getAllCMSPosts({ tags: [params.tag] }) - const blogs = [...(staticPosts as any[]), ...(cmsPosts as any[])] as unknown as PostTypes[] + const blogs = [...staticPosts] as PostTypes[] const capitalizedTag = capitalize(params?.tag.replaceAll('-', ' ')) return ( diff --git a/apps/www/components/Blog/BlogGridItem.tsx b/apps/www/components/Blog/BlogGridItem.tsx index ddaa0bddc4ec3..8489fe4d785f0 100644 --- a/apps/www/components/Blog/BlogGridItem.tsx +++ b/apps/www/components/Blog/BlogGridItem.tsx @@ -1,10 +1,10 @@ import dayjs from 'dayjs' -import authors from 'lib/authors.json' import Image from 'next/image' import Link from 'next/link' -import type Author from '~/types/author' -import type PostTypes from '~/types/post' +import authors from '@/lib/authors.json' +import type Author from '@/types/author' +import type PostTypes from '@/types/post' interface Props { post: PostTypes @@ -29,11 +29,10 @@ const BlogGridItem = ({ post }: Props) => { return img.startsWith('/') || img.startsWith('http') ? img : `/images/blog/${img}` } - const imageUrl = post.isCMS - ? post.imgThumb || post.imgSocial || '/images/blog/blog-placeholder.png' - : resolveImagePath(post.imgThumb) || - resolveImagePath(post.imgSocial) || - '/images/blog/blog-placeholder.png' + const imageUrl = + resolveImagePath(post.imgThumb) || + resolveImagePath(post.imgSocial) || + '/images/blog/blog-placeholder.png' return ( { - if ('isCMS' in post && post.isCMS) { - // For CMS posts, display author directly from the blog data - const cmsBlog = post as CMSPostTypes - const authors = - cmsBlog.authors?.map((author) => ({ - author: author.author || 'Unknown Author', - author_image_url: author.author_image_url || null, - author_url: author.author_url || '#', - position: author.position || '', - })) || [] - - return authors - } - +const getAuthors = (post: PostTypes) => { const authorArray = post.author?.split(',').map((a) => a.trim()) || [] const authors = [] diff --git a/apps/www/components/Blog/BlogPostRenderer.tsx b/apps/www/components/Blog/BlogPostRenderer.tsx index f718e8d0a2eca..395610377adf6 100644 --- a/apps/www/components/Blog/BlogPostRenderer.tsx +++ b/apps/www/components/Blog/BlogPostRenderer.tsx @@ -1,7 +1,6 @@ 'use client' import dayjs from 'dayjs' -import mdxComponents from 'lib/mdx/mdxComponents' import { ChevronLeft } from 'lucide-react' import { MDXRemote } from 'next-mdx-remote' import type { MDXRemoteSerializeResult } from 'next-mdx-remote' @@ -10,20 +9,22 @@ import Image from 'next/image' import Link from 'next/link' import { useMemo, useState } from 'react' import type { ComponentType } from 'react' -import type { CMSAuthor, PostReturnType, ProcessedBlogData, Tag } from 'types/post' +import type { PostReturnType, ProcessedBlogData, StaticAuthor, Tag } from 'types/post' import { Badge } from 'ui' -const ShareArticleActions = dynamic(() => import('components/Blog/ShareArticleActions')) -const CTABanner = dynamic(() => import('components/CTABanner')) -const LW11Summary = dynamic(() => import('components/LaunchWeek/11/LW11Summary')) -const LW12Summary = dynamic(() => import('components/LaunchWeek/12/LWSummary')) -const LW13Summary = dynamic(() => import('components/LaunchWeek/13/Releases/LWSummary')) -const LW14Summary = dynamic(() => import('components/LaunchWeek/14/Releases/LWSummary')) -const LW15Summary = dynamic(() => import('components/LaunchWeek/15/LWSummary')) -const BlogLinks = dynamic(() => import('components/LaunchWeek/7/BlogLinks')) -const LWXSummary = dynamic(() => import('components/LaunchWeek/X/LWXSummary')) -const DefaultLayout = dynamic(() => import('components/Layouts/Default')) -const DraftModeBanner = dynamic(() => import('components/Blog/DraftModeBanner')) +import mdxComponents from '@/lib/mdx/mdxComponents' + +const ShareArticleActions = dynamic(() => import('@/components/Blog/ShareArticleActions')) +const CTABanner = dynamic(() => import('@/components/CTABanner')) +const LW11Summary = dynamic(() => import('@/components/LaunchWeek/11/LW11Summary')) +const LW12Summary = dynamic(() => import('@/components/LaunchWeek/12/LWSummary')) +const LW13Summary = dynamic(() => import('@/components/LaunchWeek/13/Releases/LWSummary')) +const LW14Summary = dynamic(() => import('@/components/LaunchWeek/14/Releases/LWSummary')) +const LW15Summary = dynamic(() => import('@/components/LaunchWeek/15/LWSummary')) +const BlogLinks = dynamic(() => import('@/components/LaunchWeek/7/BlogLinks')) +const LWXSummary = dynamic(() => import('@/components/LaunchWeek/X/LWXSummary')) +const DefaultLayout = dynamic(() => import('@/components/Layouts/Default')) +const DraftModeBanner = dynamic(() => import('@/components/Blog/DraftModeBanner')) const ReactMarkdown = dynamic<{ children: string }>( () => import('react-markdown').then( @@ -36,7 +37,6 @@ const BlogPostRenderer = ({ blog, blogMetaData, isDraftMode, - livePreviewData, prevPost, nextPost, authors, @@ -44,43 +44,18 @@ const BlogPostRenderer = ({ blog: ProcessedBlogData blogMetaData: ProcessedBlogData isDraftMode: boolean - livePreviewData?: ProcessedBlogData - isLivePreviewLoading?: boolean prevPost?: PostReturnType | null nextPost?: PostReturnType | null - authors: CMSAuthor[] + authors: StaticAuthor[] }) => { const [previewData] = useState(blog) - const shouldUseLivePreview = isDraftMode && blog.isCMS - // For LivePreview, we'll use the raw content directly with ReactMarkdown // instead of trying to use MDXRemote which requires specific serialization - const isLivePreview = isDraftMode && (livePreviewData !== undefined || previewData !== blog) + const isLivePreview = isDraftMode && previewData !== blog // Extract raw content from data if available const livePreviewContent = useMemo(() => { - // Priority 1: Use data from LivePreview hook (only in draft mode) - if ( - isDraftMode && - shouldUseLivePreview && - livePreviewData && - typeof livePreviewData === 'object' - ) { - // If content is a string, use it directly - if (typeof (livePreviewData as unknown as { content?: unknown }).content === 'string') { - return (livePreviewData as unknown as { content?: string }).content as string - } - - // If content is from source property - if ( - (livePreviewData as unknown as { source?: unknown }).source && - typeof (livePreviewData as unknown as { source?: unknown }).source === 'string' - ) { - return (livePreviewData as unknown as { source?: string }).source as string - } - } - // Priority 2: Use data from postMessage updates if (isDraftMode && previewData !== blog) { // If content is a string, use it directly @@ -98,9 +73,8 @@ const BlogPostRenderer = ({ } return blog.source || '' - }, [isDraftMode, shouldUseLivePreview, livePreviewData, previewData, blog]) + }, [isDraftMode, previewData, blog]) - const isCMS = blogMetaData.isCMS const isLaunchWeek7 = blogMetaData.launchweek === '7' const isLaunchWeekX = blogMetaData.launchweek?.toString().toLocaleLowerCase() === 'x' const isGAWeek = blogMetaData.launchweek?.toString().toLocaleLowerCase() === '11' @@ -157,13 +131,11 @@ const BlogPostRenderer = ({ ) - const imageUrl = isCMS - ? blogMetaData.imgThumb ?? '' - : blogMetaData.imgThumb - ? blogMetaData.imgThumb.startsWith('/') || blogMetaData.imgThumb.startsWith('http') - ? blogMetaData.imgThumb - : `/images/blog/${blogMetaData.imgThumb}` - : '' + const imageUrl = blogMetaData.imgThumb + ? blogMetaData.imgThumb.startsWith('/') || blogMetaData.imgThumb.startsWith('http') + ? blogMetaData.imgThumb + : `/images/blog/${blogMetaData.imgThumb}` + : '' return ( <> @@ -203,11 +175,7 @@ const BlogPostRenderer = ({
{authors.map((author, i: number) => { - // Handle both static and CMS author image formats - const authorImageUrl = - typeof author.author_image_url === 'string' - ? author.author_image_url - : (author.author_image_url as { url: string })?.url || '' + const authorImageUrl = author.author_image_url const authorId = (author as any).author_id || diff --git a/apps/www/components/Blog/DraftModeBanner.tsx b/apps/www/components/Blog/DraftModeBanner.tsx index edc80e73fe862..d96eed0d06531 100644 --- a/apps/www/components/Blog/DraftModeBanner.tsx +++ b/apps/www/components/Blog/DraftModeBanner.tsx @@ -22,16 +22,6 @@ function DraftModeBanner({ onDismiss }: DraftModeBannerProps) {

-
- -
{onDismiss && (