Files
supabase/apps/www/lib/posts.tsx
Charis 29820d8309 www: properly surface not-found blog slugs (#45537)
Recent changes to blog pages surfaced existing errors because we stopped
silently swallowing errors. We were not properly handing the case when
the requested slug was not found, now this properly calls notFound()

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **Bug Fixes**
* Enhanced error handling for missing blog posts to display a proper
"Not Found" page instead of showing an application error when users
attempt to access unavailable blog content.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-05-04 11:29:25 -04:00

229 lines
5.8 KiB
TypeScript

import fs from 'fs'
import path from 'path'
import matter from 'gray-matter'
import { validateBlogFrontmatterImages } from './blog-images'
import { generateReadingTime } from './helpers'
type Directories = '_blog' | '_case-studies' | '_customers' | '_alternatives' | '_events'
// substring amount for file names
// based on YYYY-MM-DD format
export const FILENAME_SUBSTRING = 11
export type Post = {
slug: string
title?: string
description?: string
author?: string
imgSocial?: string
imgThumb?: string
categories?: string[]
tags?: string[]
date?: string
toc_depth?: number
formattedDate: string
readingTime: string
url: string
path: string
[key: string]: any // Allow additional properties from frontmatter
}
type GetSortedPostsParams = {
directory: Directories
limit?: number
tags?: string[]
runner?: unknown
currentPostSlug?: string
categories?: any
}
export const getSortedPosts = ({
directory,
limit,
tags,
categories,
currentPostSlug,
}: GetSortedPostsParams): Post[] => {
//Finding directory named "blog" from the current working directory of Node.
const postDirectory = path.join(process.cwd(), directory)
//Reads all the files in the post directory
const fileNames = fs.readdirSync(postDirectory)
const allPosts = fileNames
.map((filename) => {
const slug =
directory === '_blog' || directory === '_events'
? filename.replace('.mdx', '').substring(FILENAME_SUBSTRING)
: filename.replace('.mdx', '')
const fullPath = path.join(postDirectory, filename)
//Extracts contents of the MDX file
const fileContents = fs.readFileSync(fullPath, 'utf8')
const { data, content } = matter(fileContents) as unknown as {
data: { [key: string]: any; tags?: string[] }
content: string
}
if (directory === '_blog') {
validateBlogFrontmatterImages(data as { imgSocial?: string; imgThumb?: string }, fullPath)
}
const options: Intl.DateTimeFormatOptions = { month: 'long', day: 'numeric', year: 'numeric' }
const formattedDate = new Date(data.date).toLocaleDateString('en-IN', options)
const readingTime = generateReadingTime(content)
const url = `/${directory.replace('_', '')}/${slug}`
const contentPath = `/${directory.replace('_', '')}/${slug}`
const frontmatter = {
...data,
formattedDate,
readingTime,
url: url,
path: contentPath,
}
return {
slug,
...frontmatter,
}
})
// avoid reading content if it's the same post as the one the user is already reading
.filter((post) => post.slug !== currentPostSlug)
let sortedPosts = [...allPosts]
sortedPosts = sortedPosts.sort(
(a: any, b: any) => new Date(b.date).getTime() - new Date(a.date).getTime()
)
if (categories) {
sortedPosts = sortedPosts.filter((post: any) => {
const found = categories?.some((tag: any) => post.categories?.includes(tag))
return found
})
}
if (tags) {
sortedPosts = sortedPosts.filter((post: any) => {
const found = tags.some((tag: any) => post.tags?.includes(tag))
return found
})
}
if (limit) sortedPosts = sortedPosts.slice(0, limit)
return sortedPosts
}
// Get Slugs
export const getAllPostSlugs = (directory: Directories) => {
//Finding directory named "blog" from the current working directory of Node.
const postDirectory = path.join(process.cwd(), directory)
const fileNames = fs.readdirSync(postDirectory)
const files = fileNames.map((filename) => {
const dates =
directory === '_blog'
? getDatesFromFileName(filename)
: {
year: '2021',
month: '04',
day: '02',
}
return {
params: {
...dates,
slug: filename
.replace('.mdx', '')
.substring(directory === '_blog' || directory === '_events' ? FILENAME_SUBSTRING : 0),
},
}
})
return files
}
export const getPostdata = async (slug: string, directory: string) => {
/**
* All files are mdx files
*/
const fileType = 'mdx'
slug = slug + '.' + fileType
/**
* Return full directory
*/
const postDirectory = path.join(process.cwd(), directory)
const folderfiles = fs.readdirSync(postDirectory)
/**
* Check if the file exists in the directory
* This should return 1 result
*
* this is so slugs like 'blog-post.mdx' will work
* even if the mdx file is date namednamed like '2022-01-01-blog-post.mdx'
*/
const found = folderfiles.filter((x) => x.includes(slug))[0]
if (!found) {
throw Object.assign(new Error(`Post not found: ${slug}`), { code: 'POST_NOT_FOUND' })
}
const fullPath = path.join(postDirectory, found)
const postContent = fs.readFileSync(fullPath, 'utf8')
if (directory === '_blog') {
const { data } = matter(postContent) as unknown as { data: { [key: string]: any } }
validateBlogFrontmatterImages(data as { imgSocial?: string; imgThumb?: string }, fullPath)
}
return postContent
}
export const getAllCategories = (directory: Directories) => {
const posts = getSortedPosts({ directory })
let categories: any = []
posts.map((post: any) => {
post.categories?.map((tag: string) => {
if (!categories.includes(tag)) return categories.push(tag)
})
})
return categories
}
export const getAllTags = (directory: Directories) => {
const posts = getSortedPosts({ directory })
let tags: any = []
posts.map((post: any) => {
post.tags?.map((tag: string) => {
if (!tags.includes(tag)) return tags.push(tag)
})
})
return tags
}
const getDatesFromFileName = (filename: string) => {
// extract YYYY, MM, DD from post name
const year = filename.substring(0, 4)
const month = filename.substring(5, 7)
const day = filename.substring(8, 10)
return {
year,
month,
day,
}
}