Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(blog): add author avatars #9131

Merged
merged 6 commits into from
Jun 26, 2023
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading