mirror of
https://github.com/graphql-hive/console
synced 2026-04-21 14:37:17 +00:00
Hive Blog (#6625)
Co-authored-by: Dotan Simha <dotansimha@users.noreply.github.com>
This commit is contained in:
parent
e6a970f790
commit
4a865c03b2
47 changed files with 1268 additions and 204 deletions
|
|
@ -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'),
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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": {
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
18
packages/web/docs/src/app/blog/(posts)/layout.tsx
Normal file
18
packages/web/docs/src/app/blog/(posts)/layout.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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.
|
||||
5
packages/web/docs/src/app/blog/_meta.ts
Normal file
5
packages/web/docs/src/app/blog/_meta.ts
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
export default {
|
||||
'*': {
|
||||
display: 'hidden',
|
||||
},
|
||||
};
|
||||
21
packages/web/docs/src/app/blog/blog-types.ts
Normal file
21
packages/web/docs/src/app/blog/blog-types.ts
Normal 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;
|
||||
}
|
||||
89
packages/web/docs/src/app/blog/components/blog-card.tsx
Normal file
89
packages/web/docs/src/app/blog/components/blog-card.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
53
packages/web/docs/src/app/blog/components/blog-page-hero.tsx
Normal file
53
packages/web/docs/src/app/blog/components/blog-page-hero.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
@ -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" />
|
||||
);
|
||||
}
|
||||
31
packages/web/docs/src/app/blog/components/blog-tag-chip.tsx
Normal file
31
packages/web/docs/src/app/blog/components/blog-tag-chip.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
22
packages/web/docs/src/app/blog/page.tsx
Normal file
22
packages/web/docs/src/app/blog/page.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
27
packages/web/docs/src/app/blog/tag/[tag]/page.tsx
Normal file
27
packages/web/docs/src/app/blog/tag/[tag]/page.tsx
Normal 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 }));
|
||||
}
|
||||
16
packages/web/docs/src/app/blog/tag/layout.tsx
Normal file
16
packages/web/docs/src/app/blog/tag/layout.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
|
|
|
|||
|
|
@ -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];
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
|
@ -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%;
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 757 B After Width: | Height: | Size: 757 B |
|
|
@ -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',
|
||||
},
|
||||
};
|
||||
255
packages/web/docs/src/authors/index.ts
Normal file
255
packages/web/docs/src/authors/index.ts
Normal 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;
|
||||
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 18 KiB |
26
packages/web/docs/src/components/comparison.tsx
Normal file
26
packages/web/docs/src/components/comparison.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}: {
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
/>
|
||||
26
packages/web/docs/src/components/use-frontmatter.ts
Normal file
26
packages/web/docs/src/components/use-frontmatter.ts
Normal 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,
|
||||
};
|
||||
}
|
||||
15
packages/web/docs/src/mdx-types.ts
Normal file
15
packages/web/docs/src/mdx-types.ts
Normal 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];
|
||||
13
patches/nextra-theme-docs.patch
Normal file
13
patches/nextra-theme-docs.patch
Normal 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
26
patches/nextra.patch
Normal 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 || [],
|
||||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in a new issue