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
4 changes: 3 additions & 1 deletion apps/website/.env.example
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
# RESEND_API_KEY=
# NEXT_PUBLIC_POSTHOG_KEY=phc_KllQh2hOMmXJ3YwdiLHswb1CfOaEWzoC30mA0u3ZJgp
# NEXT_PUBLIC_POSTHOG_HOST=https://us.i.posthog.com
# NEXT_PUBLIC_POSTHOG_HOST=https://us.i.posthog.com
# BUTTONDOWN_API_KEY=
# BUTTONDOWN_NEWSLETTER_SUBJECT_FILTER=Newsletter
6 changes: 2 additions & 4 deletions apps/website/app/(home)/blog/readBlogs.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import fs from "node:fs/promises";
import path from "node:path";
import matter from "gray-matter";
import { sortByDateDesc } from "~/utils/sortByDate";
import { BLOG_DIRECTORY } from "./blogDirectory";
import { BlogFrontmatterSchema, type BlogData } from "./blogSchema";

Expand Down Expand Up @@ -40,9 +41,6 @@ const processBlogFile = async (filename: string): Promise<BlogData | null> => {
}
};

const sortBlogsByDate = (left: BlogData, right: BlogData): number =>
new Date(right.date).getTime() - new Date(left.date).getTime();

const listBlogFiles = async (): Promise<string[]> => {
const directoryExists = await validateBlogDirectory();

Expand All @@ -61,7 +59,7 @@ export const getAllBlogs = async (): Promise<BlogData[]> => {
const blogs = await Promise.all(files.map(processBlogFile));
const validBlogs = blogs.filter((blog): blog is BlogData => blog !== null);

return validBlogs.filter((blog) => blog.published).sort(sortBlogsByDate);
return validBlogs.filter((blog) => blog.published).sort(sortByDateDesc);
} catch (error) {
console.error("Error reading blog directory:", error);
return [];
Expand Down
8 changes: 4 additions & 4 deletions apps/website/app/(home)/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import type { Metadata } from "next";
import type { ReactElement, ReactNode } from "react";
import { Inter } from "next/font/google";
import { getAllBlogs } from "~/(home)/blog/readBlogs";
import { Logo } from "~/components/Logo";
import { STATIC_NEWS_ITEMS } from "~/data/news";
import { PostHogProvider } from "../providers";
import { HomeNavigationMenu } from "./HomeNavigationMenu";
import "~/globals.css";
Expand Down Expand Up @@ -39,13 +39,13 @@ const HomeLayout = async ({
}: {
children: ReactNode;
}): Promise<ReactElement> => {
const hasUpdates = !!(await getAllBlogs()).length;
const navigationItems = [
{ href: "/#about", label: "About" },
{ href: "/#plugins", label: "Plugins" },
{ href: "/#resources", label: "Resources" },
{ href: "/#events", label: "Events" },
...(hasUpdates ? [{ href: "/#updates", label: "Updates" }] : []),
...(STATIC_NEWS_ITEMS.length > 0
? [{ href: "/#news", label: "News" }]
: []),
{ href: "/#talks", label: "Talks" },
{ href: "/#team", label: "Team" },
{ href: "/#supporters", label: "Supporters" },
Expand Down
160 changes: 64 additions & 96 deletions apps/website/app/(home)/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,16 @@ import {
Puzzle,
Sparkles,
} from "lucide-react";
import { getLatestBlogs } from "~/(home)/blog/readBlogs";
import { getAllBlogs } from "~/(home)/blog/readBlogs";
import { Logo } from "~/components/Logo";
import { PlatformBadge } from "~/components/PlatformBadge";
import { TeamPerson } from "~/components/TeamPerson";
import { TEAM_MEMBERS } from "~/data/constants";
import { STATIC_NEWS_ITEMS } from "~/data/news";
import type { NewsItem } from "~/types/news";
import { getButtondownNewsletterItems } from "~/utils/buttondown";
import { formatDisplayDate } from "~/utils/formatDate";
import { sortByDateDesc } from "~/utils/sortByDate";

const SLACK_URL =
"https://join.slack.com/t/discoursegraphs/shared_invite/zt-37xklatti-cpEjgPQC0YyKYQWPNgAkEg";
Expand Down Expand Up @@ -89,39 +94,10 @@ const RESOURCE_LINKS = [
},
] as const;

const EVENTS = [
{
href: "https://discoursegraphs.github.io/panel-qa-site/",
linkText: "View panel notes",
meta: "June 18, 2026 | Zoom",
title: "Frontiers in Research: Open Science Catalyze Panel",
},
{
href: "https://bsky.app/profile/atproto.science/post/3mh6kak5agk2z",
linkText: "View event post",
meta: "March 27, 2026 | ATScience Conference, Vancouver",
title: "Toward Modular Open Science",
},
{
href: "https://www.mcgill.ca/qls/channels/event/qls-seminar-series-matthew-akamatsu-371875",
linkText: "View seminar details",
meta: "March 24, 2026 | Montreal",
title: "Seminar: McGill University Quantitative Life Sciences program",
},
{
href: "https://luma.com/jijn0d5k",
linkText: "View talk page",
meta: "November 19, 2025 | Zoom",
title:
"Metagov x Future of Science Seminar: Interoperable LLM- and human-centered research with Discourse Graphs",
},
{
href: "https://iosp.io/schedule",
linkText: "View full schedule",
meta: "February 23-24, 2025 | Denver Museum of Nature and Science",
title: "IOSP '25 Winter Workshop: Discourse Graphs",
},
] as const;
// Keeps the homepage widget to a "recent news" size instead of growing
// forever as blog posts and newsletters accumulate; the full blog archive
// is still reachable via the "See all posts" link.
const MAX_NEWS_ITEMS = 10;

type Talk =
| {
Expand Down Expand Up @@ -270,7 +246,31 @@ const ArrowLink = ({
);

const Home = async (): Promise<ReactElement> => {
const blogs = await getLatestBlogs();
const [blogs, newsletterItems] = await Promise.all([
getAllBlogs(),
getButtondownNewsletterItems(),
]);

const blogNewsItems: NewsItem[] = blogs.map((blog) => ({
date: blog.date,
href: `/blog/${blog.slug}`,
linkText: "View post",
meta: `${formatDisplayDate(blog.date)} | By ${blog.author}`,
title: blog.title,
}));

const allNewsItems = [
...STATIC_NEWS_ITEMS,
...blogNewsItems,
...newsletterItems,
];
// Buttondown's pagination can occasionally return the same email twice
// (e.g. if one is sent mid-fetch); href is used as the React key, so
// dedupe before rendering.
const dedupedNewsItems = Array.from(
new Map(allNewsItems.map((item) => [item.href, item])).values(),
);
const news = dedupedNewsItems.sort(sortByDateDesc).slice(0, MAX_NEWS_ITEMS);

return (
<div>
Expand Down Expand Up @@ -614,74 +614,42 @@ const Home = async (): Promise<ReactElement> => {
</div>
</section>

<section className="px-5 py-16 sm:px-6 lg:py-24">
<div id="events" className="mx-auto max-w-7xl scroll-mt-20">
<SectionHeader
eyebrow="Events"
isWide
title="Recent talks, panels, and workshops"
description="Places where the team and collaborators have presented the Discourse Graphs model and project."
/>
<div className="mt-10 divide-y divide-neutral-dark/10 border-y border-neutral-dark/10">
{EVENTS.map((event) => (
<article
key={event.href}
className="grid gap-4 py-6 md:grid-cols-[1fr_auto] md:items-center"
>
<div>
<h3 className="text-xl font-semibold text-neutral-dark">
{event.title}
</h3>
<p className="mt-2 text-sm text-neutral-dark/65">
{event.meta}
</p>
</div>
<Link
href={event.href}
className="inline-flex items-center gap-2 text-sm font-semibold text-secondary transition-colors hover:text-secondary/70"
>
{event.linkText}
<ExternalLink className="h-4 w-4" aria-hidden="true" />
</Link>
</article>
))}
</div>
</div>
</section>

{blogs.length > 0 && (
<section className="bg-white px-5 py-16 sm:px-6 lg:py-24">
<div id="updates" className="mx-auto max-w-7xl scroll-mt-20">
{news.length > 0 && (
<section className="px-5 py-16 sm:px-6 lg:py-24">
<div id="news" className="mx-auto max-w-7xl scroll-mt-20">
<div className="flex flex-col justify-between gap-6 md:flex-row md:items-end">
<SectionHeader
eyebrow="Updates"
eyebrow="News"
isWide
title="Latest project updates"
description="Recent posts from the Discourse Graphs team."
title="Talks, posts, and newsletters"
description="Recent updates from the Discourse Graphs team and places where the team and collaborators have presented the model and project."
/>
<ArrowLink href="/blog">See all updates</ArrowLink>
{blogs.length > 0 && (
<ArrowLink href="/blog">See all posts</ArrowLink>
)}
</div>
<div className="mt-10 grid gap-4 md:grid-cols-2 lg:grid-cols-3">
{blogs.map((blog) => (
<Card
key={blog.slug}
className="rounded-lg border-neutral-dark/10 bg-neutral-light shadow-sm"
<div className="mt-10 divide-y divide-neutral-dark/10 border-y border-neutral-dark/10">
{news.map((item) => (
<article
key={item.href}
className="grid gap-4 py-6 md:grid-cols-[1fr_auto] md:items-center"
>
<CardContent className="flex h-full flex-col p-6">
<p className="text-sm text-neutral-dark/60">
{blog.date}
</p>
<Link
href={`/blog/${blog.slug}`}
className="mt-4 text-xl font-semibold text-neutral-dark transition-colors hover:text-secondary"
>
{blog.title}
</Link>
<p className="mt-6 text-sm text-neutral-dark/60">
By {blog.author}
<div>
<h3 className="text-xl font-semibold text-neutral-dark">
{item.title}
</h3>
<p className="mt-2 text-sm text-neutral-dark/65">
{item.meta}
</p>
</CardContent>
</Card>
</div>
<Link
href={item.href}
className="inline-flex items-center gap-2 text-sm font-semibold text-secondary transition-colors hover:text-secondary/70"
>
{item.linkText}
<ExternalLink className="h-4 w-4" aria-hidden="true" />
</Link>
</article>
))}
</div>
</div>
Expand Down
63 changes: 63 additions & 0 deletions apps/website/app/data/news.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import type { NewsItem } from "~/types/news";
import { formatDisplayDate } from "~/utils/formatDate";

type StaticEventSource = {
// Overrides the date shown in `meta`, for events spanning multiple days
// (e.g. "February 23-24, 2025") that a single `date` can't represent.
dateLabel?: string;
date: string;
href: string;
linkText: string;
location: string;
title: string;
};

const STATIC_EVENT_SOURCES: StaticEventSource[] = [
{
date: "2026-06-18",
href: "https://discoursegraphs.github.io/panel-qa-site/",
linkText: "View panel notes",
location: "Zoom",
title: "Frontiers in Research: Open Science Catalyze Panel",
},
{
date: "2026-03-27",
href: "https://bsky.app/profile/atproto.science/post/3mh6kak5agk2z",
linkText: "View event post",
location: "ATScience Conference, Vancouver",
title: "Toward Modular Open Science",
},
{
date: "2026-03-24",
href: "https://www.mcgill.ca/qls/channels/event/qls-seminar-series-matthew-akamatsu-371875",
linkText: "View seminar details",
location: "Montreal",
title: "Seminar: McGill University Quantitative Life Sciences program",
},
{
date: "2025-11-19",
href: "https://luma.com/jijn0d5k",
linkText: "View talk page",
location: "Zoom",
title:
"Metagov x Future of Science Seminar: Interoperable LLM- and human-centered research with Discourse Graphs",
},
{
date: "2025-02-23",
dateLabel: "February 23-24, 2025",
href: "https://iosp.io/schedule",
linkText: "View full schedule",
location: "Denver Museum of Nature and Science",
title: "IOSP '25 Winter Workshop: Discourse Graphs",
},
];

export const STATIC_NEWS_ITEMS: NewsItem[] = STATIC_EVENT_SOURCES.map(
(event) => ({
date: event.date,
href: event.href,
linkText: event.linkText,
meta: `${event.dateLabel ?? formatDisplayDate(event.date)} | ${event.location}`,
title: event.title,
}),
);
7 changes: 7 additions & 0 deletions apps/website/app/types/news.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export type NewsItem = {
date: string;
href: string;
linkText: string;
meta: string;
title: string;
};
Loading