Co-authored-by: Dotan Simha <dotansimha@users.noreply.github.com>
This commit is contained in:
Piotr Monwid-Olechnowicz 2025-03-27 16:12:42 +01:00 committed by GitHub
parent e6a970f790
commit 4a865c03b2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
47 changed files with 1268 additions and 204 deletions

View file

@ -231,6 +231,7 @@ module.exports = {
'nextra-scrollbar',
'no-scrollbar', // from Nextra
'hive-slider',
'hive-prose',
'subheader',
],
config: path.join(__dirname, './packages/web/docs/tailwind.config.ts'),

View file

@ -133,7 +133,9 @@
"@oclif/core@4.0.6": "patches/@oclif__core@4.0.6.patch",
"@fastify/vite": "patches/@fastify__vite.patch",
"p-cancelable@4.0.1": "patches/p-cancelable@4.0.1.patch",
"bentocache": "patches/bentocache.patch"
"bentocache": "patches/bentocache.patch",
"nextra": "patches/nextra.patch",
"nextra-theme-docs": "patches/nextra-theme-docs.patch"
}
}
}

View file

@ -24,6 +24,7 @@
"react-countup": "6.5.3",
"react-dom": "19.0.0",
"react-icons": "5.4.0",
"rehype-frontmatter-mdx-imports": "0.1.1",
"tailwind-merge": "2.6.0"
},
"devDependencies": {

View file

@ -82,7 +82,10 @@ const meta: Record<string, DeepPartial<Item | MenuItem | PageItem>> = {
blog: {
title: 'Blog',
type: 'page',
href: 'https://the-guild.dev/blog',
theme: {
breadcrumb: false,
sidebar: false,
},
},
github: {
title: 'GitHub',

View file

@ -0,0 +1,93 @@
---
title: Hive Platform Achieves SOC-2 Type II Certification
tags: [security, cloud, hive, platform, compliance]
authors: dotan
date: 2025-03-25
description:
The certification process involved a thorough review of our security controls and processes, and
we are proud to have achieved SOC-2 compliance.
featured: true
---
import { Callout } from '@theguild/components'
We're excited to announce that Hive Platform (and The Guild) has successfully achieved SOC-2 Type II
certification for our flagship products.
This milestone represents a significant step forward in our commitment to security, privacy, and
enterprise-grade reliability.
<Callout emoji="🚀" type="info">
TL;DR: In [our Trust Center](https://security.graphql-hive.com) you can find all information about
security controls, documents and certificates.
</Callout>
## Overview
After a decade of serving the GraphQL open-source community, The Guild has reached another important
milestone in our journey. We're proud to announce that we have successfully completed our SOC-2 Type
II audit, conducted by [Advantage Partners](https://advantage-partners.com/), using the
[Vanta](https://vanta.com/) compliance platform.
As The Guild celebrates its 10th year of operations, this certification marks an important
investment in being enterprise-ready partner equipped to meet the rigorous security and compliance
requirements of organizations of all sizes.
## What's SOC-2 Compliance? Why Type II Matters for Enterprises?
[SOC-2](https://www.vanta.com/collection/soc-2/what-is-soc-2) (Service Organization Control 2) is a
voluntary compliance standard developed by the American Institute of CPAs (AICPA) that specifies how
organizations should manage customer data based on five "trust service criteria": security,
availability, processing integrity, confidentiality, and privacy.
While SOC-2 Type I certifications assess whether a company's systems are suitably designed at a
specific point in time, **Type II certification** goes much further. It evaluates the operational
effectiveness of those controls over time, providing assurance that a company not only has
appropriate security measures in place but consistently follows them.
For enterprises, this distinction is crucial:
1. **Proven Reliability**: Type II demonstrates consistent adherence to security practices over
time, not just a one-time assessment
2. **Risk Reduction**: The certification significantly reduces the risk of data breaches and
security incidents
3. **Compliance Support**: It helps enterprises meet their own regulatory and internal compliance
requirements
4. **Vendor Management**: It simplifies vendor assessment processes, allowing for faster procurement
decisions
5. **Trust Verification**: It provides independent verification of security claims by a qualified
third party
## What This Means for The Guild Customers
For our customers, this certification brings several tangible benefits:
### Enhanced Security and Privacy
Our SOC-2 Type II certification confirms that we have robust controls in place to protect your data.
This includes comprehensive security policies, regular testing, monitoring, and incident response
procedures that have been independently verified.
### Simplified Vendor Assessment
For enterprise customers with stringent security requirements, our certification streamlines the
procurement process. Instead of conducting extensive security assessments, you can rely on our SOC-2
Type II report as evidence of our security posture.
### Enterprise Readiness
This certification demonstrates that The Guild is an organization that can confidently serve
enterprise customers with complex security and compliance requirements, while maintaining technical
excellence.
---
<Callout emoji="🙏" type="info">
We'd like to thank our team for their dedication to this process, our auditors at
[Advantage Partners](https://advantage-partners.com/) for their thorough assessment, and the
[Vanta](https://vanta.com/) platform for streamlining our compliance journey and making it easier
for us to achieve this certification.
</Callout>

View file

@ -0,0 +1,18 @@
import { cn, HiveLayoutConfig } from '@theguild/components';
import { LandingPageContainer } from '../../../components/landing-page-container';
import '../../hive-prose-styles.css';
import { BlogPostHeader } from '../components/blog-post-layout/blog-post-header';
const MAIN_CONTENT = 'main-content';
export default function BlogPostLayout({ children }: { children: React.ReactNode }) {
return (
<LandingPageContainer className="hive-prose text-green-1000 mx-auto max-w-[90rem] overflow-hidden dark:text-white">
<HiveLayoutConfig widths="landing-narrow" />
<BlogPostHeader className="mx-auto" />
<div className={cn(MAIN_CONTENT, 'mx-auto flex [&_main>p:first-of-type]:text-2xl/8')}>
{children}
</div>
</LandingPageContainer>
);
}

View file

@ -0,0 +1,100 @@
---
title: Understanding the Differences Between GraphQL and REST API Gateways
tags: [graphql, rest]
authors: saihaj
date: 2024-12-03
description: What is the difference between GraphQL and REST API Gateway?
featured: true
---
API gateways serve as crucial intermediaries between clients and backend services, but GraphQL and
REST gateways handle this responsibility quite differently. While a GraphQL gateway can be
considered a superset of REST gateway functionality, each has its distinct characteristics and use
cases.
## Core Differences
### Request Processing
- **REST API Gateway**: Handles traditional HTTP requests with fixed endpoints. Each endpoint
typically serves a specific purpose and returns a predefined data structure. The gateway routes
these requests to appropriate microservices based on URL patterns.
- **GraphQL Gateway**: Processes queries written in the GraphQL query language, typically through a
single endpoint. It can understand complex queries requesting specific fields and relationships,
making it more flexible in handling varied data requirements.
### Data Aggregation
- **REST Gateway**: Often requires multiple endpoints to gather related data, leading to potential
over-fetching or under-fetching of data. The gateway might need to make several internal calls to
different services to compose a complete response.
- **GraphQL Gateway**: Excels at data aggregation by allowing clients to specify exactly what data
they need in a single request. The gateway can efficiently collect data from multiple services
based on the query structure.
## Key Features and Capabilities
### Caching
- **REST Gateway**: Implements straightforward HTTP caching mechanisms. Responses can be cached
based on URLs and HTTP methods.
- **GraphQL Gateway**: Requires more sophisticated caching strategies due to the dynamic nature of
queries. Often implements field-level caching and needs to consider query complexity.
### Security
- **REST Gateway**: Security is typically implemented at the endpoint level with traditional
authentication and authorization mechanisms.
- **GraphQL Gateway**: Provides more granular security controls, allowing permissions to be set at
the field level. Can implement query complexity analysis to prevent abuse.
### Schema Management
- **REST Gateway**: No built-in schema management. API documentation typically relies on external
tools like Swagger/OpenAPI.
- **GraphQL Gateway**: Schema management can be as straightforward as maintaining schema definitions
in code and versioning them with Git. Teams can choose between simple code-first approaches or
leverage specialized tools like GraphQL Hive for more advanced schema registry and validation
features. This flexibility allows teams to scale their schema management practices as their needs
grow.
### Service Integration
- **REST Gateway**: No built-in way to integrate with other protocols.
- **GraphQL Gateway**: A GraphQL gateway like
[Hive Gateway](https://the-guild.dev/graphql/hive/docs/gateway?utm_source=the_guild&utm_medium=blog&utm_campaign=understanding-the-differences-between-graphql-and-rest-api-gateways)
unifies multiple protocols
([REST](https://the-guild.dev/graphql/mesh/v1/source-handlers/openapi?utm_source=the_guild&utm_medium=blog&utm_campaign=understanding-the-differences-between-graphql-and-rest-api-gateways),
[gRPC](https://the-guild.dev/graphql/mesh/v1/source-handlers/grpc?utm_source=the_guild&utm_medium=blog&utm_campaign=understanding-the-differences-between-graphql-and-rest-api-gateways),
[SOAP](https://the-guild.dev/graphql/mesh/v1/source-handlers/soap?utm_source=the_guild&utm_medium=blog&utm_campaign=understanding-the-differences-between-graphql-and-rest-api-gateways)
&
[many more](https://the-guild.dev/graphql/mesh/v1/source-handlers?utm_source=the_guild&utm_medium=blog&utm_campaign=understanding-the-differences-between-graphql-and-rest-api-gateways))
into a consistent interface using tools like
[GraphQL Mesh](https://the-guild.dev/graphql/mesh?utm_source=the_guild&utm_medium=blog&utm_campaign=understanding-the-differences-between-graphql-and-rest-api-gateways),
while supporting federation capabilities that let teams independently develop and deploy subgraphs
as part of a unified supergraph. Learn more about Federation
[here](https://the-guild.dev/graphql/hive/federation?utm_source=the_guild&utm_medium=blog&utm_campaign=understanding-the-differences-between-graphql-and-rest-api-gateways).
## Why Choose GraphQL Gateway?
GraphQL gateways represent the future of API architecture for several compelling reasons:
1. **Enhanced Developer Experience**: GraphQL's intuitive query language and self-documenting nature
significantly improve developer productivity.
2. **Integration**: Easily integrate legacy services and offer a unified query experience.
3. **Optimal Performance**: By allowing clients to request exactly what they need, GraphQL
eliminates the over-fetching and under-fetching problems common with REST APIs.
4. **Future-Proof Architecture**: GraphQL's flexible schema system makes it easier to evolve your
API over time without breaking existing clients.
5. **Better Resource Utilization**: The ability to combine multiple data requirements into a single
request reduces server load and network overhead.
6. **Strong Ecosystem**: The GraphQL ecosystem offers excellent tools for monitoring, testing, and
managing your API gateway.
## Conclusion
While REST API gateways have served us well, GraphQL gateways offer superior capabilities for modern
applications. Their ability to handle complex data requirements efficiently, combined with excellent
developer experience and powerful tools, makes them the recommended choice for new API gateway
implementations. Organizations can start simple with basic schema management in Git and gradually
adopt more sophisticated tools like Hive as their needs evolve.

View file

@ -0,0 +1,5 @@
export default {
'*': {
display: 'hidden',
},
};

View file

@ -0,0 +1,21 @@
import type { StaticImageData } from 'next/image';
import { AuthorId } from '../../authors';
import { MdxFile, PageMapItem } from '../../mdx-types';
export interface BlogFrontmatter {
authors: AuthorId | AuthorId[];
title: string;
date: string;
tags: string | string[];
featured?: boolean;
image?: VideoPath | StaticImageData;
thumbnail?: StaticImageData;
}
type VideoPath = `${string}.${'webm' | 'mp4'}`;
export type BlogPostFile = Required<MdxFile<BlogFrontmatter>>;
export function isBlogPost(item: PageMapItem): item is BlogPostFile {
return item && 'route' in item && 'name' in item && 'frontMatter' in item && !!item.frontMatter;
}

View file

@ -0,0 +1,89 @@
import Image from 'next/image';
import { Anchor, cn } from '@theguild/components';
import { Author, AuthorId, authors } from '../../../authors';
import { BlogPostFile } from '../blog-types';
import { BlogTagChip } from './blog-tag-chip';
export interface BlogCardProps {
post: Pick<BlogPostFile, 'frontMatter' | 'route'>;
className?: string;
variant?: 'default' | 'featured';
/**
* The tag to display on the card. If not provided, the first tag will be used.
* Used for tag index page, where we want to show all cards with the same tag.
*/
tag?: string | null;
}
export function BlogCard({ post, className, variant, tag }: BlogCardProps) {
const frontmatter = post.frontMatter;
const { title, tags } = frontmatter;
const date = new Date(frontmatter.date);
const postAuthors: Author[] = (
typeof frontmatter.authors === 'string'
? [authors[frontmatter.authors as AuthorId]]
: frontmatter.authors.map(author => authors[author as AuthorId])
).filter(Boolean);
if (postAuthors.length === 0) {
console.error('author not found', frontmatter);
throw new Error(`authors ${JSON.stringify(frontmatter.authors)} not found`);
}
const firstAuthor = postAuthors[0];
// todo: show more authors on hover?
const avatarSrc =
firstAuthor.avatar || `https://avatars.githubusercontent.com/${firstAuthor.github}?v=4&s=48`;
return (
<Anchor
className={cn(
'group/card hive-focus hover:ring-beige-400 block rounded-2xl dark:ring-neutral-600 hover:[&:not(:focus)]:ring dark:hover:[&:not(:focus)]:ring-neutral-600',
className,
)}
href={post.route}
>
<article
className={cn(
'text-green-1000 flex h-full flex-col gap-6 rounded-2xl p-6 lg:gap-10 dark:text-white',
variant === 'featured'
? 'bg-beige-200 group-hover/card:bg-beige-300/70 dark:bg-neutral-700/70 dark:hover:bg-neutral-700'
: 'group-hover/card:bg-beige-200/70 bg-beige-100 dark:bg-neutral-800/70 dark:hover:bg-neutral-800',
)}
>
<header className="flex items-center justify-between gap-1 text-sm/5 font-medium">
<BlogTagChip tag={tag ?? tags[0]} colorScheme={variant || 'default'} inert />
<time
dateTime={date.toISOString()}
className="text-beige-800 whitespace-pre text-sm/5 font-medium"
>
{date.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' })}
</time>
</header>
<h3
className={cn(
'text-xl/7 lg:min-h-[172px]',
variant === 'featured' ? 'text-2xl/8' : 'xl:min-h-[120px]',
)}
>
{title}
</h3>
<footer className="mt-auto flex items-center gap-3">
<div className="relative size-6">
<Image
src={avatarSrc}
alt={firstAuthor.name}
width={24}
height={24}
className="rounded-full"
/>
<div className="bg-beige-200/70 absolute inset-0 size-full rounded-full opacity-30 mix-blend-hue" />
</div>
<span className="text-sm/5 font-medium">{firstAuthor.name}</span>
</footer>
</article>
</Anchor>
);
}

View file

@ -0,0 +1,53 @@
import { ArchDecoration, cn, DecorationIsolation, Heading } from '@theguild/components';
export function BlogPageHero({ className }: { className?: string }) {
return (
<div
className={cn(
'bg-beige-200 relative isolate flex max-w-[90rem] flex-col items-center justify-center gap-8 overflow-hidden rounded-3xl px-4 py-6 max-md:min-h-[240px] sm:py-12 lg:py-24 dark:bg-neutral-900',
className,
)}
>
<DecorationIsolation className="dark:opacity-85">
<ArchDecoration className="pointer-events-none absolute -top-64 left-[-60px] rotate-180 max-md:-left-64" />
<ArchDecoration className="pointer-events-none absolute -bottom-64 right-0 max-md:-right-64" />
<svg width="432" height="432" viewBox="0 0 432 432" className="absolute -z-10">
<defs>
<linearGradient
id="arch-decoration-a"
x1="48.5"
y1="53.5"
x2="302.5"
y2="341"
gradientUnits="userSpaceOnUse"
>
<stop stopColor="#fff" stopOpacity="0.3" />
<stop offset="1" stopColor="#fff" stopOpacity="1" />
</linearGradient>
<linearGradient
id="arch-decoration-b"
x1="1"
y1="1"
x2="431"
y2="431"
gradientUnits="userSpaceOnUse"
>
<stop stopColor="#fff" stopOpacity="0.1" />
<stop offset="1" stopColor="#fff" stopOpacity="0.4" />
</linearGradient>
</defs>
</svg>
</DecorationIsolation>
<Heading
as="h1"
size="xl"
className="text-green-1000 z-0 mx-auto max-w-3xl text-center max-md:!text-5xl dark:text-white"
>
GraphQL Stories
</Heading>
<p className="z-0 mx-auto max-w-[80%] text-center leading-6 text-green-800 dark:text-white/80">
Explore insights on managing and optimizing your GraphQL APIs
</p>
</div>
);
}

View file

@ -0,0 +1,55 @@
'use client';
import { Anchor, cn, Heading } from '@theguild/components';
import { ArrowIcon } from '../../../../components/arrow-icon';
import { useFrontmatter } from '../../../../components/use-frontmatter';
import { ProductUpdateAuthors } from '../../../product-updates/(posts)/product-update-header';
import type { BlogFrontmatter } from '../../blog-types';
import { BlogTagChip } from '../blog-tag-chip';
import { BlogPostPicture } from './blog-post-picture';
export function BlogPostHeader({ className }: { className?: string }) {
const {
frontmatter: { image, tags, authors, title, date },
} = useFrontmatter<BlogFrontmatter>();
const tag = tags[0];
return (
<>
{image && <BlogPostPicture image={image} />}
<header
className={cn(
'flex flex-col items-center rounded-3xl bg-[rgb(var(--nextra-bg))] px-1 pb-6 pt-4 md:px-12 md:pb-12 md:pt-6 xl:w-[888px]',
image && '-mt-20 max-sm:mx-6',
className,
)}
>
<div className="flex items-center gap-2">
<Anchor href="/blog" className="flex items-center gap-2 text-sm font-medium">
<ArrowIcon className="text-beige-1000 mr-1 size-4 rotate-180" />
<span className="text-beige-800">
Blog
{tag && <span> /</span>}
</span>
</Anchor>
{tag && <BlogTagChip tag={tag} colorScheme="default" />}
</div>
<Heading
as="h1"
size="md"
className="mb-0 mt-4 w-[--article-max-width] text-pretty text-center"
>
{title}
</Heading>
<ProductUpdateAuthors
meta={{
authors: Array.isArray(authors) ? authors : [authors],
date,
}}
className="mt-4"
/>
</header>
</>
);
}

View file

@ -0,0 +1,20 @@
import Image, { StaticImageData } from 'next/image';
import { cn } from '@theguild/components';
export function BlogPostPicture({
className,
image,
}: {
className?: string;
image: string | StaticImageData;
}) {
className = cn('h-[324px] rounded-3xl overflow-hidden object-cover', className);
if (typeof image === 'string' && (image.endsWith('.webm') || image.endsWith('.mp4'))) {
return <video className={className} src={image} autoPlay muted loop playsInline />;
}
return (
<Image width={1392} height={324} className={className} src={image} alt="" placeholder="blur" />
);
}

View file

@ -0,0 +1,31 @@
import { Anchor, cn } from '@theguild/components';
import { prettyPrintTag } from './pretty-print-tag';
export interface BlogTagChipProps {
tag: string;
colorScheme: 'default' | 'featured';
inert?: boolean;
}
export function BlogTagChip({ tag, colorScheme, inert }: BlogTagChipProps) {
const className = cn(
'rounded-full px-3 py-1 text-white text-sm',
colorScheme === 'featured'
? 'dark:bg-primary/80 dark:text-neutral-900 bg-green-800'
: 'bg-beige-800 dark:bg-beige-800/40',
!inert &&
(colorScheme === 'featured'
? 'hover:bg-green-900 dark:hover:bg-primary'
: 'hover:bg-beige-900 dark:hover:bg-beige-900/40'),
);
if (inert) {
return <span className={className}>{prettyPrintTag(tag)}</span>;
}
return (
<Anchor href={`/blog/tag/${tag}`} className={className}>
{prettyPrintTag(tag)}
</Anchor>
);
}

View file

@ -0,0 +1,59 @@
import { cn, Heading } from '@theguild/components';
import { BlogCard } from './blog-card';
export function CompanyNewsAndPressSection({ className }: { className?: string }) {
return (
<section className={cn('py-6 lg:py-24', className)}>
<Heading as="h3" size="md" className="text-balance text-center">
Company News and Press
</Heading>
<ul className="mt-6 flex items-stretch gap-4 *:flex-1 max-md:flex-col sm:gap-6 lg:mt-16">
<li>
<BlogCard
variant="featured"
post={{
route: '/blog/understanding-the-differences-between-graphql-and-rest-api-gateways',
frontMatter: {
title: 'Understanding the Differences Between GraphQL and REST API Gateways',
authors: ['saihaj'],
tags: ['graphql'],
date: '2024-12-03',
},
}}
className="h-full"
/>
</li>
<li>
<BlogCard
variant="featured"
post={{
route: '/blog/stellate-acquisition',
frontMatter: {
title: 'The Guild acquires Stellate',
authors: ['uri'],
tags: ['Company', 'GraphQL'],
date: '2024-09-10',
},
}}
className="h-full"
/>
</li>
<li>
<BlogCard
variant="featured"
post={{
route: 'https://the-guild.dev/blog/rebranding-in-open-source',
frontMatter: {
title: 'Rebranding in open source',
authors: ['uri'],
tags: ['branding'],
date: '2024-02-24',
},
}}
className="h-full"
/>
</li>
</ul>
</section>
);
}

View file

@ -0,0 +1,25 @@
import Link from 'next/link';
import { cn } from '@theguild/components';
import { prettyPrintTag } from '../pretty-print-tag';
export function CategoryFilterLink({
category,
currentCategory,
}: {
category: string | null;
currentCategory: string | null;
}) {
return (
<Link
href={category ? `/blog/tag/${category}` : '/blog'}
className={cn(
'hive-focus inline-block min-w-16 rounded-full border border-green-200 px-3 py-2.5 text-center transition duration-100 hover:border-green-800 dark:border-neutral-700 dark:hover:border-neutral-500',
currentCategory === category
? 'text-green-1000 bg-green-200 hover:!bg-transparent hover:text-green-800 dark:bg-neutral-800 dark:text-white dark:hover:text-white/80'
: 'hover:bg-beige-100 hover:text-green-1000 text-green-800 dark:text-white/80 dark:hover:bg-neutral-800 dark:hover:text-white',
)}
>
{prettyPrintTag(category || 'All')}
</Link>
);
}

View file

@ -0,0 +1,22 @@
import { CategoryFilterLink } from './category-filter-link';
export function CategorySelect({
tag: currentTag,
categories,
}: {
tag: string | null;
categories: string[];
}) {
return (
<ul className="flex flex-wrap items-center justify-center gap-2 px-4 py-6">
<li>
<CategoryFilterLink category={null} currentCategory={currentTag} />
</li>
{categories.map(category => (
<li key={category}>
<CategoryFilterLink category={category} currentCategory={currentTag} />
</li>
))}
</ul>
);
}

View file

@ -0,0 +1,34 @@
import { cn } from '@theguild/components';
import { BlogPostFile } from '../../blog-types';
import { BlogCard } from '../blog-card';
export function FeaturedPosts({
posts,
tag,
className,
}: {
posts: BlogPostFile[];
tag: string | null;
className?: string;
}) {
const featuredPosts = posts.filter(post => post.frontMatter.featured).slice(0, 3);
if (featuredPosts.length === 0) {
return null;
}
return (
<ul
className={cn(
'grid grid-cols-1 gap-4 sm:grid sm:grid-cols-2 sm:gap-6 lg:grid-cols-3',
className,
)}
>
{featuredPosts.map(post => (
<li key={post.route}>
<BlogCard post={post} className="h-full" variant="featured" tag={tag} />
</li>
))}
</ul>
);
}

View file

@ -0,0 +1,39 @@
import { cn } from '@theguild/components';
import { BlogPostFile } from '../../blog-types';
import { CategorySelect } from './category-select';
import { FeaturedPosts } from './featured-posts';
import { LatestPosts } from './latest-posts';
const TOP_10_TAGS = [
'graphql',
'graphql-federation',
'codegen',
'typescript',
'react',
'graphql-hive',
'node',
'graphql-modules',
'angular',
'graphql-tools',
];
export function PostsByTag(props: { posts: BlogPostFile[]; tag?: string; className?: string }) {
const tag = props.tag ?? null;
const posts = [...props.posts].sort(
(a, b) => new Date(b.frontMatter.date).getTime() - new Date(a.frontMatter.date).getTime(),
);
let categories = TOP_10_TAGS;
if (tag && !TOP_10_TAGS.includes(tag)) {
categories = [tag, ...TOP_10_TAGS];
}
return (
<section className={cn('px-4 sm:px-6', props.className)}>
<CategorySelect tag={tag} categories={categories} />
<FeaturedPosts posts={posts} className="sm:mb-12 md:mt-16" tag={tag} />
<LatestPosts posts={posts} tag={tag} />
</section>
);
}

View file

@ -0,0 +1,31 @@
import { Heading } from '@theguild/components';
import { BlogPostFile } from '../../blog-types';
import { BlogCard } from '../blog-card';
import { prettyPrintTag } from '../pretty-print-tag';
export function LatestPosts({ posts, tag }: { posts: BlogPostFile[]; tag: string | null }) {
return (
<section className="pt-6 sm:pt-12">
<Heading size="md" as="h2" className="text-center">
Latest posts
{tag ? (
<>
{' '}
in <span>{prettyPrintTag(tag)}</span>
</>
) : (
''
)}
</Heading>
<ul className="mt-6 grid grid-cols-1 gap-4 sm:grid sm:grid-cols-2 sm:gap-6 md:mt-16 lg:grid-cols-3 xl:grid-cols-4">
{posts.map(post => {
return (
<li key={post.route} className="basis-1/3">
<BlogCard post={post} tag={tag} />
</li>
);
})}
</ul>
</section>
);
}

View file

@ -0,0 +1,17 @@
export function prettyPrintTag(tag: string) {
let text = tag || 'All';
if (text === 'typescript') {
text = 'TypeScript';
}
if (text === 'css') {
text = 'CSS';
}
if (text === 'rest') {
text = 'REST';
}
text = text.replace('graphql', 'GraphQL');
text = text.replace(/-js$/, ' JS');
text = text[0].toUpperCase() + text.slice(1);
text = text.replace(/-/g, ' ').replace(/\b\w/g, char => char.toUpperCase());
return text;
}

View file

@ -0,0 +1,22 @@
import { getPageMap } from '@theguild/components/server';
import { isBlogPost } from './blog-types';
import { PostsByTag } from './components/posts-by-tag';
// We can't move this page to `(index)` dir together with `tag` page because Nextra crashes for
// some reason. It will cause an extra rerender on first navigation to a tag page, which isn't
// great, but it's not terrible.
import BlogPageLayout from './tag/layout';
export const metadata = {
title: 'Hive Blog',
};
export default async function BlogPage() {
const [_meta, _indexPage, ...pageMap] = await getPageMap('/blog');
const allPosts = pageMap.filter(isBlogPost);
return (
<BlogPageLayout>
<PostsByTag posts={allPosts} />
</BlogPageLayout>
);
}

View file

@ -0,0 +1,27 @@
import { NextPageProps } from '@theguild/components';
import { getPageMap } from '@theguild/components/server';
import { isBlogPost } from '../../blog-types';
import { PostsByTag } from '../../components/posts-by-tag';
export async function generateMetadata({ params }: NextPageProps<'tag'>) {
return {
title: `Hive Blog - ${(await params).tag}`,
};
}
export default async function BlogTagPage(props: NextPageProps<'tag'>) {
const [_meta, _indexPage, ...pageMap] = await getPageMap('/blog');
const allPosts = pageMap.filter(isBlogPost);
const tag = (await props.params).tag;
const posts = allPosts.filter(post => post.frontMatter.tags.includes(tag));
return <PostsByTag posts={posts} tag={tag} />;
}
export async function generateStaticParams() {
const [_meta, _indexPage, ...pageMap] = await getPageMap('/blog');
const allPosts = pageMap.filter(isBlogPost);
const tags = allPosts.flatMap(post => post.frontMatter.tags);
const uniqueTags = [...new Set(tags)];
return uniqueTags.map(tag => ({ tag }));
}

View file

@ -0,0 +1,16 @@
import { GetYourAPIGameRightSection, HiveLayoutConfig } from '@theguild/components';
import { LandingPageContainer } from '../../../components/landing-page-container';
import { BlogPageHero } from '../components/blog-page-hero';
import { CompanyNewsAndPressSection } from '../components/company-news-and-press-section';
export default function BlogPageLayout({ children }: { children: React.ReactNode }) {
return (
<LandingPageContainer className="text-green-1000 mx-auto max-w-[90rem] overflow-hidden dark:text-white">
<HiveLayoutConfig widths="landing-narrow" />
<BlogPageHero className="mx-4 max-sm:mt-2 md:mx-6" />
{children}
<CompanyNewsAndPressSection className="mx-4 md:mx-6" />
<GetYourAPIGameRightSection className="light text-green-1000 dark:bg-primary/95 mx-4 sm:mb-6 md:mx-6" />
</LandingPageContainer>
);
}

View file

@ -1,25 +1,25 @@
import { GetYourAPIGameWhite } from '#components/get-your-api-game-white';
import { cn } from '@theguild/components';
import { cn, HiveLayoutConfig } from '@theguild/components';
import { CaseStudiesHeader } from '../case-studies-header';
import { MoreStoriesSection } from '../more-stories-section';
import '../case-studies-styles.css';
import '../../hive-prose-styles.css';
import { LookingToUseHiveUpsellBlock } from '../looking-to-use-hive-upsell-block';
// We can't use CSS Modules together with Tailwind,
// because the class responsible for dark mode gets transformed
// so `dark:` prefixes don't work.
const ONE_OFF_CLASS_CASE_STUDIES = 'case-studies';
const MAIN_CONTENT = 'main-content';
export default function CaseStudiesLayout({ children }: { children: React.ReactNode }) {
return (
<div className={cn(ONE_OFF_CLASS_CASE_STUDIES, 'mx-auto box-content max-w-[90rem]')}>
<div className="hive-prose mx-auto box-content max-w-[90rem]">
<HiveLayoutConfig widths="landing-narrow" />
<CaseStudiesHeader className="mx-auto max-w-[--nextra-content-width] pl-6 sm:my-12 md:pl-12 lg:my-24" />
<div className={cn(MAIN_CONTENT, 'mx-auto flex')}>
<div className={cn(MAIN_CONTENT, 'mx-auto flex [&>div:first-of-type>:first-child]:hidden')}>
{children}
<LookingToUseHiveUpsellBlock className="sticky right-2 top-[108px] mb-8 h-min max-lg:hidden lg:w-[320px] xl:w-[400px]" />
</div>
<MoreStoriesSection />
<MoreStoriesSection className="mx-4 md:mx-6" />
<GetYourAPIGameWhite className="sm:my-24" />
</div>
);

View file

@ -3,7 +3,7 @@ import { CaseStudyCard } from './case-study-card';
import { CaseStudyFile } from './case-study-types';
import { getCompanyLogo } from './company-logos';
export async function AllCaseStudiesList({ caseStudies }: { caseStudies: CaseStudyFile[] }) {
export function AllCaseStudiesList({ caseStudies }: { caseStudies: CaseStudyFile[] }) {
return (
<section className="py-6 sm:pt-24">
<Heading size="md" as="h2" className="text-center">

View file

@ -2,12 +2,12 @@
import { cn, DecorationIsolation, Heading } from '@theguild/components';
import { SmallAvatar } from '../../components/small-avatar';
import { CaseStudyAuthor } from './case-study-types';
import { useFrontmatter } from '../../components/use-frontmatter';
import { CaseStudyAuthor, CaseStudyFrontmatter } from './case-study-types';
import { companyLogos } from './company-logos';
import { useFrontmatter } from './use-frontmatter';
export function CaseStudiesHeader(props: React.HTMLAttributes<HTMLDivElement>) {
const { name, frontmatter } = useFrontmatter();
const { name, frontmatter } = useFrontmatter<CaseStudyFrontmatter>();
if (!name) {
throw new Error('unexpected');

View file

@ -1,4 +1,4 @@
import type { getPageMap } from '@theguild/components/server';
import type { MdxFile } from '../../mdx-types';
export type CaseStudyFrontmatter = {
title: string;
@ -13,18 +13,4 @@ export type CaseStudyAuthor = {
avatar?: string;
};
/**
* TODO: This type should be exported from `nextra` and `@theguild/components`
*/
export type MdxFile<FrontMatterType> = {
name: string;
route: string;
frontMatter?: FrontMatterType;
};
export type CaseStudyFile = Required<MdxFile<CaseStudyFrontmatter>>;
/**
* TODO: This should be exported from `nextra` and `@theguild/components`
*/
export type PageMapItem = Awaited<ReturnType<typeof getPageMap>>[number];

View file

@ -1,4 +1,5 @@
import { CaseStudyFile, PageMapItem } from './case-study-types';
import type { PageMapItem } from '../../mdx-types';
import { CaseStudyFile } from './case-study-types';
export function isCaseStudy(item: PageMapItem): item is CaseStudyFile {
return item && 'route' in item && 'name' in item && 'frontMatter' in item && !!item.frontMatter;

View file

@ -1,9 +1,9 @@
'use client';
import { useFrontmatter } from '../../../components/use-frontmatter';
import { CaseStudyCard } from '../case-study-card';
import { CaseStudyFile } from '../case-study-types';
import { getCompanyLogo } from '../company-logos';
import { useFrontmatter } from '../use-frontmatter';
export function OtherCaseStudies({ caseStudies }: { caseStudies: CaseStudyFile[] }) {
const frontmatter = useFrontmatter();

View file

@ -1,22 +0,0 @@
'use client';
import { useConfig } from '@theguild/components';
import { CaseStudyFrontmatter } from './case-study-types';
/**
* TODO: Lobby Dima to add an imperative way to do this in a server component.
*/
export function useFrontmatter() {
const normalizePagesResult = useConfig().normalizePagesResult;
const frontmatter = normalizePagesResult.activeMetadata as CaseStudyFrontmatter;
const name = normalizePagesResult.activePath.at(-1)?.name;
if (!name) {
throw new Error('unexpected');
}
return {
frontmatter,
name,
};
}

View file

@ -1,4 +1,4 @@
.case-studies {
.hive-prose {
--nextra-content-width: 1208px;
--article-max-width: 640px;
@ -7,13 +7,8 @@
width: var(--nextra-content-width);
max-width: 100%;
/* hide leftmost column to left-align the case study */
& > div:first-of-type > :first-child {
@apply hidden;
}
& > div {
@apply ml-0 pl-6 md:pl-12;
@apply ml-0 pl-6 max-sm:pr-6 md:pl-12;
}
& > div > article {
@ -71,3 +66,9 @@
@apply mt-2;
}
}
@media (max-width: 768px) {
.hive-prose {
--article-max-width: 100%;
}
}

View file

@ -1,36 +1,54 @@
'use client';
import { format } from 'date-fns';
import { Anchor, useConfig } from '@theguild/components';
import { authors } from '../../../authors';
import { Anchor, cn, useConfig } from '@theguild/components';
import { AuthorId, authors } from '../../../authors';
import { SocialAvatar } from '../../../components/social-avatar';
type Meta = {
authors: string[];
authors: AuthorId[];
date: string;
title: string;
description: string;
};
const Authors = ({ meta }: { meta: Meta }) => {
export const ProductUpdateAuthors = ({
meta,
className,
}: {
meta: Pick<Meta, 'authors' | 'date'>;
className?: string;
}) => {
const date = meta.date ? new Date(meta.date) : new Date();
if (meta.authors.length === 1) {
const author = authors[meta.authors[0]];
const author = authors[meta.authors[0] as AuthorId];
if (!author) {
throw new Error(`Author ${meta.authors[0]} not found`);
}
return (
<div className="my-5 flex flex-row items-center justify-center">
<div
className={cn(
'has-[a:hover]:bg-beige-900/5 dark:has[a:hover]:bg-neutral-50/5 my-4 -mb-1 flex flex-row items-center justify-center rounded-xl py-1 pl-1 pr-3',
className,
)}
>
<Anchor href={author.link} title={author.name}>
<SocialAvatar author={author} />
</Anchor>
<div className="ml-2.5 flex flex-col">
<Anchor href={author.link} title={author.name} className="font-semibold text-[#777]">
<Anchor
href={author.link}
title={author.name}
className="text-green-1000 font-semibold dark:text-neutral-200"
>
{author.name}
</Anchor>
<time
dateTime={date.toISOString()}
title={`Posted ${format(date, 'EEEE, LLL do y')}`}
className="text-xs text-[#777]"
className="text-green-1000 text-xs dark:text-neutral-200"
>
{format(date, 'EEEE, LLL do y')}
</time>
@ -49,10 +67,18 @@ const Authors = ({ meta }: { meta: Meta }) => {
</time>
<div className="my-5 flex flex-wrap justify-center gap-5">
{meta.authors.map(authorId => {
const author = authors[authorId];
const author = authors[authorId as AuthorId];
if (!author) {
throw new Error(`Author ${authorId} not found`);
}
return (
<div key={authorId}>
<Anchor href={author.link} title={author.name} className="font-semibold text-[#777]">
<Anchor
href={author.link}
title={author.name}
className="text-green-1000 font-semibold dark:text-neutral-200"
>
<SocialAvatar author={author} />
<span className="ml-2.5 text-sm">{author.name}</span>
</Anchor>
@ -71,7 +97,7 @@ export const ProductUpdateHeader = () => {
return (
<div className="x:max-w-[90rem] mx-auto">
<h1 className="mt-12 text-center text-4xl">{metadata.title}</h1>
<Authors meta={metadata} />
<ProductUpdateAuthors meta={metadata} />
</div>
);
};

View file

Before

Width:  |  Height:  |  Size: 757 B

After

Width:  |  Height:  |  Size: 757 B

View file

@ -1,54 +0,0 @@
type Author = {
name: string;
link: `https://${string}`;
github?: string;
twitter?: string;
};
export const authors: Record<string, Author> = {
kamil: {
name: 'Kamil Kisiela',
link: 'https://x.com/kamilkisiela',
github: 'kamilkisiela',
},
laurin: {
name: 'Laurin Quast',
link: 'https://x.com/n1rual',
github: 'n1ru4l',
},
arda: {
name: 'Arda Tanrikulu',
link: 'https://twitter.com/ardatanrikulu',
github: 'ardatan',
},
aleksandra: {
name: 'Aleksandra Sikora',
link: 'https://x.com/aleksandrasays',
github: 'beerose',
},
jiri: {
name: 'Jiri Spac',
link: 'https://x.com/capajj',
github: 'capaj',
},
dimitri: {
name: 'Dimitri Postolov',
link: 'https://x.com/dimaMachina_',
github: 'dimaMachina',
},
denis: {
name: 'Denis Badurina',
link: 'https://github.com/enisdenjo',
github: 'enisdenjo',
},
dotan: {
name: 'Dotan Simha',
link: 'https://github.com/dotansimha',
github: 'dotansimha',
},
jdolle: {
name: 'Jeff Dolle',
link: 'https://github.com/jdolle',
github: 'jdolle',
},
};

View file

@ -0,0 +1,255 @@
import type { StaticImageData } from 'next/image';
import saihajPhoto from './saihaj.webp';
export type Author =
| {
name: string;
link: `https://${string}`;
twitter?: string;
github?: string;
avatar: string | StaticImageData;
}
| {
name: string;
link: `https://${string}`;
twitter?: string;
github: string;
// if the author has no avatar, we'll take it from GitHub
avatar?: string | StaticImageData;
};
export const authors = {
kamil: {
name: 'Kamil Kisiela',
link: 'https://x.com/kamilkisiela',
github: 'kamilkisiela',
},
laurin: {
name: 'Laurin Quast',
link: 'https://twitter.com/n1rual',
github: 'n1ru4l',
},
arda: {
name: 'Arda Tanrikulu',
link: 'https://twitter.com/ardatanrikulu',
github: 'ardatan',
},
aleksandra: {
name: 'Aleksandra Sikora',
link: 'https://x.com/aleksandrasays',
github: 'beerose',
},
jiri: {
name: 'Jiri Spac',
link: 'https://x.com/capajj',
github: 'capaj',
},
dimitri: {
name: 'Dimitri Postolov',
link: 'https://x.com/dimaMachina_',
github: 'dimaMachina',
},
denis: {
name: 'Denis Badurina',
link: 'https://github.com/enisdenjo',
github: 'enisdenjo',
},
dotan: {
name: 'Dotan Simha',
link: 'https://github.com/dotansimha',
github: 'dotansimha',
},
jdolle: {
name: 'Jeff Dolle',
link: 'https://github.com/jdolle',
github: 'jdolle',
},
jason: {
name: 'Jason Kuhrt',
link: 'https://github.com/jasonkuhrt',
github: 'jasonkuhrt',
},
valentin: {
name: 'Valentin Cocaud',
link: 'https://github.com/EmrysMyrddin',
github: 'EmrysMyrddin',
},
tuval: {
name: 'Tuval Simha',
link: 'https://github.com/tuvalsimha',
github: 'tuvalsimha',
},
uri: {
name: 'Uri Goldshtein',
link: 'https://github.com/Urigo',
github: 'Urigo',
},
gil: {
name: 'Gil Gardosh',
link: 'https://github.com/gilgardosh',
github: 'gilgardosh',
},
saihaj: {
name: 'Saihajpreet Singh',
link: 'https://github.com/saihaj',
github: 'saihaj',
avatar: saihajPhoto,
},
eytan: {
name: 'Eytan Manor',
link: 'https://twitter.com/eytan_manor',
github: 'DAB0mB',
},
leonardo: {
name: 'Leonardo Ascione',
link: 'https://twitter.com/leonardfactory',
github: 'leonardfactory',
},
niccolo: {
name: 'Niccolo Belli',
link: 'https://twitter.com/niccolobelli',
github: 'darkbasic',
},
david: {
name: 'David Yahalomi',
link: 'https://twitter.com/DavidYahalomi',
github: 'davidyaha',
},
enisdenjo: {
name: 'Denis Badurina',
link: 'https://twitter.com/enisdenjo',
github: 'enisdenjo',
},
ephelan: {
name: 'Enda Phelan',
link: 'https://twitter.com/PhelanEnda',
github: 'craicoverflow',
},
soichi: {
name: 'Soichi Takamura',
link: 'https://twitter.com/piglovesyou1',
github: 'piglovesyou',
},
giladtidhar: {
name: 'Gilad Tidhar',
link: 'https://twitter.com/tidhar_gilad',
github: 'giladd123',
},
gmac: {
name: 'Greg MacWilliam',
link: 'https://twitter.com/gmacwilliam',
github: 'gmac',
},
croutonn: {
name: 'Yuta Haga',
link: 'https://twitter.com/croutnn',
github: 'croutonn',
},
jycouet: {
name: 'Jean-Yves Couët',
link: 'https://twitter.com/jycouet',
github: 'jycouet',
},
AlecAivazis: {
name: 'Alec Aivazis',
link: 'https://twitter.com/AlecAivazis',
github: 'AlecAivazis',
},
tvvignesh: {
name: 'Vignesh T.V.',
link: 'https://twitter.com/techahoy',
github: 'tvvignesh',
},
charlypoly: {
name: 'Charly Poly',
link: 'https://charlypoly.com',
github: 'charlypoly',
},
maticzav: {
name: 'Matic Zavadlal',
link: 'https://twitter.com/maticzav',
github: 'maticzav',
},
pablosz: {
name: 'Pablo Sáez',
link: 'https://twitter.com/PabloSz_',
github: 'PabloSzx',
},
dimatill: {
name: 'Dmitry Til',
link: 'https://github.com/dimatill',
github: 'dimatill',
},
gthau: {
name: 'Ghislain Thau',
link: 'https://github.com/gthau',
github: 'gthau',
},
notrab: {
name: 'Jamie Barton',
link: 'https://graphql.wtf',
github: 'notrab',
},
tuvalSimha: {
name: 'Tuval Simha',
link: 'https://twitter.com/SimhaTuval',
github: 'TuvalSimha',
},
gabotechs: {
name: 'Gabriel Musat',
link: 'https://github.com/gabotechs',
github: 'gabotechs',
},
shuding: {
name: 'Shu Ding',
link: 'https://shud.in',
github: 'shuding',
},
eddeee888: {
name: 'Eddy Nguyen',
link: 'https://twitter.com/eddeee888',
github: 'eddeee888',
},
tshedor: {
name: 'Tim Shedor',
link: 'https://github.com/tshedor',
github: 'tshedor',
},
josiassejod1: {
name: 'Dalvin Sejour',
link: 'https://github.com/josiassejod1',
github: 'josiassejod1',
},
warrenjday: {
name: 'Warren Day',
link: 'https://twitter.com/warrenjday',
github: 'warrenjday',
},
jessevelden: {
name: 'Jesse van der Velden',
link: 'https://twitter.com/JesseVelden',
github: 'jessevelden',
},
yassin: {
name: 'Yassin Eldeeb',
link: 'https://twitter.com/yassineldeeb7',
github: 'yassineldeeb',
},
chimame: {
name: 'Rito Tamata',
link: 'https://twitter.com/chimame_rt',
github: 'chimame',
},
nohehf: {
name: 'Nohé Hinniger-Foray',
link: 'https://twitter.com/NoheHf',
github: 'nohehf',
},
egoodwinx: {
name: 'Emily Goodwin',
link: 'https://www.linkedin.com/in/emily-y-goodwin/',
github: 'egoodwinx',
},
} satisfies Record<string, Author>;
export type AuthorId = keyof typeof authors;

View file

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 18 KiB

View file

@ -0,0 +1,26 @@
import { ReactElement, ReactNode } from 'react';
import { cn } from '@theguild/components';
/**
* This is used in MDX blog posts.
*/
export function Comparison({
children,
className,
}: {
children: ReactNode;
className: string;
}): ReactElement {
return (
<div
className={cn(
'mt-6 grid grid-cols-2 items-stretch gap-1',
'*:!mt-0 [&_button]:hidden',
'[&_pre]:h-[calc(100%-3rem)]',
className,
)}
>
{children}
</div>
);
}

View file

@ -30,7 +30,7 @@ export function GetYourAPIGameWhite(props: React.HTMLAttributes<HTMLDivElement>)
</div>
</div>
<DecorationIsolation>
<ArchDecoration className="top-6" />
<ArchDecoration className="top-6 max-sm:hidden" />
<ArchDecoration className="right-0 top-6 rotate-180" />
<GradientDefs />
</DecorationIsolation>

View file

@ -91,7 +91,12 @@ export interface LandingPageFeatureTabsProps {
}
export function LandingPageFeatureTabs({ className }: LandingPageFeatureTabsProps) {
const icons = [<SchemaRegistryIcon />, <GraphQLObservabilityIcon />, <GatewayIcon />];
const icons = [
// The keys here are redundant, but Next.js started false positive linting them even if the ESLint integration is disabled.
<SchemaRegistryIcon key="schema-registry-icon" />,
<GraphQLObservabilityIcon key="graphql-observability-icon" />,
<GatewayIcon key="gateway-icon" />,
];
return (
<FeatureTabs className={className} highlights={highlights} icons={icons}>
<FeatureTab

View file

@ -3,6 +3,10 @@
import { ReactElement } from 'react';
import ReactAvatar from 'react-avatar';
/**
* TODO: We should drop this and use the avatars defined in authors/index.ts
* for consistency with the team section on the landing page.
*/
export const SocialAvatar = ({
author,
}: {

View file

@ -1,7 +1,7 @@
import Image, { StaticImageData } from 'next/image';
import Image from 'next/image';
import { CallToAction, cn, Heading } from '@theguild/components';
import { ArrowIcon } from '../arrow-icon';
import saihajPhoto from './saihaj.webp';
import { Author, authors } from '../authors';
import { ArrowIcon } from './arrow-icon';
export function TeamSection({ className }: { className?: string }) {
return (
@ -36,7 +36,7 @@ export function TeamSection({ className }: { className?: string }) {
<TeamGallery
className={cn(
'max-xl:-mx-4 max-xl:max-w-[calc(100%-1rem)] max-xl:px-4 max-xl:py-6 max-lg:max-w-[calc(100%+2rem)] xl:ml-auto',
team.length === 12 ? 'xl:w-[628px]' : 'xl:w-[664px]',
teamMembers.length === 12 ? 'xl:w-[628px]' : 'xl:w-[664px]',
)}
/>
</div>
@ -44,67 +44,19 @@ export function TeamSection({ className }: { className?: string }) {
);
}
type TeamMember = [name: string, avatar: string | StaticImageData, social: string];
const team: TeamMember[] = [
[
'Denis Badurina',
'https://avatars.githubusercontent.com/enisdenjo?v=4&s=180',
'https://github.com/enisdenjo',
],
[
'Dotan Simha',
'https://avatars.githubusercontent.com/dotansimha?v=4&s=180',
'https://github.com/dotansimha',
],
[
'Gil Gardosh',
'https://avatars.githubusercontent.com/gilgardosh?v=4&s=180',
'https://github.com/gilgardosh',
],
[
'Kamil Kisiela',
'https://avatars.githubusercontent.com/kamilkisiela?v=4&s=180',
'https://github.com/kamilkisiela',
],
[
'Laurin Quast',
'https://avatars.githubusercontent.com/n1ru4l?v=4&s=180',
'https://github.com/n1ru4l',
],
// ['Noam Malka', noamPhoto, 'https://noam-malka.com/'],
['Saihajpreet Singh', saihajPhoto, 'https://github.com/saihaj'],
[
'Tuval Simha',
'https://avatars.githubusercontent.com/tuvalsimha?v=4&s=180',
'https://github.com/tuvalsimha',
],
[
'Uri Goldshtein',
'https://avatars.githubusercontent.com/Urigo?v=4&s=180',
'https://github.com/Urigo',
],
[
'Valentin Cocaud',
'https://avatars.githubusercontent.com/EmrysMyrddin?v=4&s=180',
'https://github.com/EmrysMyrddin',
],
[
'Jason Kuhrt',
'https://avatars.githubusercontent.com/jasonkuhrt?v=4&s=180',
'https://github.com/jasonkuhrt',
],
[
'Arda Tanrikulu',
'https://avatars.githubusercontent.com/ardatan?v=4&s=180',
'https://github.com/ardatan',
],
[
'Jeff Dolle',
'https://avatars.githubusercontent.com/jdolle?v=4&s=180',
'https://github.com/jdolle',
],
const teamMembers: Author[] = [
authors.denis,
authors.dotan,
authors.gil,
authors.kamil,
authors.laurin,
authors.saihaj,
authors.tuval,
authors.uri,
authors.valentin,
authors.jason,
authors.arda,
authors.jdolle,
];
function TeamGallery(props: React.HTMLAttributes<HTMLElement>) {
@ -114,14 +66,14 @@ function TeamGallery(props: React.HTMLAttributes<HTMLElement>) {
className={cn(
'nextra-scrollbar flex shrink-0 grid-cols-5 flex-row items-stretch justify-items-stretch gap-2 [scrollbar-color:#00000029_transparent] [scrollbar-width:auto] max-lg:overflow-auto lg:flex-wrap lg:gap-6 lg:max-xl:grid',
'[--size:120px]',
team.length <= 12 && 'grid-cols-6 xl:[&>:nth-child(8n-7)]:ml-[calc(var(--size)/2)]',
team.length === 13 &&
teamMembers.length <= 12 && 'grid-cols-6 xl:[&>:nth-child(8n-7)]:ml-[calc(var(--size)/2)]',
teamMembers.length === 13 &&
'grid-cols-5 xl:[--size:112px] xl:[&>:nth-child(9n-8)]:ml-[calc(var(--size)/2)]',
team.length > 13 && 'nextra-scrollbar size-full flex-col p-1 xl:overflow-scroll',
teamMembers.length > 13 && 'nextra-scrollbar size-full flex-col p-1 xl:overflow-scroll',
props.className,
)}
>
{team.map((member, i) => (
{teamMembers.map((member, i) => (
<li key={i}>
<TeamAvatar data={member} />
</li>
@ -130,11 +82,11 @@ function TeamGallery(props: React.HTMLAttributes<HTMLElement>) {
);
}
function TeamAvatar({ data: [name, avatar, social] }: { data: TeamMember }) {
function TeamAvatar({ data: { name, avatar, link, github } }: { data: Author }) {
return (
<a
className="group relative flex flex-col focus-visible:outline-none focus-visible:ring-transparent focus-visible:ring-offset-transparent"
href={social}
href={link}
target="_blank"
rel="noreferrer"
>
@ -143,9 +95,11 @@ function TeamAvatar({ data: [name, avatar, social] }: { data: TeamMember }) {
<Image
alt=""
className="firefox:bg-blend-multiply firefox:![filter:grayscale(1)] rounded-2xl bg-black brightness-100 grayscale transition-all duration-500 group-hover:scale-[1.03] group-hover:brightness-110"
{...(typeof avatar === 'string'
? { src: avatar }
: { blurDataURL: avatar.blurDataURL, src: avatar.src })}
{...(!avatar
? { src: `https://avatars.githubusercontent.com/${github}?v=4&s=180` }
: typeof avatar === 'string'
? { src: avatar }
: { blurDataURL: avatar.blurDataURL, src: avatar.src })}
width={180}
height={180}
/>

View file

@ -0,0 +1,26 @@
'use client';
import { useConfig } from '@theguild/components';
/**
* Dima said there's no possible way to access frontmatter imperatively
* from a server component in Nextra, so we have a hook for client components.
*/
export function useFrontmatter<
// this is unsafe, but we're in a blog, and the frontmatter type is
// controlled by us. if it crashes, we can just fix the frontmatter in markdown
TFrontmatter,
>() {
const normalizePagesResult = useConfig().normalizePagesResult;
const frontmatter = normalizePagesResult.activeMetadata as TFrontmatter;
const name = normalizePagesResult.activePath.at(-1)?.name;
if (!name) {
throw new Error('unexpected');
}
return {
frontmatter,
name,
};
}

View file

@ -0,0 +1,15 @@
import type { getPageMap } from '@theguild/components/server';
/**
* TODO: This type should be exported from `nextra` and `@theguild/components`
*/
export type MdxFile<FrontMatterType> = {
name: string;
route: string;
frontMatter?: FrontMatterType;
};
/**
* TODO: This should be exported from `nextra` and `@theguild/components`
*/
export type PageMapItem = Awaited<ReturnType<typeof getPageMap>>[number];

View file

@ -0,0 +1,13 @@
diff --git a/dist/components/breadcrumb.js b/dist/components/breadcrumb.js
index 598b5bbc14e9521edfa53956555e57f36a821f1d..352907a9b299267559615e333f6a4884867658eb 100644
--- a/dist/components/breadcrumb.js
+++ b/dist/components/breadcrumb.js
@@ -29,7 +29,7 @@ const Breadcrumb = (t0) => {
};
function _temp(item, index, arr) {
const nextItem = arr[index + 1];
- const href = nextItem ? "frontMatter" in item ? item.route : item.children[0].route === nextItem.route ? "" : item.children[0].route : "";
+ const href = nextItem ? "frontMatter" in item ? item.route : item.children[0]?.route === nextItem.route ? "" : item.children[0]?.route : "";
const ComponentToUse = href ? NextLink : "span";
return /* @__PURE__ */ jsxs(Fragment, { children: [
index > 0 && /* @__PURE__ */ jsx(ArrowRightIcon, { height: "14", className: "x:shrink-0 x:rtl:rotate-180" }),

26
patches/nextra.patch Normal file
View file

@ -0,0 +1,26 @@
diff --git a/dist/server/compile.js b/dist/server/compile.js
index 4b3f41397995dd4c796f6945627fb14d43776eb3..889f926c097db987f8e9d262e015ae9a1f89664b 100644
--- a/dist/server/compile.js
+++ b/dist/server/compile.js
@@ -9,6 +9,7 @@ import remarkGfm from "remark-gfm";
import remarkMath from "remark-math";
import remarkReadingTime from "remark-reading-time";
import remarkSmartypants from "remark-smartypants";
+import { rehypeFrontmatterMdxImports } from 'rehype-frontmatter-mdx-imports';
import { MARKDOWN_URL_EXTENSION_RE } from "./constants.js";
import { recmaRewrite } from "./recma-plugins/index.js";
import {
@@ -142,7 +143,12 @@ async function compileMdx(source, {
rehypeTwoslashPopup,
[rehypeAttachCodeMeta, { search }]
],
- rehypeExtractTocContent
+ rehypeExtractTocContent,
+ [rehypeFrontmatterMdxImports, {
+ keys: ['thumbnail', 'image'],
+ importedAssetPathRegex: /(png|jpg|jpeg|gif|webp)$/,
+ fileRegex: /\/blog\/\(posts\)/,
+ }]
].filter((v) => !!v),
recmaPlugins: [
...recmaPlugins || [],

View file

@ -55,6 +55,12 @@ patchedDependencies:
mjml-core@4.14.0:
hash: 52f1e476e154edea0222aa95e55676888525198d882becc3b362511b77fd7e7f
path: patches/mjml-core@4.14.0.patch
nextra:
hash: c1d11430a02e4d51d69b615df3f615fd6dfbccfd71b122bcf781a8a35208fbc1
path: patches/nextra.patch
nextra-theme-docs:
hash: 38956679ac61493f4dbc6862445316e9909dd989c221357f4b21ce70d8c8fd5b
path: patches/nextra-theme-docs.patch
oclif:
hash: 0aac7abfb9211ffbd102f85b840f7fc41d8f64bcb6e71e197f2ad539e58ba9a6
path: patches/oclif.patch
@ -2069,6 +2075,9 @@ importers:
react-icons:
specifier: 5.4.0
version: 5.4.0(react@19.0.0)
rehype-frontmatter-mdx-imports:
specifier: 0.1.1
version: 0.1.1(typescript@5.7.3)
tailwind-merge:
specifier: 2.6.0
version: 2.6.0
@ -14020,6 +14029,11 @@ packages:
resolution: {integrity: sha512-qx+xQGZVsy55CH0a1hiVwHmqjLryfh7wQyF5HO07XJ9f7dQMY/gPQHhlyDkIzJKC+x2fUCpCcUODUUUFrm7SHA==}
hasBin: true
rehype-frontmatter-mdx-imports@0.1.1:
resolution: {integrity: sha512-1fkGdtwxjojoA2TDy/swedxpDR8IGB+seKnp7RyKvsthdGU68AckH1G+mSgqBWdaWbr6om6W1rkiE4QXCAqZrg==}
peerDependencies:
typescript: ^5.0.0
rehype-katex@7.0.0:
resolution: {integrity: sha512-h8FPkGE00r2XKU+/acgqwWUlyzve1IiOKwsEkg4pDL3k48PiE0Pt+/uLtVHDVkN1yA4iurZN6UES8ivHVEQV6Q==}
@ -23956,8 +23970,8 @@ snapshots:
clsx: 2.1.1
fuzzy: 0.1.3
next: 15.2.3(@babel/core@7.22.9)(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
nextra: 4.0.5(next@15.2.3(@babel/core@7.22.9)(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(typescript@5.7.3)
nextra-theme-docs: 4.0.5(@types/react@18.3.18)(immer@10.1.1)(next@15.2.3(@babel/core@7.22.9)(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(nextra@4.0.5(next@15.2.3(@babel/core@7.22.9)(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(typescript@5.7.3))(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(use-sync-external-store@1.2.0(react@19.0.0))
nextra: 4.0.5(patch_hash=c1d11430a02e4d51d69b615df3f615fd6dfbccfd71b122bcf781a8a35208fbc1)(next@15.2.3(@babel/core@7.22.9)(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(typescript@5.7.3)
nextra-theme-docs: 4.0.5(patch_hash=38956679ac61493f4dbc6862445316e9909dd989c221357f4b21ce70d8c8fd5b)(@types/react@18.3.18)(immer@10.1.1)(next@15.2.3(@babel/core@7.22.9)(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(nextra@4.0.5(patch_hash=c1d11430a02e4d51d69b615df3f615fd6dfbccfd71b122bcf781a8a35208fbc1)(next@15.2.3(@babel/core@7.22.9)(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(typescript@5.7.3))(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(use-sync-external-store@1.2.0(react@19.0.0))
react: 19.0.0
react-dom: 19.0.0(react@19.0.0)
react-paginate: 8.2.0(react@19.0.0)
@ -30357,13 +30371,13 @@ snapshots:
- '@babel/core'
- babel-plugin-macros
nextra-theme-docs@4.0.5(@types/react@18.3.18)(immer@10.1.1)(next@15.2.3(@babel/core@7.22.9)(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(nextra@4.0.5(next@15.2.3(@babel/core@7.22.9)(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(typescript@5.7.3))(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(use-sync-external-store@1.2.0(react@19.0.0)):
nextra-theme-docs@4.0.5(patch_hash=38956679ac61493f4dbc6862445316e9909dd989c221357f4b21ce70d8c8fd5b)(@types/react@18.3.18)(immer@10.1.1)(next@15.2.3(@babel/core@7.22.9)(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(nextra@4.0.5(patch_hash=c1d11430a02e4d51d69b615df3f615fd6dfbccfd71b122bcf781a8a35208fbc1)(next@15.2.3(@babel/core@7.22.9)(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(typescript@5.7.3))(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(use-sync-external-store@1.2.0(react@19.0.0)):
dependencies:
'@headlessui/react': 2.2.0(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
clsx: 2.1.1
next: 15.2.3(@babel/core@7.22.9)(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
next-themes: 0.4.3(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
nextra: 4.0.5(next@15.2.3(@babel/core@7.22.9)(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(typescript@5.7.3)
nextra: 4.0.5(patch_hash=c1d11430a02e4d51d69b615df3f615fd6dfbccfd71b122bcf781a8a35208fbc1)(next@15.2.3(@babel/core@7.22.9)(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(typescript@5.7.3)
react: 19.0.0
react-compiler-runtime: 0.0.0-experimental-22c6e49-20241219(react@19.0.0)
react-dom: 19.0.0(react@19.0.0)
@ -30376,7 +30390,7 @@ snapshots:
- immer
- use-sync-external-store
nextra@4.0.5(next@15.2.3(@babel/core@7.22.9)(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(typescript@5.7.3):
nextra@4.0.5(patch_hash=c1d11430a02e4d51d69b615df3f615fd6dfbccfd71b122bcf781a8a35208fbc1)(next@15.2.3(@babel/core@7.22.9)(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(typescript@5.7.3):
dependencies:
'@formatjs/intl-localematcher': 0.5.5
'@headlessui/react': 2.2.0(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
@ -31832,6 +31846,10 @@ snapshots:
dependencies:
jsesc: 0.5.0
rehype-frontmatter-mdx-imports@0.1.1(typescript@5.7.3):
dependencies:
typescript: 5.7.3
rehype-katex@7.0.0:
dependencies:
'@types/hast': 3.0.4