diff --git a/src/lib/blog.ts b/src/lib/blog.ts index dcf97f1f..fd03dfbc 100644 --- a/src/lib/blog.ts +++ b/src/lib/blog.ts @@ -4,7 +4,6 @@ import { join } from 'path'; import type { BlogPost } from '../data/content-types'; import { getAllContentFromDirectory } from './content-loaders/getAllContentFromDirectory'; import { getContentBySlug } from './content-loaders/getContentBySlug'; -import { getContentSlugsForTag } from './tags'; // Add markdown files in `src/content/blog` const postsDirectory = join(process.cwd(), 'src', 'data', 'posts'); @@ -24,14 +23,3 @@ export const getAllPosts = async () => { return allPosts; }; - -export const getAllPostsByTag = async (tag: string) => { - try { - const posts = await getAllPosts(); - const slugsForTag = await getContentSlugsForTag(tag); - return posts.filter((post) => slugsForTag.includes(post.slug)); - } catch (error) { - console.error('Error getting posts by tag:', error); - return []; - } -}; diff --git a/src/lib/external-references.ts b/src/lib/external-references.ts index e8ee5d41..7fea8fb5 100644 --- a/src/lib/external-references.ts +++ b/src/lib/external-references.ts @@ -3,7 +3,6 @@ import { join } from 'path'; import type { Article } from '../data/content-types'; import { getAllContentFromDirectory } from './content-loaders/getAllContentFromDirectory'; import { getContentBySlug } from './content-loaders/getContentBySlug'; -import { getContentSlugsForTag } from './tags'; // directory reference to `src/content/external-references` const externalReferencesDirectory = join( @@ -21,7 +20,7 @@ export const getExternalReferenceBySlug = async (slug: string) => { externalReferencesDirectory, EXTERNAL_REFERENCES_CONTENT_TYPE ); - return reference; + return reference as Article; }; export const getAllExternalReferences = async () => { @@ -35,14 +34,3 @@ export const getAllExternalReferences = async () => { return articles; }; - -export const getAllExternalReferencesByTag = async (tag: string) => { - try { - const refs = await getAllExternalReferences(); - const slugsForTag = await getContentSlugsForTag(tag); - return refs.filter((article) => slugsForTag.includes(article.slug)); - } catch (error) { - console.error('Error getting external references by tag:', error); - return []; - } -}; diff --git a/src/lib/newsletters.ts b/src/lib/newsletters.ts index da8aba9b..6b99d1d4 100644 --- a/src/lib/newsletters.ts +++ b/src/lib/newsletters.ts @@ -3,7 +3,6 @@ import { join } from 'path'; import type { MarkdownDocument, Newsletter } from '../data/content-types'; import { getAllContentFromDirectory } from './content-loaders/getAllContentFromDirectory'; import { getContentBySlug } from './content-loaders/getContentBySlug'; -import { getContentSlugsForTag } from './tags'; // directory reference to `src/content/newsletters` export const newslettersDirectory = join( @@ -74,16 +73,3 @@ export const getAllNewsletters = async () => { throw error; } }; - -export const getAllNewslettersByTag = async (tag: string) => { - try { - const newsletters = await getAllNewsletters(); - const slugsForTag = await getContentSlugsForTag(tag); - return newsletters.filter((newsletter) => - slugsForTag.includes(newsletter.slug) - ); - } catch (error) { - console.error('Error getting newsletters by tag:', error); - return []; - } -}; diff --git a/src/lib/tags.ts b/src/lib/tags.ts index 78204315..c3995388 100644 --- a/src/lib/tags.ts +++ b/src/lib/tags.ts @@ -1,171 +1,161 @@ -import fs from 'fs'; -import path from 'path'; +import { join } from 'path'; -import type { MarkdownDocument } from '../data/content-types'; - -const CACHE_FILE = path.join( - process.cwd(), - '.next', - 'cache', - 'tag-registry.json' -); +import { ContentTypes } from '@data/content-types'; +import type { ContentType, MarkdownDocument } from '@data/content-types'; +import { getAllContentFromDirectory } from './content-loaders/getAllContentFromDirectory'; export const parseTag = (tag: string) => { return tag.split(' ').join('-').toLocaleLowerCase(); }; -type TagMap = { - [tag: string]: string[]; // tag -> array of content slugs +export const getAllTags = async () => { + const tagRegistry = await getTagRegistryForAllContent(); + // Convert to array and sort + const uniqueTags = tagRegistry.getAllTags(); + + return uniqueTags; }; -// Singleton to manage tags across the application -class TagRegistry { - private static instance: TagRegistry; - private tagMap: Map> = new Map(); // tag -> set of content slugs - private initialized = false; +export const getContentForTag = async (tag: string) => { + const tagRegistry = await getTagRegistryForAllContent(); - private constructor() {} + return tagRegistry.getContentForTag(tag); +}; - static getInstance(): TagRegistry { - if (!TagRegistry.instance) { - TagRegistry.instance = new TagRegistry(); - } - return TagRegistry.instance; - } +const getTagRegistryForAllContent = async () => { + const tagRegistry = new TagRegistry(); + + // helper function to extract tags from a content item + const extractTags = (content: MarkdownDocument) => { + if (!content) return []; - private registerContentTags(content: MarkdownDocument) { const tags = content?.frontmatter?.tags; - if (!tags || !Array.isArray(tags)) return; + if (!tags) return []; - const slug = content.frontmatter.slug; - if (!slug) return; + if (!Array.isArray(tags)) return []; - tags.forEach((tag) => { - if (typeof tag !== 'string') return; - const parsedTag = parseTag(tag); - if (!parsedTag) return; + return tags; + }; - if (!this.tagMap.has(parsedTag)) { - this.tagMap.set(parsedTag, new Set()); - } - this.tagMap.get(parsedTag)?.add(slug); - }); - } + // Get all posts and extract tags + try { + const postsDir = join(process.cwd(), 'src', 'data', 'posts'); + const posts = await getAllContentFromDirectory(postsDir, 'post'); - registerContent(content: MarkdownDocument[]) { - content.forEach((item) => this.registerContentTags(item)); - this.initialized = true; - } + posts.forEach((post) => { + const postTags = extractTags(post); + if (!postTags) return; - isInitialized(): boolean { - return this.initialized; + postTags.forEach((tag) => + tagRegistry.addTag(tag, post.slug, ContentTypes.Post) + ); + }); + } catch (error) { + console.error('Error in getAllTags for posts:', error); } - getAllTags(): string[] { - return Array.from(this.tagMap.keys()).sort(); + // Get all newsletters and extract tags + try { + const newslettersDir = join(process.cwd(), 'src', 'data', 'newsletters'); + const newsletters = await getAllContentFromDirectory( + newslettersDir, + 'newsletter' + ); + + newsletters.forEach((newsletter) => { + const newsletterTags = extractTags(newsletter); + if (!newsletterTags) return; + + newsletterTags.forEach((tag) => + tagRegistry.addTag(tag, newsletter.slug, ContentTypes.Newsletter) + ); + }); + } catch (error) { + console.error('Error in getAllTags for newsletters:', error); } - getContentSlugsForTag(tag: string): string[] { - const parsedTag = parseTag(tag); - return Array.from(this.tagMap.get(parsedTag) || []); + // Get all external reference articles and extract tags + try { + const articlesDir = join( + process.cwd(), + 'src', + 'data', + 'external-references' + ); + const articles = await getAllContentFromDirectory(articlesDir, 'article'); + + articles.forEach((article) => { + const articleTags = extractTags(article); + if (!articleTags) return; + + articleTags.forEach((tag) => + tagRegistry.addTag(tag, article.slug, ContentTypes.Article) + ); + }); + } catch (error) { + console.error( + 'Error in getAllTags for external reference articles:', + error + ); } - hasTag(tag: string): boolean { - return this.tagMap.has(parseTag(tag)); - } + return tagRegistry; +}; - // Save the current state to disk - saveToCache(): void { - const cacheDir = path.dirname(CACHE_FILE); - if (!fs.existsSync(cacheDir)) { - fs.mkdirSync(cacheDir, { recursive: true }); - } +type TaggedContentSlugs = Record; - const serializedMap: TagMap = {}; - this.tagMap.forEach((slugs, tag) => { - serializedMap[tag] = Array.from(slugs); - }); - - fs.writeFileSync(CACHE_FILE, JSON.stringify(serializedMap, null, 2)); - } +type TagRegistryStorage = { + [tag: string]: TaggedContentSlugs; +}; - // Load state from disk - loadFromCache(): boolean { - try { - if (!fs.existsSync(CACHE_FILE)) { - return false; - } - - const data = JSON.parse(fs.readFileSync(CACHE_FILE, 'utf-8')) as TagMap; - this.tagMap.clear(); - - Object.entries(data).forEach(([tag, slugs]) => { - this.tagMap.set(tag, new Set(slugs)); - }); - - this.initialized = true; - return true; - } catch (error) { - console.error('Error loading tag registry cache:', error); - return false; - } +/* + +Tag library looks like this: +{ + "tag1": { + "post": ["slug1", "slug2"], + "newsletter": ["slug3", "slug4"] + }, + "tag2": { + "post": ["slug1", "slug2"], + "newsletter": ["slug3", "slug4"] } } +*/ -// Export functions that use the registry -export const initializeTagRegistry = async (content: MarkdownDocument[]) => { - try { - const registry = TagRegistry.getInstance(); - - // Try to load from cache first - if (!registry.isInitialized() && !registry.loadFromCache()) { - // If cache doesn't exist or is invalid, rebuild and save - registry.registerContent(content); - registry.saveToCache(); - } - } catch (error) { - console.error('Error initializing tag registry:', error); +class TagRegistry { + // Map of tag name to content paths + private tags: TagRegistryStorage = {}; + + constructor() { + this.tags = {}; } -}; -export const getAllTags = async () => { - try { - const registry = TagRegistry.getInstance(); - if (!registry.isInitialized() && !registry.loadFromCache()) { - console.warn('Tag registry accessed before initialization'); - return []; - } - return registry.getAllTags(); - } catch (error) { - console.error('Error in getAllTags:', error); - return []; + addTag(tag: string, slug: string, contentType: ContentType) { + // Initialize the array if it doesn't exist + this.tags[tag] = this.tags[tag] || { + post: [], + article: [], + newsletter: [], + }; + + // Add the slug to the tag + this.tags[tag][contentType].push(slug); } -}; -export const getContentSlugsForTag = async (tag: string) => { - try { - const registry = TagRegistry.getInstance(); - if (!registry.isInitialized() && !registry.loadFromCache()) { - console.warn('Tag registry accessed before initialization'); - return []; - } - return registry.getContentSlugsForTag(tag); - } catch (error) { - console.error('Error getting content for tag:', error); - return []; + // Return a list of content paths for a given tag + getContentForTag(tag: string) { + const content = this.tags[tag] || { + post: [], + article: [], + newsletter: [], + }; + + return content; } -}; -export const hasTag = async (tag: string) => { - try { - const registry = TagRegistry.getInstance(); - if (!registry.isInitialized() && !registry.loadFromCache()) { - console.warn('Tag registry accessed before initialization'); - return false; - } - return registry.hasTag(tag); - } catch (error) { - console.error('Error checking tag existence:', error); - return false; + // Return a list of all tags sorted alphabetically + getAllTags() { + return Object.keys(this.tags).sort(); } -}; +} diff --git a/src/pages/tags/[tag].tsx b/src/pages/tags/[tag].tsx index 261aa1e8..44066813 100644 --- a/src/pages/tags/[tag].tsx +++ b/src/pages/tags/[tag].tsx @@ -1,15 +1,15 @@ import type { GetStaticPaths, GetStaticProps } from 'next'; +import { getPostBySlug } from '@lib/blog'; +import { getNewsletterBySlug } from '@lib/newsletters'; import { ExternalWorkItem } from '../../components/ExternalWork'; import { Heading } from '../../components/Heading'; import NewsletterItem from '../../components/NewsletterFeed/NewsletterItem'; import { BlogPost as Post } from '../../components/Post'; import SEO from '../../components/seo'; import type { Article, BlogPost, Newsletter } from '../../data/content-types'; -import { getAllPostsByTag } from '../../lib/blog'; -import { getAllExternalReferencesByTag } from '../../lib/external-references'; -import { getAllNewslettersByTag } from '../../lib/newsletters'; -import { getAllTags } from '../../lib/tags'; +import { getExternalReferenceBySlug as getArticleBySlug } from '../../lib/external-references'; +import { getAllTags, getContentForTag } from '../../lib/tags'; type TagPageParams = { tag: string; @@ -33,9 +33,13 @@ export const getStaticProps: GetStaticProps< const { tag } = params; try { - const posts = await getAllPostsByTag(tag); - const articles = await getAllExternalReferencesByTag(tag); - const newsletters = await getAllNewslettersByTag(tag); + const content = await getContentForTag(tag); + + const [posts, articles, newsletters] = await Promise.all([ + Promise.all(content.post.map((slug) => getPostBySlug(slug))), + Promise.all(content.article.map((slug) => getArticleBySlug(slug))), + Promise.all(content.newsletter.map((slug) => getNewsletterBySlug(slug))), + ]); return { props: {