Skip to content

Commit

Permalink
fix: separate tag stuff from content stuff
Browse files Browse the repository at this point in the history
  • Loading branch information
mbifulco committed Feb 16, 2025
1 parent cbff005 commit fa4b8c0
Show file tree
Hide file tree
Showing 5 changed files with 137 additions and 181 deletions.
12 changes: 0 additions & 12 deletions src/lib/blog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand All @@ -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 [];
}
};
14 changes: 1 addition & 13 deletions src/lib/external-references.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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 () => {
Expand All @@ -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 [];
}
};
14 changes: 0 additions & 14 deletions src/lib/newsletters.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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 [];
}
};
260 changes: 125 additions & 135 deletions src/lib/tags.ts
Original file line number Diff line number Diff line change
@@ -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<string, Set<string>> = 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<ContentType, string[]>;

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();
}
};
}
Loading

0 comments on commit fa4b8c0

Please sign in to comment.