diff --git a/.prettierrc b/.prettierrc deleted file mode 100644 index 4041064071..0000000000 --- a/.prettierrc +++ /dev/null @@ -1,22 +0,0 @@ -{ - "arrowParens": "avoid", - "semi": false, - "singleQuote": false, - "useTabs": false, - "tabWidth": 2, - "overrides": [ - { - "files": "*.svg", - "options": { "parser": "html" } - }, - { - "files": "*.mdx", - "options": { - "proseWrap": "always", - "semi": false, - "trailingComma": "none" - } - } - ], - "plugins": ["prettier-plugin-pkg", "prettier-plugin-tailwindcss"] -} diff --git a/next.config.js b/next.config.js index 8d7395ddb3..33ff819246 100644 --- a/next.config.js +++ b/next.config.js @@ -173,6 +173,7 @@ const config = { }, ] }, + typedRoutes: true, } const withBundleAnalyzer = nextBundleAnalyzer({ diff --git a/prettier.config.mjs b/prettier.config.mjs new file mode 100644 index 0000000000..f6439a7795 --- /dev/null +++ b/prettier.config.mjs @@ -0,0 +1,32 @@ +import { dirname, resolve } from "path" +import { fileURLToPath } from "url" + +const __dirname = dirname(fileURLToPath(import.meta.url)) + +/** + * @type {import("prettier").Config} + */ +export default { + arrowParens: "avoid", + semi: false, + singleQuote: false, + useTabs: false, + tabWidth: 2, + overrides: [ + { + files: "*.svg", + options: { parser: "html" }, + }, + { + files: "*.mdx", + options: { + proseWrap: "always", + semi: false, + trailingComma: "none", + }, + }, + ], + plugins: ["prettier-plugin-pkg", "prettier-plugin-tailwindcss"], + // We need this to ensure classes format the same across CI and editors. + tailwindConfig: resolve(__dirname, "./tailwind.config.ts"), +} diff --git a/src/app/conf/_design-system/utils/arrows-move-sideways.tsx b/src/app/conf/_design-system/utils/arrows-move-sideways.tsx new file mode 100644 index 0000000000..69531ea2c5 --- /dev/null +++ b/src/app/conf/_design-system/utils/arrows-move-sideways.tsx @@ -0,0 +1,36 @@ +export function arrowsMoveSideways(event: React.KeyboardEvent) { + if ( + event.key !== "ArrowLeft" && + event.key !== "ArrowRight" && + event.key !== "ArrowUp" && + event.key !== "ArrowDown" + ) { + return + } + + let repeat = 1 + + if (event.key === "ArrowDown" || event.key === "ArrowUp") { + const vertical = event.currentTarget.dataset.vertical + console.log({ vertical }) + repeat = vertical ? parseInt(vertical) : 1 + } + + let current = event.currentTarget + for (let i = 0; i < repeat; ++i) { + if (event.key === "ArrowLeft" || event.key === "ArrowUp") { + const previousElement = current.previousElementSibling + if (previousElement) { + current = previousElement as HTMLElement + } + } else if (event.key === "ArrowRight" || event.key === "ArrowDown") { + const nextElement = current.nextElementSibling + if (nextElement) { + current = nextElement as HTMLElement + } + } + } + + event.preventDefault() + current.focus() +} diff --git a/src/components/blog-page/blog-card-picture.tsx b/src/components/blog-page/blog-card-picture.tsx new file mode 100644 index 0000000000..0bb4f386c8 --- /dev/null +++ b/src/components/blog-page/blog-card-picture.tsx @@ -0,0 +1,453 @@ +import { clsx } from "clsx" +import { type ReactNode, useEffect, useRef } from "react" + +const PIXEL_SIZE = 18 +const UINT32_MAX = 0xffffffff + +interface BlogCardPictureProps { + frontMatter: { + title: string + tags?: string[] + } + children?: ReactNode + className?: string + style?: React.CSSProperties +} + +type RgbColor = [number, number, number] + +interface OklchColor { + l: number + c: number + h: number +} + +interface GradientStop { + offset: number + color: OklchColor +} + +interface PreparedGradient { + cos: number + sin: number + minProjection: number + invProjectionRange: number + stops: GradientStop[] +} + +export function BlogCardPicture({ + frontMatter, + children, + className, + style, +}: BlogCardPictureProps) { + const containerRef = useRef(null) + const canvasRef = useRef(null) + + const seed = frontMatter.title + + useEffect(() => { + const canvas = canvasRef.current + const container = containerRef.current + if (!canvas || !container) { + return + } + + const draw = () => { + const rect = container.getBoundingClientRect() + const width = Math.max(0, Math.round(rect.width)) + const height = Math.max(0, Math.round(rect.height)) + + if (width === 0 || height === 0) { + return + } + + const columns = Math.max(1, Math.ceil(width / PIXEL_SIZE)) + const rows = Math.max(1, Math.ceil(height / PIXEL_SIZE)) + const canvasWidth = Math.round(width) + const canvasHeight = Math.round(height) + + if (canvas.width !== canvasWidth) { + canvas.width = canvasWidth + } + if (canvas.height !== canvasHeight) { + canvas.height = canvasHeight + } + + const displayWidth = `${width}px` + const displayHeight = `${height}px` + if (canvas.style.width !== displayWidth) { + canvas.style.width = displayWidth + } + if (canvas.style.height !== displayHeight) { + canvas.style.height = displayHeight + } + + const context = canvas.getContext("2d") + if (!context) { + return + } + + context.setTransform(1, 0, 0, 1, 0, 0) + context.clearRect(0, 0, width, height) + context.imageSmoothingEnabled = false + + const random = createSeededRandom(seed) + const angle = random() * Math.PI * 2 + const gradientStops = buildGradientStops(random) + const gradient = prepareGradient(angle, gradientStops, columns, rows) + + for (let row = 0; row < rows; row += 1) { + for (let column = 0; column < columns; column += 1) { + const color = sampleGradientColor(gradient, column, row, seed) + + const x = column * PIXEL_SIZE + const y = row * PIXEL_SIZE + const cellWidth = Math.min(PIXEL_SIZE, width - x) + const cellHeight = Math.min(PIXEL_SIZE, height - y) + + context.fillStyle = toCssColor(color) + context.fillRect(x, y, cellWidth, cellHeight) + } + } + } + + draw() + + canvas.dataset.visible = "true" + }, [seed]) + + return ( +
+
+ ) +} + +function createSeededRandom(seed: string) { + let state = hashString(seed) || 1 + return () => { + state ^= state << 13 + state ^= state >>> 17 + state ^= state << 5 + state >>>= 0 + return state / UINT32_MAX + } +} + +function prepareGradient( + angle: number, + stops: GradientStop[], + columns: number, + rows: number, +): PreparedGradient { + const cos = Math.cos(angle) + const sin = Math.sin(angle) + + const corners: Array<[number, number]> = [ + [0, 0], + [columns, 0], + [0, rows], + [columns, rows], + ] + + let minProjection = Infinity + let maxProjection = -Infinity + + for (const [x, y] of corners) { + const projection = x * cos + y * sin + if (projection < minProjection) { + minProjection = projection + } + if (projection > maxProjection) { + maxProjection = projection + } + } + + const range = Math.max(maxProjection - minProjection, 1e-6) + + return { + cos, + sin, + minProjection, + invProjectionRange: 1 / range, + stops, + } +} + +function buildGradientStops(random: () => number): GradientStop[] { + const baseHue = random() * 360 + + const midOffset = clamp(0.25, 0.75, 0.44 + (random() - 0.5) * 0.22) + const secondaryOffset = clamp( + 0.58, + 0.85, + midOffset + 0.16 + (random() - 0.5) * 0.08, + ) + const offsets = [0, midOffset, secondaryOffset, 1] + + const paletteBlueprint: Array<{ + hueOffset: number + hueJitter: number + lightnessBase: number + lightnessJitter: number + chromaBase: number + chromaJitter: number + chromaMin: number + chromaMax: number + }> = [ + { + hueOffset: -18, + hueJitter: 10, + lightnessBase: 0.74, + lightnessJitter: 0.06, + chromaBase: 0.18, + chromaJitter: 0.12, + chromaMin: 0.12, + chromaMax: 0.28, + }, + { + hueOffset: 8, + hueJitter: 12, + lightnessBase: 0.64, + lightnessJitter: 0.06, + chromaBase: 0.24, + chromaJitter: 0.12, + chromaMin: 0.18, + chromaMax: 0.32, + }, + { + hueOffset: 26, + hueJitter: 14, + lightnessBase: 0.78, + lightnessJitter: 0.06, + chromaBase: 0.2, + chromaJitter: 0.1, + chromaMin: 0.14, + chromaMax: 0.28, + }, + { + hueOffset: 38, + hueJitter: 12, + lightnessBase: 0.72, + lightnessJitter: 0.06, + chromaBase: 0.2, + chromaJitter: 0.1, + chromaMin: 0.14, + chromaMax: 0.28, + }, + ] + + const stops = offsets.map((offset, index) => { + const blueprint = + paletteBlueprint[index] ?? paletteBlueprint[paletteBlueprint.length - 1] + const hue = normalizeHue( + baseHue + blueprint.hueOffset + (random() - 0.5) * blueprint.hueJitter, + ) + const lightness = clamp( + 0.56, + 0.84, + blueprint.lightnessBase + (random() - 0.5) * blueprint.lightnessJitter, + ) + const chroma = clamp( + blueprint.chromaMin, + blueprint.chromaMax, + blueprint.chromaBase + (random() - 0.5) * blueprint.chromaJitter, + ) + return { + offset, + color: { + l: lightness, + c: chroma, + h: hue, + }, + } + }) + + return stops +} + +function sampleGradientColor( + gradient: PreparedGradient, + column: number, + row: number, + jitterPrefix: string, +): RgbColor { + const x = column + 0.5 + const y = row + 0.5 + + const projection = x * gradient.cos + y * gradient.sin + const baseT = + (projection - gradient.minProjection) * gradient.invProjectionRange + const noise = hashString(`${jitterPrefix}:${column}:${row}`) / UINT32_MAX + const t = clamp(0, 1, baseT + (noise - 0.5) * 0.045) + + const oklch = evaluateGradient(gradient.stops, t) + return oklchToSrgb(oklch) +} + +function evaluateGradient(stops: GradientStop[], t: number): OklchColor { + if (stops.length === 0) { + return { l: 0.72, c: 0.18, h: 0 } + } + + if (t <= stops[0].offset) { + return stops[0].color + } + + for (let index = 1; index < stops.length; index += 1) { + const stop = stops[index] + const previous = stops[index - 1] + if (t <= stop.offset) { + const span = Math.max(stop.offset - previous.offset, 1e-6) + const amount = (t - previous.offset) / span + return mixOklch(previous.color, stop.color, amount) + } + } + + return stops[stops.length - 1].color +} + +function toCssColor([r, g, b]: RgbColor) { + return `rgb(${Math.round(r)}, ${Math.round(g)}, ${Math.round(b)})` +} + +function normalizeHue(hue: number) { + return ((hue % 360) + 360) % 360 +} + +function clamp(min: number, max: number, value: number) { + return Math.min(max, Math.max(min, value)) +} + +function hashString(value: string) { + let hash = 2166136261 + for (let index = 0; index < value.length; index += 1) { + hash ^= value.charCodeAt(index) + hash = Math.imul(hash, 16777619) + } + return hash >>> 0 +} + +function mixOklch(a: OklchColor, b: OklchColor, amount: number): OklchColor { + const hueStep = shortestHueDistance(a.h, b.h) + return { + l: a.l + (b.l - a.l) * amount, + c: a.c + (b.c - a.c) * amount, + h: normalizeHue(a.h + hueStep * amount), + } +} + +function shortestHueDistance(start: number, end: number) { + const startNorm = normalizeHue(start) + const endNorm = normalizeHue(end) + let diff = endNorm - startNorm + if (diff > 180) { + diff -= 360 + } else if (diff < -180) { + diff += 360 + } + return diff +} + +function oklchToSrgb({ l, c, h }: OklchColor): RgbColor { + const hueRad = (h / 180) * Math.PI + const a = Math.cos(hueRad) * c + const b = Math.sin(hueRad) * c + + const l_ = l + 0.3963377774 * a + 0.2158037573 * b + const m_ = l - 0.1055613458 * a - 0.0638541728 * b + const s_ = l - 0.0894841775 * a - 1.291485548 * b + + const l3 = l_ * l_ * l_ + const m3 = m_ * m_ * m_ + const s3 = s_ * s_ * s_ + + const rLinear = 4.0767416621 * l3 - 3.3077115913 * m3 + 0.2309699292 * s3 + const gLinear = -1.2684380046 * l3 + 2.6097574011 * m3 - 0.3413193965 * s3 + const bLinear = -0.0041960863 * l3 - 0.7034186147 * m3 + 1.707614701 * s3 + + return [linearToSrgb(rLinear), linearToSrgb(gLinear), linearToSrgb(bLinear)] +} + +function linearToSrgb(value: number) { + const clamped = clamp(0, 1, value) + if (clamped <= 0.0031308) { + return clamped * 12.92 * 255 + } + return (1.055 * Math.pow(clamped, 1 / 2.4) - 0.055) * 255 +} + +function hexToRgb(hex: string): RgbColor | undefined { + const normalized = hex.trim().replace(/^#/, "") + if (normalized.length === 3) { + const r = normalized[0] + const g = normalized[1] + const b = normalized[2] + return [r, g, b].map(ch => parseInt(ch + ch, 16)) as RgbColor + } + if (normalized.length === 6) { + const r = parseInt(normalized.slice(0, 2), 16) + const g = parseInt(normalized.slice(2, 4), 16) + const b = parseInt(normalized.slice(4, 6), 16) + if (Number.isNaN(r) || Number.isNaN(g) || Number.isNaN(b)) { + return undefined + } + return [r, g, b] as RgbColor + } + return undefined +} + +function hexToOklch(hex: string): OklchColor | undefined { + const rgb = hexToRgb(hex) + if (!rgb) { + return undefined + } + return srgbToOklch(rgb) +} + +function srgbToOklch([r255, g255, b255]: RgbColor): OklchColor { + const r = srgbToLinear(r255 / 255) + const g = srgbToLinear(g255 / 255) + const b = srgbToLinear(b255 / 255) + + const l = 0.4122214708 * r + 0.5363325363 * g + 0.0514459929 * b + const m = 0.2119034982 * r + 0.6806995451 * g + 0.1073969566 * b + const s = 0.0883024619 * r + 0.2817188376 * g + 0.6299787005 * b + + const l_ = Math.cbrt(l) + const m_ = Math.cbrt(m) + const s_ = Math.cbrt(s) + + const L = 0.2104542553 * l_ + 0.793617785 * m_ - 0.0040720468 * s_ + const a = 1.9779984951 * l_ - 2.428592205 * m_ + 0.4505937099 * s_ + const bVal = 0.0259040371 * l_ + 0.7827717662 * m_ - 0.808675766 * s_ + + const chroma = Math.sqrt(a * a + bVal * bVal) + const hueRadians = Math.atan2(bVal, a) + const hueDegrees = ((hueRadians * 180) / Math.PI + 360) % 360 + + return { + l: clamp(0, 1, L), + c: chroma, + h: chroma < 1e-6 ? 0 : hueDegrees, + } +} + +function srgbToLinear(value: number) { + return value <= 0.04045 + ? value / 12.92 + : Math.pow((value + 0.055) / 1.055, 2.4) +} diff --git a/src/components/blog-page/blog-card.tsx b/src/components/blog-page/blog-card.tsx new file mode 100644 index 0000000000..349f5b4454 --- /dev/null +++ b/src/components/blog-page/blog-card.tsx @@ -0,0 +1,102 @@ +import { clsx } from "clsx" +import NextLink from "next/link" + +import ArrowDown from "@/app/conf/_design-system/pixelarticons/arrow-down.svg?svgr" +import { arrowsMoveSideways } from "@/app/conf/_design-system/utils/arrows-move-sideways" + +import { BlogTags } from "./blog-tags" +import { BlogCardPicture } from "./blog-card-picture" + +export interface BlogCardProps extends React.HTMLAttributes { + frontMatter: { + title: string + date: Date + tags: string[] + byline: string + } + route: string +} + +export function BlogCard({ + route, + frontMatter, + className, + ...rest +}: BlogCardProps) { + return ( +
+ + + +
+
+ {frontMatter.title} +
+
+ + +
+
+
+
+ ) +} + +export function BlogCardArrow({ className }: { className?: string }) { + return ( +
+ +
+ ) +} + +export function BlogCardFooterContent({ + byline, + date, + className, +}: { + byline: string + date: Date + className?: string +}) { + return ( + + {byline} + + + ) +} + +export { BlogCardPicture } from "./blog-card-picture" diff --git a/src/components/blog-page/blog-tag-colors.tsx b/src/components/blog-page/blog-tag-colors.tsx new file mode 100644 index 0000000000..0a69f779d6 --- /dev/null +++ b/src/components/blog-page/blog-tag-colors.tsx @@ -0,0 +1,9 @@ +export const blogTagColors: Record = { + newsletter: "#FFCCEF", + announcements: "#F80", + blog: "#012FFF", + foundation: "#5800FF", + spec: "#00C6AC", + grants: "#84BD01", + "in-the-news": "#3F3A3D", +} diff --git a/src/components/blog-page/blog-tags.tsx b/src/components/blog-page/blog-tags.tsx new file mode 100644 index 0000000000..6c1a509535 --- /dev/null +++ b/src/components/blog-page/blog-tags.tsx @@ -0,0 +1,48 @@ +import NextLink from "next/link" + +import { Tag } from "@/app/conf/_design-system/tag" + +import { blogTagColors } from "./blog-tag-colors" +import clsx from "clsx" + +export function BlogTags({ + tags, + opaque, + className, + links, +}: { + tags: string[] + opaque?: boolean + className?: string + links?: boolean +}) { + return ( + + {tags.map(tag => { + const color = blogTagColors[tag] + if (!color && process.env.NODE_ENV !== "production") { + throw new Error(`No color found for tag: ${tag}`) + } + + const tagElement = ( + + {tag.replaceAll("-", " ")} + + ) + + return links ? ( + + {tagElement} + + ) : ( + tagElement + ) + })} + + ) +} diff --git a/src/components/blog-page/blur-bean.webp b/src/components/blog-page/blur-bean.webp new file mode 100644 index 0000000000..26bccab68d Binary files /dev/null and b/src/components/blog-page/blur-bean.webp differ diff --git a/src/components/blog-page/featured-blog-posts.tsx b/src/components/blog-page/featured-blog-posts.tsx new file mode 100644 index 0000000000..fc6c7f08cc --- /dev/null +++ b/src/components/blog-page/featured-blog-posts.tsx @@ -0,0 +1,61 @@ +import { clsx } from "clsx" +import NextLink from "next/link" + +import { BlogCard, BlogCardArrow, BlogCardFooterContent } from "./blog-card" +import { BlogCardPicture } from "./blog-card-picture" +import { BlogMdxContent } from "./mdx-types" +import { BlogTags } from "./blog-tags" + +export interface FeaturedBlogPostsProps + extends React.HTMLAttributes { + blogs: BlogMdxContent[] +} + +export function FeaturedBlogPosts({ + className, + blogs, + ...props +}: FeaturedBlogPostsProps) { + const [firstFeatured, ...nextThree] = blogs + .filter(blog => blog.frontMatter.featured) + .sort((a, b) => b.frontMatter.date.getTime() - a.frontMatter.date.getTime()) + .slice(0, 4) + + return ( +
+ + +
+ +
+ {firstFeatured.frontMatter.title} +
+
+ + +
+
+
+
+ {nextThree.map(post => ( + + ))} +
+
+ ) +} diff --git a/src/components/blog-page/index.tsx b/src/components/blog-page/index.tsx new file mode 100644 index 0000000000..920801bd56 --- /dev/null +++ b/src/components/blog-page/index.tsx @@ -0,0 +1,119 @@ +import NextLink from "next/link" + +import { Tag } from "@/app/conf/_design-system/tag" +import { arrowsMoveSideways } from "@/app/conf/_design-system/utils/arrows-move-sideways" + +import { blogTagColors } from "./blog-tag-colors" +import { BlogCard } from "./blog-card" +import { LookingForMore } from "./looking-for-more" +import { BlogMdxContent } from "./mdx-types" +import { FeaturedBlogPosts } from "./featured-blog-posts" +import { StripesDecoration } from "../../app/conf/_design-system/stripes-decoration" +import clsx from "clsx" + +const mask = `url(${new URL("./blur-bean.webp", import.meta.url).href})` + +export interface BlogPageProps { + tags: Record + blogs: BlogMdxContent[] + currentTag: string + hideFeaturedPosts?: boolean +} + +export function BlogPage({ + tags, + blogs, + currentTag, + hideFeaturedPosts, +}: BlogPageProps) { + return ( +
+
+ +
+

The GraphQL Blog

+

+ Insights, updates and best practices from across the GraphQL + community. Stay connected with the ideas and innovations shaping the + GraphQL ecosystem. +

+ {!hideFeaturedPosts && ( + + )} +
+
+ +
+
+
+

+ {currentTag || "All Posts"} +

+
+

Categories

+
    + {Object.entries(tags) + .sort((a, b) => b[1] - a[1]) + .map(([tag, count], i) => ( + + + {tag.replaceAll("-", " ")} ({count}) + + + ))} +
+
+
+
+ {blogs.map( + page => + (!currentTag || page.frontMatter.tags.includes(currentTag)) && ( + + ), + )} +
+
+ +
+
+ ) +} + +function Stripes({ className }: { className?: string }) { + return ( +
+ +
+ ) +} diff --git a/src/components/blog-page/looking-for-more.tsx b/src/components/blog-page/looking-for-more.tsx new file mode 100644 index 0000000000..2441ade4e7 --- /dev/null +++ b/src/components/blog-page/looking-for-more.tsx @@ -0,0 +1,36 @@ +import { Anchor } from "@/app/conf/_design-system/anchor" + +import ArrowDownIcon from "@/app/conf/_design-system/pixelarticons/arrow-down.svg?svgr" + +export function LookingForMore() { + return ( +
+
+
+

Looking for more?

+

+ Explore learning guides and best practices โ€” or browse for tools, + libraries and other resources. +

+
+
+ + Learn + + + + Resources + + +
+
+
+ ) +} diff --git a/src/components/blog-page/mdx-types.tsx b/src/components/blog-page/mdx-types.tsx new file mode 100644 index 0000000000..830ee9722d --- /dev/null +++ b/src/components/blog-page/mdx-types.tsx @@ -0,0 +1,10 @@ +export interface BlogMdxContent { + route: string + frontMatter: { + title: string + tags: string[] + byline: string + date: Date + featured: boolean + } +} diff --git a/src/components/index-page/data-colocation/index.tsx b/src/components/index-page/data-colocation/index.tsx index 1f54f51de2..620399412d 100644 --- a/src/components/index-page/data-colocation/index.tsx +++ b/src/components/index-page/data-colocation/index.tsx @@ -150,7 +150,7 @@ export function DataColocation() { return (
) } - -function arrowsMoveSideways(event: React.KeyboardEvent) { - if (event.key === "ArrowLeft" || event.key === "ArrowUp") { - const previousElement = event.currentTarget.previousElementSibling - if (previousElement) { - event.preventDefault() - ;(previousElement as HTMLElement).focus() - } - } else if (event.key === "ArrowRight" || event.key === "ArrowDown") { - const nextElement = event.currentTarget.nextElementSibling - if (nextElement) { - event.preventDefault() - ;(nextElement as HTMLElement).focus() - } - } -} diff --git a/src/pages/blog.mdx b/src/pages/blog.mdx index df349e7b40..71ca42719d 100644 --- a/src/pages/blog.mdx +++ b/src/pages/blog.mdx @@ -4,8 +4,9 @@ import { Tag, Card } from "@/components" import NextLink from "next/link" import { useRouter } from "next/router" import { clsx } from "clsx" +import { BlogPage } from "@/components/blog-page" -export default function Blog() { +export default function Blog({ hideFeaturedPosts = false }) { const { asPath } = useRouter() const items = getPagesUnderRoute("/blog").flatMap(item => item.children || item) const blogs = items.sort( @@ -22,68 +23,11 @@ export default function Blog() { acc[tag] += 1 return acc }, {}) - // - const tagList = ( -
- {Object.entries(tags) - .sort((a, b) => b[1] - a[1]) - .map(([tag, count]) => ( - - {tag.replaceAll("-", " ")} ({count}) - - ))} -
- ) - // - const blogList = blogs.map( - page => - (!currentTag || page.frontMatter.tags.includes(currentTag)) && ( - -
- {page.frontMatter.tags.map(tag => ( - {tag.replaceAll("-", " ")} - ))} -
-
- {page.frontMatter.title} -
-
- - - by {page.frontMatter.byline} -
- - Read more โ†’ - -
- ), - ) - return ( - <> -
-

Blog

-

Categories

- {tagList} -
-
- {blogList} -
- - ) + + return } diff --git a/src/pages/blog/2025-06-20-graphql-js-org.md b/src/pages/blog/2025-06-20-graphql-js-org.md index f989081ffa..a8983f3eaa 100644 --- a/src/pages/blog/2025-06-20-graphql-js-org.md +++ b/src/pages/blog/2025-06-20-graphql-js-org.md @@ -1,8 +1,9 @@ --- -title: ๐Ÿ†• Announcing graphql-js.org! +title: Announcing graphql-js.org! tags: ["announcements"] date: 2025-06-20 byline: GraphQL-js Maintainers +featured: true --- Dear Community, diff --git a/src/pages/blog/2025-09-04-multioption-inputs-with-oneof/index.mdx b/src/pages/blog/2025-09-04-multioption-inputs-with-oneof/index.mdx index ae7c552059..7c2728d576 100644 --- a/src/pages/blog/2025-09-04-multioption-inputs-with-oneof/index.mdx +++ b/src/pages/blog/2025-09-04-multioption-inputs-with-oneof/index.mdx @@ -3,6 +3,7 @@ title: Safer Multi-option Inputs with `@oneOf` tags: [announcements, spec] date: 2025-09-04 byline: Benjie Gillam +featured: true --- Weโ€™re excited to announce **[OneOf Input diff --git a/src/pages/blog/2025-09-08-announcing-graphql-ambassadors/index.md b/src/pages/blog/2025-09-08-announcing-graphql-ambassadors/index.md index 286a9e5157..7fc2074459 100644 --- a/src/pages/blog/2025-09-08-announcing-graphql-ambassadors/index.md +++ b/src/pages/blog/2025-09-08-announcing-graphql-ambassadors/index.md @@ -3,6 +3,7 @@ title: "Announcing Our GraphQL Ambassadors" tags: ["blog"] date: 2025-09-08 byline: Jem Gillam and Jory Burson +featured: true --- The GraphQL Foundation is thrilled to announce the launch of the GraphQL Ambassadors Program โ€” a new initiative to recognize and support community leaders who are helping to grow the GraphQL ecosystem worldwide. diff --git a/src/pages/blog/2025-09-08-september-edition.md b/src/pages/blog/2025-09-08-september-edition.md index f28e81a1d6..643ef6ddb4 100644 --- a/src/pages/blog/2025-09-08-september-edition.md +++ b/src/pages/blog/2025-09-08-september-edition.md @@ -3,6 +3,7 @@ title: "Announcing the September 2025 Edition of the GraphQL Specification" tags: ["spec"] date: 2025-09-08 byline: Lee Byron +featured: true --- Itโ€™s here: the [September 2025 edition of the GraphQL specification](https://spec.graphql.org/September2025/)! diff --git a/src/pages/blog/_meta.tsx b/src/pages/blog/_meta.tsx index 1ab5fb35c7..97b97c62d4 100644 --- a/src/pages/blog/_meta.tsx +++ b/src/pages/blog/_meta.tsx @@ -1,6 +1,12 @@ import { useConfig } from "nextra-theme-docs" import NextLink from "next/link" +import { Tag } from "../../app/conf/_design-system/tag" +import { blogTagColors } from "../../components/blog-page/blog-tag-colors" +import { BlogCardPicture } from "../../components/blog-page/blog-card-picture" +import { BlogMdxContent } from "../../components/blog-page/mdx-types" +import { BlogTags } from "../../components/blog-page/blog-tags" + export default { // only for blog posts inside folders we need to specify breadcrumb title "2024-06-11-announcing-new-graphql-website": "Announcing New GraphQL Website", @@ -12,32 +18,37 @@ export default { timestamp: true, layout: "default", topContent: function TopContent() { - const { frontMatter } = useConfig() + const frontMatter = useConfig() + .frontMatter as BlogMdxContent["frontMatter"] const { title, byline, tags } = frontMatter const date = new Date(frontMatter.date) + return ( <> -

{title}

-
-