Zachary DeRose f7eab14f6b
Adding podcast episodes (#27462)
<!-- Please make sure you have read the submission guidelines before
posting an PR -->
<!--
https://github.com/nrwl/nx/blob/master/CONTRIBUTING.md#-submitting-a-pr
-->

<!-- Please make sure that your commit message follows our format -->
<!-- Example: `fix(nx): must begin with lowercase` -->

<!-- If this is a particularly complex change or feature addition, you
can request a dedicated Nx release for this pull request branch. Mention
someone from the Nx team or the `@nrwl/nx-pipelines-reviewers` and they
will confirm if the PR warrants its own release for testing purposes,
and generate it for you if appropriate. -->

## Current Behavior
<!-- This is the behavior we have today -->

## Expected Behavior
<!-- This is the behavior we should expect with the changes in this PR
-->

## Related Issue(s)
<!-- Please link the issue being fixed so it gets closed when this is
merged. -->

Fixes #
2024-08-15 16:12:25 -04:00

167 lines
5.1 KiB
TypeScript

import { readFileSync, accessSync, constants } from 'fs';
import { join, basename, parse, resolve } from 'path';
import { extractFrontmatter } from '@nx/nx-dev/ui-markdoc';
import { sortPosts } from './blog.util';
import { BlogPostDataEntry } from './blog.model';
import { readFile, readdir } from 'fs/promises';
export class BlogApi {
constructor(
private readonly options: {
id: string;
blogRoot: string;
}
) {
if (!options.id) {
throw new Error('id cannot be undefined');
}
if (!options.blogRoot) {
throw new Error('public blog root cannot be undefined');
}
}
async getBlogTags(): Promise<string[]> {
const blogs = await this.getBlogs();
const tags = new Set<string>();
blogs.forEach((blog) => {
blog.tags.forEach((tag) => tags.add(tag));
});
return Array.from(tags);
}
async getBlogs(
filterFn?: (post: BlogPostDataEntry) => boolean
): Promise<BlogPostDataEntry[]> {
return await this.getAllBlogs(filterFn);
}
async getAllBlogs(
filterFn?: (post: BlogPostDataEntry) => boolean
): Promise<BlogPostDataEntry[]> {
const files: string[] = await readdir(this.options.blogRoot);
const authors = JSON.parse(
readFileSync(join(this.options.blogRoot, 'authors.json'), 'utf8')
);
const allPosts: BlogPostDataEntry[] = [];
for (const file of files) {
const filePath = join(this.options.blogRoot, file);
if (!filePath.endsWith('.md')) continue;
const content = await readFile(filePath, 'utf8');
const frontmatter = extractFrontmatter(content);
const slug = this.calculateSlug(filePath, frontmatter);
const { image, type } = this.determineOgImage(frontmatter.cover_image);
const post = {
content,
title: frontmatter.title ?? null,
description: frontmatter.description ?? null,
authors: authors.filter((author) =>
frontmatter.authors.includes(author.name)
),
date: this.calculateDate(file, frontmatter),
cover_image: frontmatter.cover_image
? `/documentation${frontmatter.cover_image}` // Match the prefix used by markdown parser
: null,
tags: frontmatter.tags ?? [],
reposts: frontmatter.reposts ?? [],
pinned: frontmatter.pinned ?? false,
ogImage: image,
ogImageType: type,
filePath,
slug,
podcastYoutubeId: frontmatter.podcastYoutubeId,
podcastSpotifyId: frontmatter.podcastSpotifyId,
podcastIHeartUrl: frontmatter.podcastIHeartUrl,
podcastAppleUrl: frontmatter.podcastAppleUrl,
podcastAmazonUrl: frontmatter.podcastAmazonUrl,
};
const isDevelopment = process.env.NODE_ENV === 'development';
const shouldIncludePost = !frontmatter.draft || isDevelopment;
if (shouldIncludePost && (!filterFn || filterFn(post))) {
allPosts.push(post);
}
}
return sortPosts(allPosts);
}
// Optimize this so we don't read the FS multiple times
async getBlogPostBySlug(
slug: string | null
): Promise<BlogPostDataEntry | undefined> {
if (!slug) throw new Error(`Could not find blog post with slug: ${slug}`);
return (await this.getBlogs()).find((post) => post.slug === slug);
}
private calculateSlug(filePath: string, frontmatter: any): string {
const baseName = basename(filePath, '.md');
return frontmatter.slug || baseName;
}
private calculateDate(filename: string, frontmatter: any): string {
const date: Date = new Date();
const timeString = date.toTimeString();
if (frontmatter.date) {
return new Date(frontmatter.date + ' ' + timeString).toISOString();
} else {
const regexp = /^(\d\d\d\d-\d\d-\d\d).+$/;
const match = filename.match(regexp);
if (match) {
return new Date(match[1] + ' ' + timeString).toISOString();
} else {
throw new Error(`Could not parse date from filename: ${filename}`);
}
}
}
private fileExists(filePath: string): boolean {
try {
accessSync(filePath, constants.F_OK);
return true;
} catch (error) {
return false;
}
}
private determineOgImage(imagePath: string): {
image: string;
type: string;
} {
const allowedExtensions = ['.png', '.webp', '.jpg', '.jpeg'];
const defaultImage = 'https://nx.dev/socials/nx-media.png';
const defaultType = 'png';
if (!imagePath) {
return { image: defaultImage, type: defaultType };
}
const { ext } = parse(imagePath);
if (!allowedExtensions.includes(ext)) {
const foundExt = allowedExtensions.find((allowedExt) => {
const ogImagePath = imagePath.replace(ext, allowedExt);
return this.fileExists(
join(
'public',
'documentation',
resolve(this.options.blogRoot, ogImagePath)
)
);
});
if (!foundExt) {
return { image: defaultImage, type: defaultType };
}
return {
image: join('documentation', imagePath.replace(ext, foundExt)),
type: foundExt.replace('.', ''),
};
}
return {
image: join('documentation', imagePath),
type: ext.replace('.', ''),
};
}
}