Skip to content
Merged
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
4 changes: 4 additions & 0 deletions website/astro.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,11 @@ export default defineConfig({
lastmod: new Date(),
i18n: { defaultLocale: DEFAULT_LOCALE, locales: LOCALES_MAP },
filter: (page) => {
const path = new URL(page).pathname.replace(/\/$/, "") || "/";
const isTeamIndex =
path === "/team" || /\/[a-z]{2}-[A-Z]{2}\/team$/.test(path);
return (
!isTeamIndex &&
!page.includes("/tags/") &&
!page.includes("/account/") &&
!page.includes("/oauth/") &&
Expand Down
93 changes: 60 additions & 33 deletions website/src/components/features/about/TeamSection.astro
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,13 @@ import JosephMattia from "@/assets/team/joe-mattia.webp";
import NikolausHager from "@/assets/team/nikolaus-hager.webp";
import JonathanAngle from "@/assets/team/jonathan-angle.webp";
import JarrodJayFrankel from "@/assets/team/jarrod-jay-frankel.webp";
import { createTranslator, getLocaleFromUrl } from "@/utils/i18n";
import {
createTranslator,
getLocaleFromUrl,
getLocalizedPath,
} from "@/utils/i18n";
import { TEAMS } from "@/constants/teams";
import { isPersonId } from "@/constants/people";

const locale = getLocaleFromUrl(Astro.url.pathname);
const t = await createTranslator(locale);
Expand All @@ -32,40 +37,54 @@ const TEAM_IMAGES = {
</h2>
<div class="team-grid">
{
TEAMS.map((team) => (
<div class="team-card about-animate" data-delay="0">
<div class="team-img-wrap">
<a
href={team.social.url}
target="_blank"
rel="noopener"
style="display:block;line-height:0;"
>
<Image src={TEAM_IMAGES[team.id]} alt={team.name} />
</a>
</div>
<div class="team-card-body">
<span class="team-name">{team.name}</span>
<span class="team-role">{team.role}</span>
<a
href={team.social.url}
target="_blank"
rel="noopener"
class="team-x-link"
>
<svg
viewBox="0 0 24 24"
width="12"
height="12"
fill="currentColor"
TEAMS.map((team) => {
const profileHref = isPersonId(team.id)
? getLocalizedPath(locale, `/team/${team.id}`)
: undefined;
const photoHref = profileHref ?? team.social.url;
const photoIsExternal = !profileHref;

return (
<div class="team-card about-animate" data-delay="0">
<div class="team-img-wrap">
<a
href={photoHref}
target={photoIsExternal ? "_blank" : undefined}
rel={photoIsExternal ? "noopener" : undefined}
style="display:block;line-height:0;"
>
<Image src={TEAM_IMAGES[team.id]} alt={team.name} />
</a>
</div>
<div class="team-card-body">
{profileHref ? (
<a href={profileHref} class="team-name-link">
<span class="team-name">{team.name}</span>
</a>
) : (
<span class="team-name">{team.name}</span>
)}
<span class="team-role">{team.role}</span>
<a
href={team.social.url}
target="_blank"
rel="noopener"
class="team-x-link"
>
<path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z" />
</svg>
@{team.social.username}
</a>
<svg
viewBox="0 0 24 24"
width="12"
height="12"
fill="currentColor"
>
<path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z" />
</svg>
{team.social.username}
</a>
</div>
</div>
</div>
))
);
})
}
</div>
</div>
Expand Down Expand Up @@ -148,6 +167,14 @@ const TEAM_IMAGES = {
display: block;
}

.team-name-link {
text-decoration: none;
}

.team-name-link:hover .team-name {
color: #ff6b35;
}

.team-role {
font-family: "Geist Mono", monospace;
font-size: 11px;
Expand Down
45 changes: 34 additions & 11 deletions website/src/components/features/blog/BlogPost.astro
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,18 @@
import Layout from "@/components/layout/Layout.astro";
import Card from "@/components/features/blog/Card.astro";
import type { CollectionEntry } from "astro:content";
import type { Article, WithContext } from "schema-dts";
import type { Article } from "schema-dts";
import {
getLocaleFromUrl,
getLocalizedPath,
createTranslator,
} from "@/utils/i18n";
import env from "@/config";
import { createMetadata } from "@/utils/create-metadata";
import { JsonLdGraph } from "@/utils/build-json-ld";
import { PEOPLE, isPersonId, personId } from "@/constants/people";
import { getPersonJsonLd } from "@/constants/person-jsonld";
import { organizationJsonLd } from "@/constants/default-jsonld";
import env from "@/config";

type Props = CollectionEntry<"blog">["data"] & {
relatedPosts?: CollectionEntry<"blog">[];
Expand All @@ -23,6 +27,7 @@ const {
heroImage,
heroAlt,
tags = [],
author: authorId,
relatedPosts = [],
} = Astro.props;

Expand All @@ -38,25 +43,32 @@ const metadata = createMetadata({
});

const blogIndexHref = getLocalizedPath(currentLocale, "/blog");
const author = authorId && isPersonId(authorId) ? PEOPLE[authorId] : undefined;

const articleSchema: WithContext<Article> = {
"@context": "https://schema.org",
const article: Article = {
"@type": "Article",
"@id": metadata.canonical as string,
headline: title,
description: description,
url: metadata.canonical as string,
image: metadata.openGraph?.basic?.image,
datePublished: pubDate.toISOString(),
dateModified: updatedDate?.toISOString() || pubDate.toISOString(),
author: {
"@type": "Organization",
name: env.SITE_NAME,
url: env.SITE_BASE_URL,
},
author: author
? { "@id": personId(author.id) }
: { "@id": env.SITE_BASE_URL },
publisher: { "@id": env.SITE_BASE_URL },
};

const jsonLd = JsonLdGraph({
"@context": "https://schema.org",
"@graph": author
? [article, getPersonJsonLd(author.id), organizationJsonLd]
: [article, organizationJsonLd],
});
---

<Layout {...metadata} jsonLd={articleSchema}>
<Layout {...metadata} jsonLd={jsonLd}>
<article class="blog-post">
<section class="bg-void">
<div
Expand Down Expand Up @@ -105,7 +117,18 @@ const articleSchema: WithContext<Article> = {
}
</time>
<span class="text-content-35">&nbsp;·&nbsp;</span>
<span>{t("blog.post.author")}</span>
{
author ? (
<a
href={getLocalizedPath(currentLocale, `/team/${author.id}`)}
class="hover:text-flare text-inherit no-underline transition-colors"
>
{author.name}
</a>
) : (
<span>{t("blog.post.author")}</span>
)
}
</p>
{
updatedDate && (
Expand Down
164 changes: 164 additions & 0 deletions website/src/components/features/team/PersonPage.astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
---
import { Image } from "astro:assets";
import Layout from "@/components/layout/Layout.astro";
import { PEOPLE, type PersonId } from "@/constants/people";
import { PERSON_IMAGES } from "@/constants/person-images";
import { getProfilePageJsonLd } from "@/constants/person-jsonld";
import { createMetadata } from "@/utils/create-metadata";
import {
createTranslator,
getLocaleFromUrl,
getLocalizedPath,
} from "@/utils/i18n";

interface Props {
slug: PersonId;
}

const { slug } = Astro.props;
const person = PEOPLE[slug];
const image = PERSON_IMAGES[slug];

const locale = getLocaleFromUrl(Astro.url.pathname);
const t = await createTranslator(locale);

const metadata = createMetadata({
title: t(`team.people.${slug}.meta.title`),
description: t(`team.people.${slug}.meta.description`),
pathname: Astro.url.pathname,
imageUrl: image.src,
imageAlt: person.name,
});

const jobTitle = t(`team.people.${slug}.jobTitle`);
const bio = t(`team.people.${slug}.bio`);

const jsonLd = getProfilePageJsonLd(slug, metadata.canonical as string, {
jobTitle,
description: bio,
});
const aboutTeamHref = `${getLocalizedPath(locale, "/about")}#team`;
---

<Layout {...metadata} jsonLd={jsonLd}>
<article class="person-page bg-void">
<div
class="px-content-edges max-md:px-content-edges-mobile mx-auto max-w-[860px] pt-[140px] pb-24 max-md:pt-[120px] max-md:pb-20"
>
<a
href={aboutTeamHref}
class="person-page-back text-content-40 hover:text-flare mb-10 inline-flex items-center gap-2 font-mono text-[11px] tracking-[0.16em] uppercase no-underline transition-colors"
>
{t("team.back_to_about")}
</a>

<header class="mb-10 flex items-start gap-8 max-md:flex-col max-md:gap-6">
<div class="person-photo-wrap">
<Image
src={image}
alt={person.name}
width={240}
height={240}
class="person-photo"
/>
</div>
<div class="min-w-0 pt-1">
<h1
class="font-primary mb-3 text-[clamp(28px,3.5vw,48px)] leading-[1.18] font-medium text-white"
>
{person.name}
</h1>
<p
class="text-flare font-mono text-[11px] tracking-[0.14em] uppercase"
>
{jobTitle}
</p>
<a
href={person.social.url}
target="_blank"
rel="noopener"
class="person-x-link"
>
<svg
viewBox="0 0 24 24"
width="12"
height="12"
fill="currentColor"
aria-hidden="true"
>
<path
d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z"
></path>
</svg>
{t("team.follow_on_x")}
<span>{person.social.username}</span>
</a>
</div>
</header>

<div class="person-bio">
{bio.split(/\n\n+/).map((paragraph: string) => <p>{paragraph}</p>)}
</div>
</div>
</article>
</Layout>

<style>
.person-page-back::before {
content: "←";
margin-right: 4px;
}

.person-photo-wrap {
width: 200px;
flex-shrink: 0;
overflow: hidden;
border: 1px solid rgba(232, 230, 224, 0.08);
background: #111;
}

.person-photo {
display: block;
width: 200px;
height: 200px;
object-fit: cover;
object-position: center top;
}

.person-x-link {
display: inline-flex;
align-items: center;
gap: 8px;
margin-top: 16px;
text-decoration: none;
font-family: var(--font-mono);
font-size: 11px;
color: rgba(232, 230, 224, 0.35);
letter-spacing: 0.1em;
text-transform: uppercase;
transition: color 0.2s ease;
}

.person-x-link:hover {
color: var(--color-flare);
}

.person-bio p {
font-size: 16px;
line-height: 1.75;
color: var(--color-content-70);
margin-bottom: 24px;
}

.person-bio p:last-child {
margin-bottom: 0;
}

@media (max-width: 768px) {
.person-photo-wrap,
.person-photo {
width: 160px;
height: 160px;
}
}
</style>
Loading