Skip to content

Commit

Permalink
feat(blog): add author avatars
Browse files Browse the repository at this point in the history
https://mozilla-hub.atlassian.net/browse/MP-467

move from using one type for both frontmatter and parsed metadata, to
one for frontmatter and one for the parsed metadata

support using a separate author frontmatter file, while maintaining
backwards compatibility
  • Loading branch information
LeoMcA committed Jun 22, 2023
1 parent 394c348 commit 25a3593
Show file tree
Hide file tree
Showing 9 changed files with 181 additions and 49 deletions.
83 changes: 75 additions & 8 deletions build/blog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,11 @@ import { BLOG_ROOT, BUILD_OUT_ROOT, BASE_URL } from "../libs/env/index.js";
import {
BlogPostData,
BlogPostFrontmatter,
BlogPostFrontmatterLinks,
BlogPostLimitedFrontmatter,
BlogPostMetadataLinks,
BlogPostLimitedMetadata,
BlogPostMetadata,
AuthorFrontmatter,
AuthorMetadata,
} from "../libs/types/blog.js";
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
Expand Down Expand Up @@ -47,40 +50,68 @@ function calculateReadTime(copy: string): number {
);
}

async function getLinks(slug: string): Promise<BlogPostFrontmatterLinks> {
async function getLinks(slug: string): Promise<BlogPostMetadataLinks> {
const posts = await allPostFrontmatter();
const index = posts.findIndex((post) => post.slug === slug);
const filterFrontmatter = (
f?: BlogPostFrontmatter
): BlogPostLimitedFrontmatter => f && { title: f.title, slug: f.slug };
): BlogPostLimitedMetadata => f && { title: f.title, slug: f.slug };
return {
previous: filterFrontmatter(posts[index + 1]),
next: filterFrontmatter(posts[index - 1]),
};
}

async function getAuthor(slug: string | AuthorFrontmatter) {
if (typeof slug === "string") {
const filePath = path.join(BLOG_ROOT, "..", "authors", slug, "index.md");
const { attributes } = await readAuthor(filePath);
return parseAuthor(slug, attributes);
}
return slug;
}

async function readAuthor(filePath: string) {
const raw = await fs.readFile(filePath, "utf-8");
return frontmatter<AuthorFrontmatter>(raw);
}

function parseAuthor(slug: string, { name, link, avatar }: AuthorFrontmatter) {
return {
name,
link,
avatar_url: avatar
? `/${DEFAULT_LOCALE}/blog/author/${slug}/${avatar}`
: undefined,
} satisfies AuthorMetadata;
}

async function readPost(
file: string,
options?: {
readTime?: boolean;
previousNext?: boolean;
}
): Promise<{ blogMeta: BlogPostFrontmatter; body: string }> {
): Promise<{ blogMeta: BlogPostMetadata; body: string }> {
const raw = await fs.readFile(file, "utf-8");

const { attributes, body } = frontmatter<BlogPostFrontmatter>(raw);
const blogMeta: BlogPostMetadata = {
...attributes,
author: await getAuthor(attributes.author),
};

const { readTime = true, previousNext = true } = options || {};

if (readTime) {
attributes.readTime = calculateReadTime(body);
blogMeta.readTime = calculateReadTime(body);
}

if (previousNext) {
attributes.links = await getLinks(attributes.slug);
blogMeta.links = await getLinks(blogMeta.slug);
}

return { blogMeta: attributes, body };
return { blogMeta, body };
}

export function findPostPathBySlug(slug: string): string | null {
Expand Down Expand Up @@ -145,6 +176,15 @@ async function allPostFiles(): Promise<string[]> {
return await api.withPromise();
}

async function allAuthorFiles(): Promise<string[]> {
return await new fdir()
.withFullPaths()
.withErrors()
.filter((filePath) => filePath.endsWith("index.md"))
.crawl(path.join(BLOG_ROOT, "..", "authors"))
.withPromise();
}

export const allPostFrontmatter = memoize(
async ({
includeUnpublished,
Expand Down Expand Up @@ -396,3 +436,30 @@ export async function buildBlogFeed(options: { verbose?: boolean }) {
console.log("Wrote", filePath);
}
}

export async function buildAuthors(options: { verbose?: boolean }) {
for (const filePath of await allAuthorFiles()) {
const dirname = path.dirname(filePath);
const slug = path.basename(dirname);
const outPath = path.join(
BUILD_OUT_ROOT,
DEFAULT_LOCALE.toLowerCase(),
"blog",
"author",
slug
);
await fs.mkdir(outPath, { recursive: true });

const {
attributes: { avatar },
} = await readAuthor(filePath);
if (avatar) {
const from = path.join(dirname, avatar);
const to = path.join(outPath, avatar);
await fs.copyFile(from, to);
if (options.verbose) {
console.log("Copied", from, "to", to);
}
}
}
}
8 changes: 7 additions & 1 deletion build/build-blog.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
import { buildBlogFeed, buildBlogIndex, buildBlogPosts } from "./blog.js";
import {
buildAuthors,
buildBlogFeed,
buildBlogIndex,
buildBlogPosts,
} from "./blog.js";

await buildBlogIndex({ verbose: true });
await buildBlogPosts({ verbose: true });
await buildAuthors({ verbose: true });
await buildBlogFeed({ verbose: true });
12 changes: 10 additions & 2 deletions client/src/blog/index.scss
Original file line number Diff line number Diff line change
Expand Up @@ -59,9 +59,10 @@
flex-direction: column;

figure.blog-image {
margin-bottom: 0.5rem;
margin-bottom: 0;

img {
margin-bottom: 0;
max-height: 200px;
width: auto;
}
Expand All @@ -71,13 +72,20 @@
font: var(--type-heading-h3);
font-size: 1.75rem;
font-weight: 400;
margin-top: var(--sponsored-height);
margin-bottom: 2.25rem;
margin-top: 1.5rem;
}

.sponsored ~ h2:first-of-type {
margin-top: 0;
}
}

p {
margin-bottom: 2.25rem;
margin-top: 1.5rem;
}

@media screen and (min-width: 50rem) {
width: max(40vw, 20rem);
}
Expand Down
6 changes: 3 additions & 3 deletions client/src/blog/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,15 @@ import { WRITER_MODE } from "../env";
import { Route, Routes } from "react-router-dom";
import { HydrationData } from "../../../libs/types/hydration";
import { BlogPost, AuthorDateReadTime } from "./post";
import { BlogImage, BlogPostFrontmatter } from "../../../libs/types/blog.js";
import { BlogImage, BlogPostMetadata } from "../../../libs/types/blog.js";

import "./index.scss";
import "./post.scss";
import { Button } from "../ui/atoms/button";
import { SignUpSection as NewsletterSignUp } from "../newsletter";

interface BlogIndexData {
posts: BlogPostFrontmatter[];
posts: BlogPostMetadata[];
}

export function Blog(appProps: HydrationData) {
Expand Down Expand Up @@ -46,7 +46,7 @@ export function BlogIndexImageFigure({
);
}

function PostPreview({ fm }: { fm: BlogPostFrontmatter }) {
function PostPreview({ fm }: { fm: BlogPostMetadata }) {
return (
<article>
<header>
Expand Down
20 changes: 20 additions & 0 deletions client/src/blog/post.scss
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,28 @@
}

.blog-container {
.date-author,
.author {
--date-author-gap: 1rem;
align-items: center;
display: flex;
flex-wrap: wrap;
gap: var(--date-author-gap);
}

.author {
font-weight: var(--font-body-strong-weight);

&::after {
margin-left: calc(4px - var(--date-author-gap));
}

img {
border: none !important;
border-radius: 3rem;
height: 3rem;
margin: 0;
}
}

.read-time {
Expand Down
47 changes: 29 additions & 18 deletions client/src/blog/post.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,25 +9,33 @@ import "./post.scss";
import {
BlogImage,
BlogPostData,
BlogPostFrontmatter,
BlogPostFrontmatterLinks,
BlogPostLimitedFrontmatter,
BlogPostMetadata,
BlogPostMetadataLinks,
BlogPostLimitedMetadata,
AuthorMetadata,
} from "../../../libs/types/blog";
import { useCopyExamplesToClipboard } from "../document/hooks";
import { DEFAULT_LOCALE } from "../../../libs/constants";
import { SignUpSection as NewsletterSignUp } from "../newsletter";

function MaybeLink({ link, children }) {
function MaybeLink({ className = "", link, children }) {
return link ? (
link.startsWith("https://") ? (
<a href={link} className="external" target="_blank" rel="noreferrer">
<a
href={link}
className={`external ${className}`}
target="_blank"
rel="noreferrer"
>
{children}
</a>
) : (
<a href={link}>{children}</a>
<a href={link} className={className}>
{children}
</a>
)
) : (
<>{children}</>
<span className={className}>{children}</span>
);
}

Expand All @@ -48,26 +56,29 @@ export function PublishDate({ date }: { date: string }) {
);
}

export function Author({ metadata }: { metadata: BlogPostFrontmatter }) {
export function Author({ metadata }: { metadata: AuthorMetadata | undefined }) {
return (
<MaybeLink link={metadata?.author?.link}>
<span className="author">{metadata?.author?.name || "The MDN Team"}</span>
<MaybeLink link={metadata?.link} className="author">
<img
src={metadata?.avatar_url ?? "/assets/avatar.png"}
alt="Author avatar"
/>
{metadata?.name || "The MDN Team"}
</MaybeLink>
);
}

export function AuthorDateReadTime({
metadata,
}: {
metadata: BlogPostFrontmatter;
metadata: BlogPostMetadata;
}) {
return (
<span className="date-author">
<Author metadata={metadata} />
<br />
<PublishDate date={metadata.date} />{" "}
<div className="date-author">
<Author metadata={metadata.author} />
<PublishDate date={metadata.date} />
<TimeToRead readTime={metadata.readTime} />
</span>
</div>
);
}

Expand Down Expand Up @@ -113,7 +124,7 @@ function BlogImageFigure({
function PreviousNext({
links: { previous, next },
}: {
links: BlogPostFrontmatterLinks;
links: BlogPostMetadataLinks;
}) {
return (
<section className="previous-next">
Expand All @@ -130,7 +141,7 @@ function PreviousNextLink({
metadata: { slug, title },
}: {
direction: "Previous" | "Next";
metadata: BlogPostLimitedFrontmatter;
metadata: BlogPostLimitedMetadata;
}) {
return (
<a
Expand Down
Loading

0 comments on commit 25a3593

Please sign in to comment.