diff --git a/apps/marketing/content/blog/_archive/history-of-git-worktrees.mdx b/apps/marketing/content/blog/_archive/history-of-git-worktrees.mdx index 2439ec7ae23..941013ffefb 100644 --- a/apps/marketing/content/blog/_archive/history-of-git-worktrees.mdx +++ b/apps/marketing/content/blog/_archive/history-of-git-worktrees.mdx @@ -1,7 +1,7 @@ --- title: The History of Git Worktrees description: How git worktrees evolved from a niche feature to the foundation of modern parallel development workflows. -author: Avi Peltz +author: avi date: 2025-01-18 category: Research --- diff --git a/apps/marketing/content/blog/_archive/introducing-superset.mdx b/apps/marketing/content/blog/_archive/introducing-superset.mdx index bc2f6924721..46390598801 100644 --- a/apps/marketing/content/blog/_archive/introducing-superset.mdx +++ b/apps/marketing/content/blog/_archive/introducing-superset.mdx @@ -1,7 +1,7 @@ --- title: Introducing Superset description: Run 10+ parallel coding agents on your machine. A new way to work with AI coding assistants. -author: Avi Peltz +author: avi date: 2025-01-15 category: Product --- diff --git a/apps/marketing/content/blog/_archive/parallel-agents-guide.mdx b/apps/marketing/content/blog/_archive/parallel-agents-guide.mdx index f3e57916476..7572f0dbe57 100644 --- a/apps/marketing/content/blog/_archive/parallel-agents-guide.mdx +++ b/apps/marketing/content/blog/_archive/parallel-agents-guide.mdx @@ -1,7 +1,7 @@ --- title: A Guide to Parallel Coding Agents description: Learn how to maximize your productivity by running multiple AI coding agents simultaneously. -author: Avi Peltz +author: avi date: 2025-01-20 category: Research --- diff --git a/apps/marketing/content/blog/how-to-get-hit.mdx b/apps/marketing/content/blog/how-to-get-hit.mdx index ef53fa29082..8c9a0e37fb1 100644 --- a/apps/marketing/content/blog/how-to-get-hit.mdx +++ b/apps/marketing/content/blog/how-to-get-hit.mdx @@ -1,7 +1,7 @@ --- title: How to get hit (probably in the face but anywhere else works too) description: I ran out of technical content to write for the blog so here is something I am interested in instead. -author: Kiet Ho +author: kiet date: 2026-01-28 category: Company relatedSlugs: diff --git a/apps/marketing/content/blog/terminal-daemon-deep-dive.mdx b/apps/marketing/content/blog/terminal-daemon-deep-dive.mdx index 9248b9c1cbc..599fb34005f 100644 --- a/apps/marketing/content/blog/terminal-daemon-deep-dive.mdx +++ b/apps/marketing/content/blog/terminal-daemon-deep-dive.mdx @@ -1,7 +1,7 @@ --- title: "The Terminal That (Almost) Never Dies: Building a Persistent Terminal Daemon for Electron" description: "How we built a process-isolated terminal host that survives app restarts, handles backpressure gracefully, and enables cold restore from disk." -author: Avi Peltz +author: avi date: 2026-01-26 category: Engineering relatedSlugs: diff --git a/apps/marketing/content/people/avi.mdx b/apps/marketing/content/people/avi.mdx new file mode 100644 index 00000000000..dcef90f18a2 --- /dev/null +++ b/apps/marketing/content/people/avi.mdx @@ -0,0 +1,8 @@ +--- +name: Avi Peltz +role: Cofounder +avatar: /team/avi.jpg +twitter: avimakesrobots +github: avipeltz +linkedin: avipeltz +--- diff --git a/apps/marketing/content/people/kiet.mdx b/apps/marketing/content/people/kiet.mdx new file mode 100644 index 00000000000..692c733a1d0 --- /dev/null +++ b/apps/marketing/content/people/kiet.mdx @@ -0,0 +1,8 @@ +--- +name: Kiet Ho +role: Cofounder +avatar: /team/kiet.jpg +twitter: kietho_ +github: kietho +linkedin: kiet-ho +--- diff --git a/apps/marketing/content/people/satya.mdx b/apps/marketing/content/people/satya.mdx new file mode 100644 index 00000000000..84cb3592e8c --- /dev/null +++ b/apps/marketing/content/people/satya.mdx @@ -0,0 +1,8 @@ +--- +name: Satya Patel +role: Cofounder +avatar: /team/satya.webp +twitter: saddle_paddle +github: saddlepaddle +linkedin: saddlepaddle +--- diff --git a/apps/marketing/public/team/avi.jpg b/apps/marketing/public/team/avi.jpg new file mode 100644 index 00000000000..da475f589fd Binary files /dev/null and b/apps/marketing/public/team/avi.jpg differ diff --git a/apps/marketing/public/team/kiet.jpg b/apps/marketing/public/team/kiet.jpg new file mode 100644 index 00000000000..64a3aa2a691 Binary files /dev/null and b/apps/marketing/public/team/kiet.jpg differ diff --git a/apps/marketing/public/team/satya.webp b/apps/marketing/public/team/satya.webp new file mode 100644 index 00000000000..569d187677e Binary files /dev/null and b/apps/marketing/public/team/satya.webp differ diff --git a/apps/marketing/src/app/blog/[slug]/components/BlogPostLayout/BlogPostLayout.tsx b/apps/marketing/src/app/blog/[slug]/components/BlogPostLayout/BlogPostLayout.tsx index d4f7ec7ba1b..56e206d4ffa 100644 --- a/apps/marketing/src/app/blog/[slug]/components/BlogPostLayout/BlogPostLayout.tsx +++ b/apps/marketing/src/app/blog/[slug]/components/BlogPostLayout/BlogPostLayout.tsx @@ -1,8 +1,11 @@ -"use client"; - import { ArrowLeft } from "lucide-react"; import Link from "next/link"; import type { ReactNode } from "react"; +import { + RiGithubFill, + RiLinkedinBoxFill, + RiTwitterXFill, +} from "react-icons/ri"; import { AuthorAvatar } from "@/app/blog/components/AuthorAvatar"; import { BlogCard } from "@/app/blog/components/BlogCard"; import { GridCross } from "@/app/blog/components/GridCross"; @@ -21,6 +24,7 @@ export function BlogPostLayout({ children, }: BlogPostLayoutProps) { const formattedDate = formatBlogDate(post.date); + const { author } = post; return (
@@ -56,15 +60,48 @@ export function BlogPostLayout({

)} -
- - {post.author} - · - +
+ +
+ {author.name} + + {author.role} + · + + +
+
+
+ {author.twitter && ( + + + + )} + {author.github && ( + + + + )} + {author.linkedin && ( + + + + )}
diff --git a/apps/marketing/src/app/blog/[slug]/page.tsx b/apps/marketing/src/app/blog/[slug]/page.tsx index 9e808283021..4e8fd8b0e4e 100644 --- a/apps/marketing/src/app/blog/[slug]/page.tsx +++ b/apps/marketing/src/app/blog/[slug]/page.tsx @@ -29,15 +29,31 @@ export default async function BlogPostPage({ params }: PageProps) { slug, relatedSlugs: post.relatedSlugs, }); + const { author } = post; const url = `${COMPANY.MARKETING_URL}/blog/${slug}`; + const sameAs: string[] = []; + if (author.twitter) { + sameAs.push(`https://x.com/${author.twitter}`); + } + if (author.github) { + sameAs.push(`https://github.com/${author.github}`); + } + if (author.linkedin) { + sameAs.push(`https://linkedin.com/in/${author.linkedin}`); + } + return (
0 ? sameAs : undefined, + }} publishedTime={new Date(post.date).toISOString()} url={url} image={post.image} @@ -85,7 +101,7 @@ export async function generateMetadata({ url, siteName: COMPANY.NAME, publishedTime: post.date, - authors: [post.author], + authors: [post.author.name], ...(post.image && { images: [post.image] }), }, twitter: { diff --git a/apps/marketing/src/app/blog/components/AuthorAvatar/AuthorAvatar.tsx b/apps/marketing/src/app/blog/components/AuthorAvatar/AuthorAvatar.tsx index f5f5438906c..f88e355d985 100644 --- a/apps/marketing/src/app/blog/components/AuthorAvatar/AuthorAvatar.tsx +++ b/apps/marketing/src/app/blog/components/AuthorAvatar/AuthorAvatar.tsx @@ -1,33 +1,18 @@ -"use client"; +import Image from "next/image"; -import { Tooltip, TooltipContent, TooltipTrigger } from "@superset/ui/tooltip"; - -function XIcon({ className }: { className?: string }) { - return ( - - ); -} +const SIZE_CONFIG = { + sm: { container: "size-6", text: "size-6 text-[10px]", imgSize: "24px" }, + md: { container: "size-8", text: "size-8 text-xs", imgSize: "32px" }, + lg: { container: "size-12", text: "size-12 text-sm", imgSize: "48px" }, +}; interface AuthorAvatarProps { name: string; - twitterHandle?: string; - title?: string; - size?: "sm" | "md"; + avatar?: string; + size?: "sm" | "md" | "lg"; } -export function AuthorAvatar({ - name, - twitterHandle, - title, - size = "md", -}: AuthorAvatarProps) { +export function AuthorAvatar({ name, avatar, size = "md" }: AuthorAvatarProps) { const initials = name .split(" ") .map((n) => n[0]) @@ -35,45 +20,29 @@ export function AuthorAvatar({ .toUpperCase() .slice(0, 2); - const sizeClasses = size === "sm" ? "size-6 text-[10px]" : "size-8 text-xs"; + const config = SIZE_CONFIG[size]; + + if (avatar) { + return ( +
+ {name} +
+ ); + } - const avatar = ( + return (
{initials}
); - - if (!twitterHandle) { - return avatar; - } - - return ( - - {avatar} - -
-
- {name} - {title && {title}} -
- - - @{twitterHandle} - -
-
-
- ); } diff --git a/apps/marketing/src/app/blog/components/BlogCard/BlogCard.tsx b/apps/marketing/src/app/blog/components/BlogCard/BlogCard.tsx index 06c9d3251b1..c35b411b56c 100644 --- a/apps/marketing/src/app/blog/components/BlogCard/BlogCard.tsx +++ b/apps/marketing/src/app/blog/components/BlogCard/BlogCard.tsx @@ -31,12 +31,13 @@ export function BlogCard({ post }: BlogCardProps) { )}
- {post.author} + + {post.author.name} +
diff --git a/apps/marketing/src/app/changelog/[slug]/page.tsx b/apps/marketing/src/app/changelog/[slug]/page.tsx index 775ae17fd9d..2f704b8484d 100644 --- a/apps/marketing/src/app/changelog/[slug]/page.tsx +++ b/apps/marketing/src/app/changelog/[slug]/page.tsx @@ -26,7 +26,7 @@ export default async function ChangelogEntryPage({ params }: PageProps) { ${escapeXml(post.description || "")} ${new Date(post.date).toUTCString()} ${baseUrl}/blog/${post.slug} - ${escapeXml(post.author)} + ${escapeXml(post.author.name)} `, ) .join("")} diff --git a/apps/marketing/src/components/JsonLd/JsonLd.tsx b/apps/marketing/src/components/JsonLd/JsonLd.tsx index 476f88e7a27..695de20be2c 100644 --- a/apps/marketing/src/components/JsonLd/JsonLd.tsx +++ b/apps/marketing/src/components/JsonLd/JsonLd.tsx @@ -45,10 +45,16 @@ export function SoftwareApplicationJsonLd() { ); } +interface ArticleAuthor { + name: string; + url?: string; + sameAs?: string[]; +} + interface ArticleJsonLdProps { title: string; description?: string; - author: string; + author: ArticleAuthor; publishedTime: string; url: string; image?: string; @@ -69,7 +75,10 @@ export function ArticleJsonLd({ description: description || title, author: { "@type": "Person", - name: author, + name: author.name, + ...(author.url && { url: author.url }), + ...(author.sameAs && + author.sameAs.length > 0 && { sameAs: author.sameAs }), }, publisher: { "@type": "Organization", diff --git a/apps/marketing/src/lib/blog-utils.ts b/apps/marketing/src/lib/blog-utils.ts index 60815fec421..cfa4f014a23 100644 --- a/apps/marketing/src/lib/blog-utils.ts +++ b/apps/marketing/src/lib/blog-utils.ts @@ -4,6 +4,7 @@ */ import type { BlogCategory } from "./blog-constants"; +import type { Person } from "./people"; export interface TocItem { id: string; @@ -16,7 +17,7 @@ export interface BlogPost { url: string; title: string; description?: string; - author: string; + author: Person; date: string; category: BlogCategory; image?: string; diff --git a/apps/marketing/src/lib/blog.ts b/apps/marketing/src/lib/blog.ts index e4768b0b7be..4931233613b 100644 --- a/apps/marketing/src/lib/blog.ts +++ b/apps/marketing/src/lib/blog.ts @@ -2,6 +2,7 @@ import fs from "node:fs"; import path from "node:path"; import matter from "gray-matter"; import { type BlogPost, slugify, type TocItem } from "./blog-utils"; +import { getPersonById } from "./people"; export { BLOG_CATEGORIES, type BlogCategory } from "./blog-constants"; export { @@ -10,6 +11,7 @@ export { slugify, type TocItem, } from "./blog-utils"; +export type { Person } from "./people"; const BLOG_DIR = path.join(process.cwd(), "content/blog"); @@ -29,12 +31,20 @@ function parseFrontmatter(filePath: string): BlogPost | null { dateValue = new Date().toISOString().split("T")[0] as string; } + const authorId: string = data.author ?? "unknown"; + const author = getPersonById(authorId) ?? { + id: authorId, + name: authorId, + role: "", + content: "", + }; + return { slug, url: `/blog/${slug}`, title: data.title ?? "Untitled", description: data.description, - author: data.author ?? "Unknown", + author, date: dateValue, category: data.category ?? "News", image: data.image, diff --git a/apps/marketing/src/lib/people.ts b/apps/marketing/src/lib/people.ts new file mode 100644 index 00000000000..9d889b92aa4 --- /dev/null +++ b/apps/marketing/src/lib/people.ts @@ -0,0 +1,66 @@ +import fs from "node:fs"; +import path from "node:path"; +import matter from "gray-matter"; +import { z } from "zod"; + +const peopleDirectory = path.join(process.cwd(), "content/people"); + +export const personSchema = z.object({ + name: z.string().min(1, "Name is required"), + role: z.string().min(1, "Role is required"), + bio: z.string().optional(), + twitter: z.string().optional(), + github: z.string().optional(), + linkedin: z.string().optional(), + avatar: z.string().optional(), +}); + +export type PersonMetadata = z.infer; + +export interface Person extends PersonMetadata { + id: string; + content: string; +} + +export function getPersonById(id: string): Person | null { + const filePath = path.join(peopleDirectory, `${id}.mdx`); + + if (!fs.existsSync(filePath)) { + return null; + } + + try { + const fileContent = fs.readFileSync(filePath, "utf-8"); + const { data, content } = matter(fileContent); + const validatedData = personSchema.parse(data); + + return { + ...validatedData, + id, + content, + }; + } catch (error) { + console.error(`[people] Failed to parse ${id}.mdx:`, error); + return null; + } +} + +export function getAllPeople(): Person[] { + if (!fs.existsSync(peopleDirectory)) { + return []; + } + + const fileNames = fs.readdirSync(peopleDirectory); + const mdxFiles = fileNames.filter((fileName) => fileName.endsWith(".mdx")); + const people: Person[] = []; + + for (const fileName of mdxFiles) { + const id = fileName.replace(/\.mdx$/, ""); + const person = getPersonById(id); + if (person) { + people.push(person); + } + } + + return people.sort((a, b) => a.name.localeCompare(b.name)); +}