import { Article, FetchMarkdownOptions, MarkdownContent, ProjectInterface, } from "./types" // Static content cache for deployment environments where filesystem access is limited const staticContentCache: { [key: string]: MarkdownContent[] } = {} // Production-ready dynamic imports for server-side only async function getServerModules() { // Only import on server-side to avoid bundling in client if (typeof window !== "undefined") { return null } try { const [{ default: fs }, { default: path }, { default: matter }, jsYaml] = await Promise.all([ import("fs"), import("path"), import("gray-matter"), import("js-yaml"), ]) return { fs, path, matter, jsYaml } } catch (error) { console.error("Failed to load server modules:", error) return null } } // Function to populate static cache at build time async function populateStaticCache(folderName: string) { const modules = await getServerModules() if (!modules) return [] const { fs, path } = modules const contentDirectory = path.resolve(process.cwd(), folderName) if (fs.existsSync(contentDirectory)) { console.log(`📦 Populating static cache for ${folderName}`) const content = await getMarkdownFilesFromPath(contentDirectory, modules, { limit: 1000, }) staticContentCache[folderName] = content console.log(`✅ Cached ${content.length} items for ${folderName}`) return content } return [] } // Simplified function to get markdown files from any folder export async function getMarkdownFiles( folderName: string, options?: FetchMarkdownOptions ): Promise { // Return empty array if running on client-side if (typeof window !== "undefined") { console.warn( "getMarkdownFiles called on client-side, returning empty array" ) return [] } // First, try to get from static cache if (staticContentCache[folderName]) { console.log(`📁 Using cached content for ${folderName}`) return filterAndLimitContent(staticContentCache[folderName], options) } const modules = await getServerModules() if (!modules) { console.error("Failed to load server modules") return [] } const { fs, path, matter, jsYaml } = modules const { limit = 1000, tag, project } = options ?? {} // Enhanced path resolution for different environments let contentDirectory = path.resolve(process.cwd(), folderName) // Try alternative path resolution for Vercel deployment if (!fs.existsSync(contentDirectory)) { console.log(`First path attempt failed: ${contentDirectory}`) // For Vercel deployment, try multiple path strategies const altPaths = [ // Try relative to current working directory path.join(process.cwd(), "..", folderName), // Try relative to __dirname (built location) path.join(__dirname, "..", "..", "..", "..", folderName), path.join(__dirname, "..", "..", "..", folderName), path.join(__dirname, "..", "..", folderName), // Try from root of Next.js build path.join(process.cwd(), ".next", "..", folderName), // Try from source location if available path.join("/var/task", folderName), path.join("/tmp", folderName), // Try alternative working directory patterns path.join(process.cwd(), "app", "..", folderName), path.join(process.cwd(), "src", "..", folderName), ] console.log(`Trying ${altPaths.length} alternative paths...`) for (let i = 0; i < altPaths.length; i++) { const testPath = altPaths[i] console.log(`Path ${i + 1}: ${testPath}`) if (fs.existsSync(testPath)) { contentDirectory = testPath console.log(`✅ Found content directory at: ${contentDirectory}`) break } } // If still not found, try to populate cache from build time if (!fs.existsSync(contentDirectory)) { console.log("❌ Content directory not found, trying build-time cache...") // Try to populate cache at runtime if possible try { await populateStaticCache(folderName) if (staticContentCache[folderName]) { return filterAndLimitContent(staticContentCache[folderName], options) } } catch (error) { console.error("Failed to populate static cache:", error) } } } try { // Check if the directory exists if (!fs.existsSync(contentDirectory)) { console.error(`❌ Directory not found: ${contentDirectory}`) console.error(`Current working directory: ${process.cwd()}`) console.error(`__dirname: ${__dirname}`) // List contents of current directory for debugging try { const cwdContents = fs.readdirSync(process.cwd()) console.error(`Contents of ${process.cwd()}:`, cwdContents.slice(0, 10)) // Also check parent directory const parentDir = path.dirname(process.cwd()) if (fs.existsSync(parentDir)) { const parentContents = fs.readdirSync(parentDir) console.error( `Contents of parent ${parentDir}:`, parentContents.slice(0, 10) ) } } catch (e) { console.error("Cannot read directories: " + e) } return [] } const files = fs.readdirSync(contentDirectory) return getMarkdownFilesFromPath(contentDirectory, modules, { limit, tag, project, }) } catch (error) { console.error(`❌ Error accessing directory ${folderName}:`, error) return [] } } // Helper function to filter and limit content function filterAndLimitContent( content: MarkdownContent[], options?: FetchMarkdownOptions ): MarkdownContent[] { const { limit = 1000, tag, project } = options ?? {} let filteredContent = [...content] // Filter by tag if provided if (tag) { filteredContent = filteredContent.filter((item) => { // Handle array format (legacy articles) if (Array.isArray(item.tags)) { return item.tags.includes(tag) } // Handle object format (projects) if (typeof item.tags === "object" && item.tags !== null) { // Check all tag categories for the tag return Object.values(item.tags).some((tagArray) => Array.isArray(tagArray) ? tagArray.includes(tag) : false ) } return false }) } // Filter by project if provided if (project) { filteredContent = filteredContent.filter( (item) => Array.isArray(item.projects) && item.projects.includes(project) ) } // Sort content by date (if date exists) return filteredContent .sort((a, b) => { const dateA = new Date(a.date || "1970-01-01") const dateB = new Date(b.date || "1970-01-01") // Sort in descending order (newest first) return dateB.getTime() - dateA.getTime() }) .slice(0, limit) } // Helper function to process files from a given path async function getMarkdownFilesFromPath( contentDirectory: string, modules: any, options: { limit: number; tag?: string; project?: string } ): Promise { const { fs, path, matter, jsYaml } = modules const { limit, tag, project } = options // Get file names under the specified folder const fileNames = fs.readdirSync(contentDirectory) const allContentData = fileNames .filter((fileName: string) => fileName.endsWith(".md")) .map((fileName: string) => { const id = fileName.replace(/\.md$/, "")?.toLowerCase() if (id === "readme" || id.startsWith("_")) { return null } // Read markdown file as string const fullPath = path.join(contentDirectory, fileName) const fileContents = fs.readFileSync(fullPath, "utf8") try { // Use matter with options to handle multiline strings const matterResult = matter(fileContents, { engines: { yaml: { // Ensure multiline strings are parsed correctly parse: (str: string) => { try { // Use js-yaml's safe load to parse the YAML with type assertion return jsYaml.load(str, { schema: jsYaml.DEFAULT_SCHEMA, filename: fileName, }) as object } catch (e) { console.error( `Error parsing YAML frontmatter in ${fileName}:`, e ) console.error("YAML content preview:", str.substring(0, 200)) // Fallback to empty object if parsing fails return {} } }, }, }, }) // Handle tags properly - preserve object structure for projects, array for articles let processedTags = matterResult.data?.tags // If tags is an array (legacy format), keep it as array if (Array.isArray(matterResult.data?.tags)) { processedTags = [ ...matterResult.data.tags, ...(matterResult.data?.tag ? [matterResult.data.tag] : []), ] } // If tags is an object (new project format), preserve the object structure else if ( typeof matterResult.data?.tags === "object" && matterResult.data?.tags !== null ) { processedTags = matterResult.data.tags } // If no tags but there's a single tag field, create array else if (matterResult.data?.tag) { processedTags = [matterResult.data.tag] } // Default to empty array else { processedTags = [] } return { id, ...matterResult.data, tags: processedTags, content: matterResult.content, } } catch (error) { console.error(`Error processing ${fileName}:`, error) // Return minimal content data if there's an error return { id, title: `Error processing ${id}`, content: "This content could not be processed due to an error.", date: new Date().toISOString().split("T")[0], tags: [], } } }) let filteredContent = allContentData.filter(Boolean) as MarkdownContent[] // Filter by tag if provided if (tag) { filteredContent = filteredContent.filter((content) => { // Handle array format (legacy articles) if (Array.isArray(content.tags)) { return content.tags.includes(tag) } // Handle object format (projects) if (typeof content.tags === "object" && content.tags !== null) { // Check all tag categories for the tag return Object.values(content.tags).some((tagArray) => Array.isArray(tagArray) ? tagArray.includes(tag) : false ) } return false }) } // Filter by project if provided if (project) { filteredContent = filteredContent.filter( (content) => Array.isArray(content.projects) && content.projects.includes(project) ) } // Sort content by date (if date exists) return filteredContent .sort((a, b) => { const dateA = new Date(a.date || "1970-01-01") const dateB = new Date(b.date || "1970-01-01") // Sort in descending order (newest first) return dateB.getTime() - dateA.getTime() }) .slice(0, limit) } // Generic function to get a specific markdown file by ID from a folder export async function getMarkdownFileById( folderName: string, id: string ): Promise { // Return undefined if running on client-side if (typeof window !== "undefined") { console.warn( "getMarkdownFileById called on client-side, returning undefined" ) return undefined } const allContent = await getMarkdownFiles(folderName) return allContent.find( (content) => content.id?.toLowerCase() === id?.toLowerCase() ) } // Specific helper functions for common use cases export async function getArticles( options?: FetchMarkdownOptions ): Promise { const content = await getMarkdownFiles("content/articles", options) return content.map((item) => ({ ...item, authors: item.authors || [], image: item.image || "", tldr: item.tldr || "", })) as Article[] } export async function getProjects( options?: FetchMarkdownOptions ): Promise { const content = (await getMarkdownFiles( "content/projects", options )) as unknown as ProjectInterface[] return content.map((item) => ({ ...item, name: item.name, section: item.section || "pse", projectStatus: item.projectStatus || "active", tldr: item.tldr || "", image: item.image || "", })) as ProjectInterface[] } export async function getArticleById(id: string): Promise
{ const content = await getMarkdownFileById("content/articles", id) if (!content) return undefined return { ...content, authors: content.authors || [], image: content.image || "", tldr: content.tldr || "", } as Article } export async function getProjectById( id: string ): Promise { const content = (await getMarkdownFileById( "content/projects", id )) as unknown as ProjectInterface if (!content) return undefined return { ...content, name: content.name, section: content.section || "pse", projectStatus: content.projectStatus || "active", tldr: content.tldr || "", image: content.image || "", } as ProjectInterface } // Generic functions that return MarkdownContent for flexibility export async function getAllArticles( options?: FetchMarkdownOptions ): Promise { return await getMarkdownFiles("content/articles", options) } export async function getAllProjects( options?: FetchMarkdownOptions ): Promise { return await getMarkdownFiles("content/projects", options) } const lib = { getMarkdownFiles, getMarkdownFileById, getArticles, getProjects, getArticleById, getProjectById, getAllArticles, getAllProjects, } export default lib