Skip to content
Draft
Show file tree
Hide file tree
Changes from all 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
180 changes: 180 additions & 0 deletions apps/website/app/(home)/authors/[slug]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
import type { Metadata } from "next";
import type { ReactElement } from "react";
import Image from "next/image";
import Link from "next/link";
import { notFound } from "next/navigation";
import { ArrowLeft, ExternalLink } from "lucide-react";
import {
AUTHOR_PROFILES,
getAuthorProfileBySlug,
type AuthorWork,
} from "~/data/authorProfiles";

type AuthorPageProps = {
params: Promise<{
slug: string;
}>;
};

const WorkList = ({ items }: { items: AuthorWork[] }): ReactElement => (
<ul className="mt-5 divide-y divide-neutral-dark/10 border-y border-neutral-dark/10">
{items.map((item) => (
<li key={item.href} className="py-5">
<Link
href={item.href}
className="group flex items-start justify-between gap-4"
>
<span>
<span className="block font-semibold text-neutral-dark transition-colors group-hover:text-secondary">
{item.title}
</span>
<span className="mt-1 block text-sm text-neutral-dark/60">
{item.label}
</span>
</span>
<ExternalLink
className="mt-1 h-4 w-4 shrink-0 text-neutral-dark/45"
aria-hidden="true"
/>
</Link>
</li>
))}
</ul>
);

export const generateStaticParams = (): Array<{ slug: string }> =>
AUTHOR_PROFILES.map(({ slug }) => ({ slug }));

export const generateMetadata = async ({
params,
}: AuthorPageProps): Promise<Metadata> => {
const { slug } = await params;
const profile = getAuthorProfileBySlug(slug);

if (!profile) {
return { title: "Author" };
}

return {
title: `${profile.name} | Discourse Graphs`,
description: profile.summary,
alternates: {
canonical: `/authors/${profile.slug}`,
},
openGraph: {
title: `${profile.name} | Discourse Graphs`,
description: profile.summary,
type: "profile",
images: [{ url: profile.image, alt: profile.name }],
url: `/authors/${profile.slug}`,
},
};
};

const AuthorPage = async ({
params,
}: AuthorPageProps): Promise<ReactElement> => {
const { slug } = await params;
const profile = getAuthorProfileBySlug(slug);

if (!profile) {
notFound();
}

return (
<main className="flex-1 px-5 py-12 sm:px-6 lg:py-20">
<article className="mx-auto max-w-5xl">
<Link
href="/#team"
className="inline-flex items-center gap-2 text-sm font-semibold text-secondary transition-colors hover:text-secondary/70"
>
<ArrowLeft className="h-4 w-4" aria-hidden="true" />
Back to the team
</Link>

<header className="mt-8 grid gap-8 border-b border-neutral-dark/10 pb-10 sm:grid-cols-[10rem_1fr] sm:items-center">
<div className="relative h-40 w-40 overflow-hidden rounded-full bg-white">
<Image
src={profile.image}
alt={profile.name}
fill
priority
sizes="160px"
className="object-cover"
/>
</div>
<div>
<p className="text-sm font-semibold uppercase tracking-[0.18em] text-secondary">
Author profile
</p>
<h1 className="mt-3 text-4xl font-semibold tracking-tight text-primary sm:text-5xl">
{profile.name}
</h1>
<p className="mt-4 text-lg leading-8 text-neutral-dark/75">
{profile.summary}
</p>
</div>
</header>

<div className="mt-10 grid gap-10 lg:grid-cols-[0.7fr_1.3fr]">
<aside className="space-y-8">
<section>
<h2 className="text-xl font-semibold text-primary">
Affiliations
</h2>
<ul className="mt-4 space-y-3">
{profile.affiliations.map((affiliation) => (
<li key={`${affiliation.name}-${affiliation.role}`}>
<Link
href={affiliation.href}
className="font-semibold text-neutral-dark transition-colors hover:text-secondary"
>
{affiliation.name}
</Link>
<p className="text-sm text-neutral-dark/60">
{affiliation.role}
</p>
</li>
))}
</ul>
</section>

{profile.externalProfiles.length > 0 && (
<section>
<h2 className="text-xl font-semibold text-primary">Profiles</h2>
<ul className="mt-4 space-y-3">
{profile.externalProfiles.map((externalProfile) => (
<li key={externalProfile.href}>
<Link
href={externalProfile.href}
className="inline-flex items-center gap-2 font-semibold text-secondary transition-colors hover:text-secondary/70"
>
{externalProfile.label}
<ExternalLink className="h-4 w-4" aria-hidden="true" />
</Link>
</li>
))}
</ul>
</section>
)}
</aside>

<div className="space-y-10">
<section>
<h2 className="text-2xl font-semibold text-primary">
Publications
</h2>
<WorkList items={profile.publications} />
</section>
<section>
<h2 className="text-2xl font-semibold text-primary">Talks</h2>
<WorkList items={profile.talks} />
</section>
</div>
</div>
</article>
</main>
);
};

export default AuthorPage;
17 changes: 15 additions & 2 deletions apps/website/app/(home)/blog/[slug]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import {
createBreadcrumbStructuredData,
createStructuredDataDocument,
} from "~/utils/structuredData";
import { AuthorLink } from "~/components/AuthorLink";
import { getAuthorProfileByName } from "~/data/authorProfiles";
import type { BlogData } from "../blogSchema";
import { getAllBlogs, getBlogBySlug } from "../readBlogs";
import { getBlogPostPath, getCanonicalMetadata, getCanonicalUrl } from "~/seo";
Expand Down Expand Up @@ -47,12 +49,18 @@ const buildBlogPostMetadata = ({
metadata: pageMetadata,
});
const canonicalPath = getBlogPostPath(blog.slug);
const authorProfile = getAuthorProfileByName(blog.author);

return {
...pageMetadata,
title: blog.title,
description,
authors: [{ name: blog.author }],
authors: [
{
name: blog.author,
url: authorProfile ? `/authors/${authorProfile.slug}` : undefined,
},
],
keywords: blog.tags.length ? blog.tags : pageMetadata.keywords,
alternates: {
...pageMetadata.alternates,
Expand Down Expand Up @@ -119,7 +127,12 @@ const BlogPost = async ({ params }: Params): Promise<React.ReactElement> => {
)}
<div className={showsPrimaryHeading ? "mb-6" : "mb-8 mt-4"}>
<p className="text-sm italic text-gray-500">
By {blog.author} | {blog.date}
By{" "}
<AuthorLink
authorName={blog.author}
className="decoration-current/35 underline underline-offset-4 hover:text-blue-600"
/>{" "}
| {blog.date}
</p>
{blog.tags.length > 0 && (
<ul className="mt-4 flex flex-wrap gap-2">
Expand Down
7 changes: 6 additions & 1 deletion apps/website/app/(home)/blog/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
createBreadcrumbStructuredData,
createStructuredDataDocument,
} from "~/utils/structuredData";
import { AuthorLink } from "~/components/AuthorLink";
import { getAllBlogs } from "./readBlogs";
import { getCanonicalMetadata, PUBLIC_STATIC_PATHS } from "~/seo";

Expand Down Expand Up @@ -67,7 +68,11 @@ const BlogIndex = async (): Promise<React.ReactElement> => {
</p>
</div>
<div className="w-1/5 text-right text-gray-600">
by {blog.author}
by{" "}
<AuthorLink
authorName={blog.author}
className="decoration-current/35 underline underline-offset-4 hover:text-blue-600"
/>
</div>
</li>
))
Expand Down
34 changes: 23 additions & 11 deletions apps/website/app/(home)/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
Sparkles,
} from "lucide-react";
import { getLatestBlogs } from "~/(home)/blog/readBlogs";
import { AuthorLink, LinkedAuthorText } from "~/components/AuthorLink";
import { Logo } from "~/components/Logo";
import { PlatformBadge } from "~/components/PlatformBadge";
import { TeamPerson } from "~/components/TeamPerson";
Expand Down Expand Up @@ -622,25 +623,32 @@ const Home = async (): Promise<ReactElement> => {
</div>
<div className="mt-10 grid gap-4 lg:grid-cols-2">
{RESOURCE_LINKS.map((resource) => (
<Link
<article
key={resource.href}
href={resource.href}
className="group rounded-lg border border-neutral-dark/10 bg-neutral-light p-5 transition-colors hover:border-secondary"
className="rounded-lg border border-neutral-dark/10 bg-neutral-light p-5 transition-colors hover:border-secondary"
>
<p className="text-sm font-semibold text-secondary">
{resource.meta}
<LinkedAuthorText text={resource.meta} />
</p>
<h3 className="mt-2 text-xl font-semibold text-neutral-dark">
{resource.title}
<h3 className="mt-2 text-xl font-semibold">
<Link
href={resource.href}
className="text-neutral-dark transition-colors hover:text-secondary"
>
{resource.title}
</Link>
</h3>
<p className="mt-4 inline-flex items-center gap-2 text-sm font-semibold text-primary">
<Link
href={resource.href}
className="group mt-4 inline-flex items-center gap-2 text-sm font-semibold text-primary"
>
Open resource
<ArrowRight
className="h-4 w-4 transition-transform group-hover:translate-x-1"
aria-hidden="true"
/>
</p>
</Link>
</Link>
</article>
))}
</div>
</div>
Expand Down Expand Up @@ -710,7 +718,11 @@ const Home = async (): Promise<ReactElement> => {
{blog.title}
</Link>
<p className="mt-6 text-sm text-neutral-dark/60">
By {blog.author}
By{" "}
<AuthorLink
authorName={blog.author}
className="decoration-current/35 underline underline-offset-4 transition-colors hover:text-secondary"
/>
</p>
</CardContent>
</Card>
Expand Down Expand Up @@ -757,7 +769,7 @@ const Home = async (): Promise<ReactElement> => {
{talk.title}
</h3>
<p className="mt-2 text-sm text-neutral-dark/65">
{talk.speakers}
<LinkedAuthorText text={talk.speakers} />
</p>
</div>
</article>
Expand Down
44 changes: 44 additions & 0 deletions apps/website/app/components/AuthorLink.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import type { ReactElement, ReactNode } from "react";
import Link from "next/link";
import { getAuthorProfileByName } from "~/data/authorProfiles";

const AUTHOR_NAME_PATTERN = /(Matthew Akamatsu|Matt Akamatsu|Joel Chan)/gu;

type AuthorLinkProps = {
authorName: string;
className?: string;
};

export const AuthorLink = ({
authorName,
className,
}: AuthorLinkProps): ReactElement => {
const profile = getAuthorProfileByName(authorName);

if (!profile) {
return <span className={className}>{authorName}</span>;
}

return (
<Link href={`/authors/${profile.slug}`} className={className}>
{authorName}
</Link>
);
};

export const LinkedAuthorText = ({ text }: { text: string }): ReactNode[] =>
text.split(AUTHOR_NAME_PATTERN).map((segment, index) => {
const profile = getAuthorProfileByName(segment);

if (!profile) {
return segment;
}

return (
<AuthorLink
key={`${profile.slug}-${index}`}
authorName={segment}
className="decoration-current/35 underline underline-offset-4 transition-colors hover:text-primary"
/>
);
});
Loading