diff --git a/next.config.js b/next.config.js
index 56da077fae..3a99e7137e 100644
--- a/next.config.js
+++ b/next.config.js
@@ -37,7 +37,7 @@ const ALLOWED_SVG_REGEX = new RegExp(`${sep}icons${sep}.+\\.svg$`)
const config = {
output: undefined,
// reactStrictMode: true, provoke duplicated codemirror editors
- webpack(config) {
+ webpack(config, { isServer, dev }) {
// #region MDX
const mdxRule = config.module.rules.find(rule => rule.test?.test?.(".mdx"))
if (mdxRule) {
@@ -62,6 +62,19 @@ const config = {
fileLoaderRule.exclude = /\.svg$/i
config.module.rules.push(
+ {
+ test: /\.(png|jpg|jpeg|gif|webp|avif|ico|bmp|svg|txt)$/i,
+ resourceQuery: /resource/,
+ type: "asset/resource",
+ generator: {
+ filename: "static/media/[name].[hash][ext]",
+ publicPath: "/_next/",
+ // Server build outputs to .next/server/, so go up to reach .next/static/
+ outputPath: [isServer && "../", !dev && "../"]
+ .filter(Boolean)
+ .join(""),
+ },
+ },
// All .svg from /icons/ and with ?svgr are going to be processed by @svgr/webpack
{
test: ALLOWED_SVG_REGEX,
@@ -103,7 +116,7 @@ const config = {
test: /\.svg$/i,
exclude: ALLOWED_SVG_REGEX,
resourceQuery: {
- not: [...fileLoaderRule.resourceQuery.not, /svgr/],
+ not: [...fileLoaderRule.resourceQuery.not, /svgr|resource/],
},
},
)
diff --git a/package.json b/package.json
index d3b9d07d11..1d3a6faff6 100644
--- a/package.json
+++ b/package.json
@@ -43,6 +43,7 @@
"@tailwindcss/container-queries": "^0.1.1",
"@tailwindcss/nesting": "0.0.0-insiders.565cd3e",
"@tailwindcss/typography": "^0.5.15",
+ "arktype": "2.1.28",
"autoprefixer": "^10.4.20",
"calendar-link": "^2.10.0",
"clsx": "^2.1.1",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 0df41ae641..ba6e6906f2 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -83,6 +83,9 @@ importers:
'@tailwindcss/typography':
specifier: ^0.5.15
version: 0.5.19(tailwindcss@3.4.18(tsx@4.21.0)(yaml@2.8.1))
+ arktype:
+ specifier: 2.1.28
+ version: 2.1.28
autoprefixer:
specifier: ^10.4.20
version: 10.4.22(postcss@8.5.6)
@@ -326,7 +329,7 @@ importers:
scripts/sync-working-groups:
dependencies:
arktype:
- specifier: ^2.1.27
+ specifier: 2.1.28
version: 2.1.28
packages:
diff --git a/scripts/get-github-info/github-stats.json b/scripts/get-github-info/github-stats.json
index 9f6b835af8..dae0f6bf92 100644
--- a/scripts/get-github-info/github-stats.json
+++ b/scripts/get-github-info/github-stats.json
@@ -1,15 +1,15 @@
{
"altair-graphql/altair": {
"hasCommitsInLast3Months": false,
- "stars": 5365,
+ "stars": 5372,
"formattedStars": "5k",
"license": "MIT License",
- "lastRelease": "2025-10-28T22:43:22Z",
- "formattedLastRelease": "1 month ago"
+ "lastRelease": "2025-12-12T08:48:15Z",
+ "formattedLastRelease": "12 hours ago"
},
"apache/apisix": {
"hasCommitsInLast3Months": false,
- "stars": 15907,
+ "stars": 15968,
"formattedStars": "16k",
"license": "Apache License 2.0",
"lastRelease": "2025-10-16T07:54:57Z",
@@ -17,27 +17,27 @@
},
"apollographql/apollo-studio-community": {
"hasCommitsInLast3Months": false,
- "stars": 261,
- "formattedStars": "261",
+ "stars": 260,
+ "formattedStars": "260",
"license": "Unknown",
"lastRelease": "",
"formattedLastRelease": ""
},
"ChilliCream/hotchocolate": {
"hasCommitsInLast3Months": false,
- "stars": 5637,
+ "stars": 5650,
"formattedStars": "6k",
"license": "MIT License",
- "lastRelease": "2025-11-26T11:12:44Z",
- "formattedLastRelease": "3 days ago"
+ "lastRelease": "2025-12-10T12:55:40Z",
+ "formattedLastRelease": "2 days ago"
},
"dgraph-io/dgraph": {
"hasCommitsInLast3Months": false,
- "stars": 21367,
+ "stars": 21392,
"formattedStars": "21k",
"license": "Apache License 2.0",
- "lastRelease": "2025-10-07T20:50:36Z",
- "formattedLastRelease": "1 month ago"
+ "lastRelease": "2025-12-12T00:45:30Z",
+ "formattedLastRelease": "20 hours ago"
},
"yahoo/elide": {
"hasCommitsInLast3Months": false,
@@ -45,7 +45,7 @@
"formattedStars": "1k",
"license": "Other",
"lastRelease": "2025-09-01T03:57:54Z",
- "formattedLastRelease": "2 months ago"
+ "formattedLastRelease": "3 months ago"
},
"graphapi-io/resources": {
"hasCommitsInLast3Months": false,
@@ -57,7 +57,7 @@
},
"hasura/graphql-engine": {
"hasCommitsInLast3Months": false,
- "stars": 31827,
+ "stars": 31835,
"formattedStars": "32k",
"license": "Apache License 2.0",
"lastRelease": "2025-10-14T15:20:38Z",
@@ -65,23 +65,23 @@
},
"graphql-hive/platform": {
"hasCommitsInLast3Months": false,
- "stars": 471,
- "formattedStars": "471",
+ "stars": 472,
+ "formattedStars": "472",
"license": "MIT License",
- "lastRelease": "2025-11-25T15:15:47Z",
- "formattedLastRelease": "3 days ago"
+ "lastRelease": "2025-12-11T15:59:16Z",
+ "formattedLastRelease": "1 day ago"
},
"Kong/insomnia": {
"hasCommitsInLast3Months": false,
- "stars": 37611,
+ "stars": 37667,
"formattedStars": "38k",
"license": "Apache License 2.0",
- "lastRelease": "2025-11-21T17:29:45Z",
- "formattedLastRelease": "1 week ago"
+ "lastRelease": "2025-12-12T19:24:57Z",
+ "formattedLastRelease": "1 hour ago"
},
"postmanlabs/postman-app-support": {
"hasCommitsInLast3Months": false,
- "stars": 5979,
+ "stars": 5982,
"formattedStars": "6k",
"license": "Unknown",
"lastRelease": "",
@@ -97,87 +97,39 @@
},
"TykTechnologies/tyk": {
"hasCommitsInLast3Months": false,
- "stars": 10515,
+ "stars": 10532,
"formattedStars": "11k",
"license": "Other",
"lastRelease": "2025-11-28T16:32:34Z",
- "formattedLastRelease": "19 hours ago"
+ "formattedLastRelease": "2 weeks ago"
},
"twinlogix/typetta": {
"hasCommitsInLast3Months": false,
- "stars": 115,
- "formattedStars": "115",
+ "stars": 116,
+ "formattedStars": "116",
"license": "Apache License 2.0",
"lastRelease": "2023-10-16T07:50:50Z",
"formattedLastRelease": "2 years ago"
},
"webiny/webiny-js": {
"hasCommitsInLast3Months": false,
- "stars": 7894,
+ "stars": 7900,
"formattedStars": "8k",
"license": "Other",
- "lastRelease": "2025-09-16T08:29:00Z",
- "formattedLastRelease": "2 months ago"
+ "lastRelease": "2025-12-09T11:36:01Z",
+ "formattedLastRelease": "3 days ago"
},
"ballerina-platform/module-ballerina-graphql": {
"hasCommitsInLast3Months": false,
"stars": 138,
"formattedStars": "138",
"license": "Apache License 2.0",
- "lastRelease": "2025-11-06T10:54:08Z",
- "formattedLastRelease": "3 weeks ago"
- },
- "oliyh/re-graph": {
- "hasCommitsInLast3Months": false,
- "stars": 464,
- "formattedStars": "464",
- "license": "Unknown",
- "lastRelease": "2022-07-20T09:24:02Z",
- "formattedLastRelease": "3 years ago"
- },
- "microsoft/cppgraphqlgen": {
- "hasCommitsInLast3Months": false,
- "stars": 343,
- "formattedStars": "343",
- "license": "MIT License",
- "lastRelease": "2024-12-10T17:25:31Z",
- "formattedLastRelease": "11 months ago"
- },
- "graphql/libgraphqlparser": {
- "hasCommitsInLast3Months": false,
- "stars": 1102,
- "formattedStars": "1k",
- "license": "MIT License",
- "lastRelease": "2017-10-16T21:47:42Z",
- "formattedLastRelease": "8 years ago"
- },
- "alumbra/alumbra": {
- "hasCommitsInLast3Months": false,
- "stars": 148,
- "formattedStars": "148",
- "license": "MIT License",
- "lastRelease": "2017-06-12T12:14:25Z",
- "formattedLastRelease": "8 years ago"
- },
- "tendant/graphql-clj": {
- "hasCommitsInLast3Months": false,
- "stars": 285,
- "formattedStars": "285",
- "license": "Eclipse Public License 1.0",
- "lastRelease": "",
- "formattedLastRelease": ""
- },
- "walmartlabs/lacinia": {
- "hasCommitsInLast3Months": false,
- "stars": 1843,
- "formattedStars": "2k",
- "license": "Other",
- "lastRelease": "",
- "formattedLastRelease": ""
+ "lastRelease": "2025-12-08T12:39:34Z",
+ "formattedLastRelease": "4 days ago"
},
"graphql-dotnet/graphql-client": {
"hasCommitsInLast3Months": false,
- "stars": 644,
+ "stars": 645,
"formattedStars": "1k",
"license": "MIT License",
"lastRelease": "2024-05-21T07:06:30Z",
@@ -197,7 +149,7 @@
"formattedStars": "8",
"license": "MIT License",
"lastRelease": "2025-11-14T07:39:17Z",
- "formattedLastRelease": "2 weeks ago"
+ "formattedLastRelease": "4 weeks ago"
},
"sahb1239/SAHB.GraphQLClient": {
"hasCommitsInLast3Months": false,
@@ -217,19 +169,19 @@
},
"EntityGraphQL/EntityGraphQL": {
"hasCommitsInLast3Months": false,
- "stars": 449,
- "formattedStars": "449",
+ "stars": 450,
+ "formattedStars": "450",
"license": "MIT License",
- "lastRelease": "2025-09-16T00:35:14Z",
- "formattedLastRelease": "2 months ago"
+ "lastRelease": "2025-12-01T22:09:40Z",
+ "formattedLastRelease": "1 week ago"
},
"graphql-dotnet/graphql-dotnet": {
"hasCommitsInLast3Months": false,
- "stars": 5975,
+ "stars": 5976,
"formattedStars": "6k",
"license": "MIT License",
"lastRelease": "2025-11-17T17:57:35Z",
- "formattedLastRelease": "1 week ago"
+ "formattedLastRelease": "3 weeks ago"
},
"chkimes/graphql-net": {
"hasCommitsInLast3Months": false,
@@ -247,37 +199,37 @@
"lastRelease": "",
"formattedLastRelease": ""
},
- "burner/graphqld": {
+ "microsoft/cppgraphqlgen": {
"hasCommitsInLast3Months": false,
- "stars": 35,
- "formattedStars": "35",
- "license": "GNU Lesser General Public License v3.0",
- "lastRelease": "2024-05-14T13:42:29Z",
+ "stars": 343,
+ "formattedStars": "343",
+ "license": "MIT License",
+ "lastRelease": "2024-12-10T17:25:31Z",
"formattedLastRelease": "1 year ago"
},
- "annkissam/common_graphql_client": {
+ "graphql/libgraphqlparser": {
"hasCommitsInLast3Months": false,
- "stars": 42,
- "formattedStars": "42",
+ "stars": 1102,
+ "formattedStars": "1k",
"license": "MIT License",
- "lastRelease": "2020-05-05T16:48:50Z",
- "formattedLastRelease": "5 years ago"
+ "lastRelease": "2017-10-16T21:47:42Z",
+ "formattedLastRelease": "8 years ago"
},
- "uesteibar/neuron": {
+ "burner/graphqld": {
"hasCommitsInLast3Months": false,
- "stars": 333,
- "formattedStars": "333",
- "license": "Other",
- "lastRelease": "",
- "formattedLastRelease": ""
+ "stars": 35,
+ "formattedStars": "35",
+ "license": "GNU Lesser General Public License v3.0",
+ "lastRelease": "2024-05-14T13:42:29Z",
+ "formattedLastRelease": "1 year ago"
},
"absinthe-graphql/absinthe": {
"hasCommitsInLast3Months": false,
- "stars": 4374,
+ "stars": 4379,
"formattedStars": "4k",
"license": "Other",
"lastRelease": "2025-11-21T15:08:24Z",
- "formattedLastRelease": "1 week ago"
+ "formattedLastRelease": "3 weeks ago"
},
"graphql-elixir/graphql": {
"hasCommitsInLast3Months": false,
@@ -295,6 +247,14 @@
"lastRelease": "",
"formattedLastRelease": ""
},
+ "oliyh/re-graph": {
+ "hasCommitsInLast3Months": false,
+ "stars": 464,
+ "formattedStars": "464",
+ "license": "Unknown",
+ "lastRelease": "2022-07-20T09:24:02Z",
+ "formattedLastRelease": "3 years ago"
+ },
"jlouis/graphql-erlang": {
"hasCommitsInLast3Months": false,
"stars": 314,
@@ -303,9 +263,33 @@
"lastRelease": "2018-06-22T12:35:43Z",
"formattedLastRelease": "7 years ago"
},
+ "alumbra/alumbra": {
+ "hasCommitsInLast3Months": false,
+ "stars": 148,
+ "formattedStars": "148",
+ "license": "MIT License",
+ "lastRelease": "2017-06-12T12:14:25Z",
+ "formattedLastRelease": "8 years ago"
+ },
+ "tendant/graphql-clj": {
+ "hasCommitsInLast3Months": false,
+ "stars": 285,
+ "formattedStars": "285",
+ "license": "Eclipse Public License 1.0",
+ "lastRelease": "",
+ "formattedLastRelease": ""
+ },
+ "walmartlabs/lacinia": {
+ "hasCommitsInLast3Months": false,
+ "stars": 1843,
+ "formattedStars": "2k",
+ "license": "Other",
+ "lastRelease": "",
+ "formattedLastRelease": ""
+ },
"gql-dart/ferry": {
"hasCommitsInLast3Months": false,
- "stars": 628,
+ "stars": 630,
"formattedStars": "1k",
"license": "MIT License",
"lastRelease": "",
@@ -319,9 +303,33 @@
"lastRelease": "2025-10-21T16:42:55Z",
"formattedLastRelease": "1 month ago"
},
+ "annkissam/common_graphql_client": {
+ "hasCommitsInLast3Months": false,
+ "stars": 42,
+ "formattedStars": "42",
+ "license": "MIT License",
+ "lastRelease": "2020-05-05T16:48:50Z",
+ "formattedLastRelease": "5 years ago"
+ },
+ "uesteibar/neuron": {
+ "hasCommitsInLast3Months": false,
+ "stars": 333,
+ "formattedStars": "333",
+ "license": "Other",
+ "lastRelease": "",
+ "formattedLastRelease": ""
+ },
+ "dosco/graphjin": {
+ "hasCommitsInLast3Months": false,
+ "stars": 3014,
+ "formattedStars": "3k",
+ "license": "Apache License 2.0",
+ "lastRelease": "2025-11-05T07:51:12Z",
+ "formattedLastRelease": "1 month ago"
+ },
"Khan/genqlient": {
"hasCommitsInLast3Months": false,
- "stars": 1266,
+ "stars": 1274,
"formattedStars": "1k",
"license": "MIT License",
"lastRelease": "2025-05-18T19:09:08Z",
@@ -329,15 +337,15 @@
},
"hasura/go-graphql-client": {
"hasCommitsInLast3Months": false,
- "stars": 459,
- "formattedStars": "459",
+ "stars": 461,
+ "formattedStars": "461",
"license": "MIT License",
"lastRelease": "2025-11-05T06:45:53Z",
- "formattedLastRelease": "3 weeks ago"
+ "formattedLastRelease": "1 month ago"
},
"shurcooL/graphql": {
"hasCommitsInLast3Months": false,
- "stars": 728,
+ "stars": 727,
"formattedStars": "1k",
"license": "MIT License",
"lastRelease": "",
@@ -353,11 +361,11 @@
},
"99designs/gqlgen": {
"hasCommitsInLast3Months": false,
- "stars": 10593,
+ "stars": 10607,
"formattedStars": "11k",
"license": "MIT License",
"lastRelease": "2025-11-24T16:56:20Z",
- "formattedLastRelease": "4 days ago"
+ "formattedLastRelease": "2 weeks ago"
},
"andrewwphillips/eggql": {
"hasCommitsInLast3Months": false,
@@ -377,15 +385,15 @@
},
"graph-gophers/graphql-go": {
"hasCommitsInLast3Months": false,
- "stars": 4741,
+ "stars": 4743,
"formattedStars": "5k",
"license": "BSD 2-Clause \"Simplified\" License",
"lastRelease": "2025-09-09T11:37:07Z",
- "formattedLastRelease": "2 months ago"
+ "formattedLastRelease": "3 months ago"
},
"graphql-go/graphql": {
"hasCommitsInLast3Months": false,
- "stars": 10132,
+ "stars": 10135,
"formattedStars": "10k",
"license": "MIT License",
"lastRelease": "2023-04-10T18:20:23Z",
@@ -409,43 +417,43 @@
},
"wundergraph/graphql-go-tools": {
"hasCommitsInLast3Months": false,
- "stars": 790,
+ "stars": 814,
"formattedStars": "1k",
"license": "MIT License",
- "lastRelease": "2025-11-21T21:18:20Z",
- "formattedLastRelease": "1 week ago"
+ "lastRelease": "2025-12-10T11:25:46Z",
+ "formattedLastRelease": "2 days ago"
},
- "dosco/graphjin": {
+ "morpheusgraphql/morpheus-graphql": {
"hasCommitsInLast3Months": false,
- "stars": 3011,
- "formattedStars": "3k",
- "license": "Apache License 2.0",
- "lastRelease": "2025-11-05T07:51:12Z",
- "formattedLastRelease": "3 weeks ago"
+ "stars": 418,
+ "formattedStars": "418",
+ "license": "MIT License",
+ "lastRelease": "2024-06-10T08:34:35Z",
+ "formattedLastRelease": "1 year ago"
},
- "grails/gorm-graphql": {
+ "apollographql/apollo-kotlin": {
"hasCommitsInLast3Months": false,
- "stars": 81,
- "formattedStars": "81",
- "license": "Unknown",
- "lastRelease": "2023-12-08T10:48:05Z",
- "formattedLastRelease": "1 year ago"
+ "stars": 3930,
+ "formattedStars": "4k",
+ "license": "MIT License",
+ "lastRelease": "2025-11-13T17:33:51Z",
+ "formattedLastRelease": "4 weeks ago"
},
- "grooviter/gql": {
+ "ExpediaGroup/graphql-kotlin": {
"hasCommitsInLast3Months": false,
- "stars": 49,
- "formattedStars": "49",
+ "stars": 1796,
+ "formattedStars": "2k",
"license": "Apache License 2.0",
- "lastRelease": "2024-11-05T10:13:23Z",
- "formattedLastRelease": "1 year ago"
+ "lastRelease": "2025-06-16T17:02:18Z",
+ "formattedLastRelease": "5 months ago"
},
- "morpheusgraphql/morpheus-graphql": {
+ "americanexpress/nodes": {
"hasCommitsInLast3Months": false,
- "stars": 418,
- "formattedStars": "418",
- "license": "MIT License",
- "lastRelease": "2024-06-10T08:34:35Z",
- "formattedLastRelease": "1 year ago"
+ "stars": 307,
+ "formattedStars": "307",
+ "license": "Apache License 2.0",
+ "lastRelease": "2019-07-13T22:47:01Z",
+ "formattedLastRelease": "6 years ago"
},
"jasonsychau/graphql-w-persistent": {
"hasCommitsInLast3Months": false,
@@ -463,47 +471,135 @@
"lastRelease": "2021-01-11T11:19:38Z",
"formattedLastRelease": "4 years ago"
},
- "apollographql/apollo-client": {
+ "grails/gorm-graphql": {
"hasCommitsInLast3Months": false,
- "stars": 19673,
- "formattedStars": "20k",
- "license": "MIT License",
- "lastRelease": "2025-11-19T01:20:25Z",
- "formattedLastRelease": "1 week ago"
+ "stars": 81,
+ "formattedStars": "81",
+ "license": "Unknown",
+ "lastRelease": "2023-12-08T10:48:05Z",
+ "formattedLastRelease": "2 years ago"
},
- "aws-amplify/amplify-js": {
+ "grooviter/gql": {
"hasCommitsInLast3Months": false,
- "stars": 9567,
- "formattedStars": "10k",
+ "stars": 49,
+ "formattedStars": "49",
"license": "Apache License 2.0",
- "lastRelease": "2025-11-06T13:36:19Z",
- "formattedLastRelease": "3 weeks ago"
+ "lastRelease": "2024-11-05T10:13:23Z",
+ "formattedLastRelease": "1 year ago"
},
- "Houfeng/gq-loader": {
+ "graphql-java-generator/graphql-gradle-plugin-project": {
"hasCommitsInLast3Months": false,
- "stars": 59,
- "formattedStars": "59",
- "license": "Unknown",
+ "stars": 57,
+ "formattedStars": "57",
+ "license": "MIT License",
"lastRelease": "",
"formattedLastRelease": ""
},
- "gqty-dev/gqty": {
+ "graphql-calculator/graphql-calculator": {
"hasCommitsInLast3Months": false,
- "stars": 1030,
- "formattedStars": "1k",
- "license": "MIT License",
- "lastRelease": "2025-10-26T19:29:38Z",
- "formattedLastRelease": "1 month ago"
+ "stars": 112,
+ "formattedStars": "112",
+ "license": "Apache License 2.0",
+ "lastRelease": "2021-09-03T01:56:25Z",
+ "formattedLastRelease": "4 years ago"
},
- "grafoojs/grafoo": {
+ "graphql-java-kickstart/graphql-spring-boot": {
"hasCommitsInLast3Months": false,
- "stars": 274,
- "formattedStars": "274",
+ "stars": 1513,
+ "formattedStars": "2k",
"license": "MIT License",
- "lastRelease": "2018-06-20T15:21:00Z",
- "formattedLastRelease": "7 years ago"
+ "lastRelease": "2023-12-07T11:07:47Z",
+ "formattedLastRelease": "2 years ago"
},
- "badbatch/graphql-box": {
+ "graphql-java/graphql-java": {
+ "hasCommitsInLast3Months": false,
+ "stars": 6227,
+ "formattedStars": "6k",
+ "license": "MIT License",
+ "lastRelease": "2025-11-10T01:21:35Z",
+ "formattedLastRelease": "1 month ago"
+ },
+ "babyfish-ct/jimmer": {
+ "hasCommitsInLast3Months": false,
+ "stars": 1581,
+ "formattedStars": "2k",
+ "license": "Apache License 2.0",
+ "lastRelease": "2025-11-26T13:05:27Z",
+ "formattedLastRelease": "2 weeks ago"
+ },
+ "aPureBase/KGraphQL": {
+ "hasCommitsInLast3Months": false,
+ "stars": 308,
+ "formattedStars": "308",
+ "license": "MIT License",
+ "lastRelease": "2023-01-27T10:09:55Z",
+ "formattedLastRelease": "2 years ago"
+ },
+ "eclipse/microprofile-graphql": {
+ "hasCommitsInLast3Months": false,
+ "stars": 101,
+ "formattedStars": "101",
+ "license": "Apache License 2.0",
+ "lastRelease": "2022-03-21T18:26:51Z",
+ "formattedLastRelease": "3 years ago"
+ },
+ "netflix/dgs-framework": {
+ "hasCommitsInLast3Months": false,
+ "stars": 3281,
+ "formattedStars": "3k",
+ "license": "Apache License 2.0",
+ "lastRelease": "2025-12-05T21:45:25Z",
+ "formattedLastRelease": "6 days ago"
+ },
+ "spring-projects/spring-graphql": {
+ "hasCommitsInLast3Months": false,
+ "stars": 1580,
+ "formattedStars": "2k",
+ "license": "Apache License 2.0",
+ "lastRelease": "2025-11-18T10:05:26Z",
+ "formattedLastRelease": "3 weeks ago"
+ },
+ "apollographql/apollo-client": {
+ "hasCommitsInLast3Months": false,
+ "stars": 19681,
+ "formattedStars": "20k",
+ "license": "MIT License",
+ "lastRelease": "2025-12-10T08:08:29Z",
+ "formattedLastRelease": "2 days ago"
+ },
+ "aws-amplify/amplify-js": {
+ "hasCommitsInLast3Months": false,
+ "stars": 9570,
+ "formattedStars": "10k",
+ "license": "Apache License 2.0",
+ "lastRelease": "2025-12-10T14:00:23Z",
+ "formattedLastRelease": "2 days ago"
+ },
+ "Houfeng/gq-loader": {
+ "hasCommitsInLast3Months": false,
+ "stars": 59,
+ "formattedStars": "59",
+ "license": "Unknown",
+ "lastRelease": "",
+ "formattedLastRelease": ""
+ },
+ "gqty-dev/gqty": {
+ "hasCommitsInLast3Months": false,
+ "stars": 1031,
+ "formattedStars": "1k",
+ "license": "MIT License",
+ "lastRelease": "2025-10-26T19:29:38Z",
+ "formattedLastRelease": "1 month ago"
+ },
+ "grafoojs/grafoo": {
+ "hasCommitsInLast3Months": false,
+ "stars": 274,
+ "formattedStars": "274",
+ "license": "MIT License",
+ "lastRelease": "2018-06-20T15:21:00Z",
+ "formattedLastRelease": "7 years ago"
+ },
+ "badbatch/graphql-box": {
"hasCommitsInLast3Months": false,
"stars": 27,
"formattedStars": "27",
@@ -513,32 +609,32 @@
},
"nearform/graphql-hooks": {
"hasCommitsInLast3Months": false,
- "stars": 1889,
+ "stars": 1888,
"formattedStars": "2k",
"license": "Other",
"lastRelease": "2025-01-08T18:45:52Z",
- "formattedLastRelease": "10 months ago"
+ "formattedLastRelease": "11 months ago"
},
"graphql/graphql-http": {
"hasCommitsInLast3Months": false,
- "stars": 357,
- "formattedStars": "357",
+ "stars": 359,
+ "formattedStars": "359",
"license": "MIT License",
"lastRelease": "2025-01-17T14:16:52Z",
"formattedLastRelease": "10 months ago"
},
"jasonkuhrt/graphql-request": {
"hasCommitsInLast3Months": false,
- "stars": 6081,
+ "stars": 6086,
"formattedStars": "6k",
"license": "MIT License",
- "lastRelease": "2025-11-25T16:55:56Z",
- "formattedLastRelease": "3 days ago"
+ "lastRelease": "2025-12-12T15:51:04Z",
+ "formattedLastRelease": "4 hours ago"
},
"enisdenjo/graphql-sse": {
"hasCommitsInLast3Months": false,
- "stars": 435,
- "formattedStars": "435",
+ "stars": 438,
+ "formattedStars": "438",
"license": "MIT License",
"lastRelease": "2025-10-22T16:19:40Z",
"formattedLastRelease": "1 month ago"
@@ -553,7 +649,7 @@
},
"enisdenjo/graphql-ws": {
"hasCommitsInLast3Months": false,
- "stars": 1843,
+ "stars": 1846,
"formattedStars": "2k",
"license": "MIT License",
"lastRelease": "2025-07-14T12:15:37Z",
@@ -585,59 +681,43 @@
},
"facebook/relay": {
"hasCommitsInLast3Months": false,
- "stars": 18893,
+ "stars": 18900,
"formattedStars": "19k",
"license": "MIT License",
"lastRelease": "2025-08-06T23:45:00Z",
- "formattedLastRelease": "3 months ago"
+ "formattedLastRelease": "4 months ago"
},
"FormidableLabs/urql": {
"hasCommitsInLast3Months": false,
- "stars": 8900,
+ "stars": 8905,
"formattedStars": "9k",
"license": "MIT License",
"lastRelease": "2025-08-29T08:06:41Z",
"formattedLastRelease": "3 months ago"
},
- "apollographql/apollo-server": {
- "hasCommitsInLast3Months": false,
- "stars": 13926,
- "formattedStars": "14k",
- "license": "MIT License",
- "lastRelease": "2025-11-21T23:19:03Z",
- "formattedLastRelease": "1 week ago"
- },
- "graphql/graphql-js": {
+ "neomatrixcode/Diana.jl": {
"hasCommitsInLast3Months": false,
- "stars": 20279,
- "formattedStars": "20k",
+ "stars": 117,
+ "formattedStars": "117",
"license": "MIT License",
- "lastRelease": "2025-11-01T14:18:53Z",
- "formattedLastRelease": "3 weeks ago"
+ "lastRelease": "2022-08-16T03:22:22Z",
+ "formattedLastRelease": "3 years ago"
},
- "dotansimha/graphql-yoga": {
+ "DeloitteDigitalAPAC/GraphQLClient.jl": {
"hasCommitsInLast3Months": false,
- "stars": 8455,
- "formattedStars": "8k",
- "license": "MIT License",
- "lastRelease": "2025-11-28T11:05:21Z",
- "formattedLastRelease": "1 day ago"
+ "stars": 47,
+ "formattedStars": "47",
+ "license": "Other",
+ "lastRelease": "2022-10-26T16:48:16Z",
+ "formattedLastRelease": "3 years ago"
},
- "mercurius-js/mercurius": {
+ "andreas/ocaml-graphql-server": {
"hasCommitsInLast3Months": false,
- "stars": 2462,
- "formattedStars": "2k",
+ "stars": 621,
+ "formattedStars": "1k",
"license": "MIT License",
- "lastRelease": "2025-11-13T14:12:18Z",
- "formattedLastRelease": "2 weeks ago"
- },
- "getcronit/pylon": {
- "hasCommitsInLast3Months": false,
- "stars": 345,
- "formattedStars": "345",
- "license": "Apache License 2.0",
- "lastRelease": "2025-10-01T08:35:15Z",
- "formattedLastRelease": "1 month ago"
+ "lastRelease": "2022-07-08T16:26:45Z",
+ "formattedLastRelease": "3 years ago"
},
"networkimprov/brangr": {
"hasCommitsInLast3Months": false,
@@ -649,19 +729,19 @@
},
"hayes/giraphql": {
"hasCommitsInLast3Months": false,
- "stars": 2553,
+ "stars": 2561,
"formattedStars": "3k",
"license": "ISC License",
- "lastRelease": "2025-11-10T01:29:18Z",
- "formattedLastRelease": "2 weeks ago"
+ "lastRelease": "2025-12-10T22:07:12Z",
+ "formattedLastRelease": "1 day ago"
},
"graphql/graphiql": {
"hasCommitsInLast3Months": false,
- "stars": 16715,
+ "stars": 16730,
"formattedStars": "17k",
"license": "MIT License",
- "lastRelease": "2025-11-01T22:30:04Z",
- "formattedLastRelease": "3 weeks ago"
+ "lastRelease": "2025-11-30T09:04:01Z",
+ "formattedLastRelease": "1 week ago"
},
"Urigo/graphql-cli": {
"hasCommitsInLast3Months": false,
@@ -673,11 +753,11 @@
},
"dotansimha/graphql-code-generator": {
"hasCommitsInLast3Months": false,
- "stars": 11179,
+ "stars": 11183,
"formattedStars": "11k",
"license": "MIT License",
"lastRelease": "2025-11-29T04:07:19Z",
- "formattedLastRelease": "7 hours ago"
+ "formattedLastRelease": "1 week ago"
},
"kamilkisiela/graphql-config": {
"hasCommitsInLast3Months": false,
@@ -689,7 +769,7 @@
},
"dimaMachina/graphql-eslint/": {
"hasCommitsInLast3Months": false,
- "stars": 831,
+ "stars": 832,
"formattedStars": "1k",
"license": "MIT License",
"lastRelease": "2025-03-26T14:11:23Z",
@@ -700,8 +780,8 @@
"stars": 1725,
"formattedStars": "2k",
"license": "MIT License",
- "lastRelease": "2025-11-15T02:42:13Z",
- "formattedLastRelease": "2 weeks ago"
+ "lastRelease": "2025-12-10T22:16:19Z",
+ "formattedLastRelease": "1 day ago"
},
"graphql/graphql-language-service": {
"hasCommitsInLast3Months": false,
@@ -713,18 +793,18 @@
},
"n1ru4l/graphql-live-query": {
"hasCommitsInLast3Months": false,
- "stars": 440,
- "formattedStars": "440",
+ "stars": 441,
+ "formattedStars": "441",
"license": "MIT License",
"lastRelease": "2022-07-29T09:27:53Z",
"formattedLastRelease": "3 years ago"
},
"Urigo/graphql-mesh": {
"hasCommitsInLast3Months": false,
- "stars": 3463,
+ "stars": 3465,
"formattedStars": "3k",
"license": "MIT License",
- "lastRelease": "2025-11-19T12:12:00Z",
+ "lastRelease": "2025-12-04T22:32:32Z",
"formattedLastRelease": "1 week ago"
},
"maticzav/graphql-middleware": {
@@ -745,7 +825,7 @@
},
"Urigo/graphql-scalars": {
"hasCommitsInLast3Months": false,
- "stars": 1927,
+ "stars": 1929,
"formattedStars": "2k",
"license": "MIT License",
"lastRelease": "2025-10-14T23:00:24Z",
@@ -753,7 +833,7 @@
},
"maticzav/graphql-shield": {
"hasCommitsInLast3Months": false,
- "stars": 3574,
+ "stars": 3573,
"formattedStars": "4k",
"license": "MIT License",
"lastRelease": "2022-11-22T19:08:37Z",
@@ -761,11 +841,11 @@
},
"ardatan/graphql-tools": {
"hasCommitsInLast3Months": false,
- "stars": 5418,
+ "stars": 5416,
"formattedStars": "5k",
"license": "MIT License",
"lastRelease": "2025-11-28T10:05:14Z",
- "formattedLastRelease": "1 day ago"
+ "formattedLastRelease": "2 weeks ago"
},
"anvilco/graphql-introspection-tools": {
"hasCommitsInLast3Months": false,
@@ -777,7 +857,7 @@
},
"graphile/postgraphile": {
"hasCommitsInLast3Months": false,
- "stars": 12858,
+ "stars": 12866,
"formattedStars": "13k",
"license": "Other",
"lastRelease": "2023-10-05T16:27:00Z",
@@ -785,7 +865,7 @@
},
"Urigo/SOFA": {
"hasCommitsInLast3Months": false,
- "stars": 1111,
+ "stars": 1112,
"formattedStars": "1k",
"license": "MIT License",
"lastRelease": "2024-12-16T10:06:41Z",
@@ -793,59 +873,131 @@
},
"anvilco/spectaql": {
"hasCommitsInLast3Months": false,
- "stars": 1205,
+ "stars": 1207,
"formattedStars": "1k",
"license": "MIT License",
"lastRelease": "",
"formattedLastRelease": ""
},
- "neomatrixcode/Diana.jl": {
+ "apollographql/apollo-server": {
"hasCommitsInLast3Months": false,
- "stars": 117,
- "formattedStars": "117",
+ "stars": 13930,
+ "formattedStars": "14k",
"license": "MIT License",
- "lastRelease": "2022-08-16T03:22:22Z",
- "formattedLastRelease": "3 years ago"
+ "lastRelease": "2025-11-21T23:19:03Z",
+ "formattedLastRelease": "2 weeks ago"
},
- "DeloitteDigitalAPAC/GraphQLClient.jl": {
+ "graphql/graphql-js": {
"hasCommitsInLast3Months": false,
- "stars": 47,
- "formattedStars": "47",
- "license": "Other",
- "lastRelease": "2022-10-26T16:48:16Z",
- "formattedLastRelease": "3 years ago"
+ "stars": 20290,
+ "formattedStars": "20k",
+ "license": "MIT License",
+ "lastRelease": "2025-11-01T14:18:53Z",
+ "formattedLastRelease": "1 month ago"
},
- "andreas/ocaml-graphql-server": {
+ "dotansimha/graphql-yoga": {
"hasCommitsInLast3Months": false,
- "stars": 621,
- "formattedStars": "1k",
+ "stars": 8460,
+ "formattedStars": "8k",
"license": "MIT License",
- "lastRelease": "2022-07-08T16:26:45Z",
- "formattedLastRelease": "3 years ago"
+ "lastRelease": "2025-12-02T00:13:11Z",
+ "formattedLastRelease": "1 week ago"
+ },
+ "mercurius-js/mercurius": {
+ "hasCommitsInLast3Months": false,
+ "stars": 2464,
+ "formattedStars": "2k",
+ "license": "MIT License",
+ "lastRelease": "2025-11-13T14:12:18Z",
+ "formattedLastRelease": "4 weeks ago"
+ },
+ "getcronit/pylon": {
+ "hasCommitsInLast3Months": false,
+ "stars": 348,
+ "formattedStars": "348",
+ "license": "Apache License 2.0",
+ "lastRelease": "2025-10-01T08:35:15Z",
+ "formattedLastRelease": "2 months ago"
},
"graphql-perl/graphql-perl": {
"hasCommitsInLast3Months": false,
- "stars": 73,
- "formattedStars": "73",
+ "stars": 72,
+ "formattedStars": "72",
+ "license": "Unknown",
+ "lastRelease": "",
+ "formattedLastRelease": ""
+ },
+ "mirumee/ariadne-codegen": {
+ "hasCommitsInLast3Months": false,
+ "stars": 373,
+ "formattedStars": "373",
+ "license": "BSD 3-Clause \"New\" or \"Revised\" License",
+ "lastRelease": "2025-12-06T19:39:02Z",
+ "formattedLastRelease": "6 days ago"
+ },
+ "graphql-python/gql": {
+ "hasCommitsInLast3Months": false,
+ "stars": 1654,
+ "formattedStars": "2k",
+ "license": "MIT License",
+ "lastRelease": "2025-09-05T14:22:54Z",
+ "formattedLastRelease": "3 months ago"
+ },
+ "denisart/graphql-query": {
+ "hasCommitsInLast3Months": false,
+ "stars": 66,
+ "formattedStars": "66",
+ "license": "MIT License",
+ "lastRelease": "2024-07-31T10:54:53Z",
+ "formattedLastRelease": "1 year ago"
+ },
+ "prisma-labs/python-graphql-client": {
+ "hasCommitsInLast3Months": false,
+ "stars": 156,
+ "formattedStars": "156",
+ "license": "MIT License",
+ "lastRelease": "",
+ "formattedLastRelease": ""
+ },
+ "dsal3389/ql": {
+ "hasCommitsInLast3Months": false,
+ "stars": 9,
+ "formattedStars": "9",
"license": "Unknown",
+ "lastRelease": "2025-02-04T17:36:51Z",
+ "formattedLastRelease": "10 months ago"
+ },
+ "qlient-org/python-qlient": {
+ "hasCommitsInLast3Months": false,
+ "stars": 46,
+ "formattedStars": "46",
+ "license": "MIT License",
+ "lastRelease": "2022-07-29T16:10:08Z",
+ "formattedLastRelease": "3 years ago"
+ },
+ "profusion/sgqlc": {
+ "hasCommitsInLast3Months": false,
+ "stars": 547,
+ "formattedStars": "1k",
+ "license": "ISC License",
"lastRelease": "",
"formattedLastRelease": ""
},
"api-platform/api-platform": {
"hasCommitsInLast3Months": false,
- "stars": 9054,
+ "stars": 9066,
"formattedStars": "9k",
"license": "MIT License",
"lastRelease": "2025-03-11T16:15:41Z",
- "formattedLastRelease": "8 months ago"
+ "formattedLastRelease": "9 months ago"
},
"GatoGraphQL/GatoGraphQL": {
"hasCommitsInLast3Months": false,
- "stars": 376,
- "formattedStars": "376",
+ "stars": 377,
+ "formattedStars": "377",
"license": "GNU General Public License v2.0",
"lastRelease": "2025-11-26T08:29:30Z",
- "formattedLastRelease": "3 days ago"
+ "formattedLastRelease": "2 weeks ago"
},
"infinityloop-dev/graphpinator": {
"hasCommitsInLast3Months": false,
@@ -861,15 +1013,15 @@
"formattedStars": "16",
"license": "MIT License",
"lastRelease": "2025-10-11T09:19:14Z",
- "formattedLastRelease": "1 month ago"
+ "formattedLastRelease": "2 months ago"
},
"webonyx/graphql-php": {
"hasCommitsInLast3Months": false,
- "stars": 4702,
+ "stars": 4706,
"formattedStars": "5k",
"license": "MIT License",
- "lastRelease": "2025-11-20T11:51:16Z",
- "formattedLastRelease": "1 week ago"
+ "lastRelease": "2025-12-09T07:31:19Z",
+ "formattedLastRelease": "3 days ago"
},
"ivome/graphql-relay-php": {
"hasCommitsInLast3Months": false,
@@ -885,7 +1037,7 @@
"formattedStars": "1k",
"license": "MIT License",
"lastRelease": "2025-10-31T08:00:22Z",
- "formattedLastRelease": "4 weeks ago"
+ "formattedLastRelease": "1 month ago"
},
"thecodingmachine/graphqlite": {
"hasCommitsInLast3Months": false,
@@ -893,15 +1045,15 @@
"formattedStars": "1k",
"license": "MIT License",
"lastRelease": "2025-09-04T16:39:26Z",
- "formattedLastRelease": "2 months ago"
+ "formattedLastRelease": "3 months ago"
},
"nuwave/lighthouse": {
"hasCommitsInLast3Months": false,
- "stars": 3465,
+ "stars": 3471,
"formattedStars": "3k",
"license": "MIT License",
- "lastRelease": "2025-09-11T08:07:50Z",
- "formattedLastRelease": "2 months ago"
+ "lastRelease": "2025-12-05T08:19:37Z",
+ "formattedLastRelease": "1 week ago"
},
"railt/railt": {
"hasCommitsInLast3Months": false,
@@ -929,71 +1081,63 @@
},
"wp-graphql/wp-graphql": {
"hasCommitsInLast3Months": false,
- "stars": 3759,
+ "stars": 3758,
"formattedStars": "4k",
"license": "GNU General Public License v3.0",
"lastRelease": "2025-11-24T22:39:55Z",
- "formattedLastRelease": "4 days ago"
+ "formattedLastRelease": "2 weeks ago"
},
- "mirumee/ariadne-codegen": {
+ "ropensci/ghql": {
"hasCommitsInLast3Months": false,
- "stars": 368,
- "formattedStars": "368",
- "license": "BSD 3-Clause \"New\" or \"Revised\" License",
- "lastRelease": "2025-10-13T06:38:02Z",
- "formattedLastRelease": "1 month ago"
+ "stars": 149,
+ "formattedStars": "149",
+ "license": "Other",
+ "lastRelease": "2025-09-08T08:41:00Z",
+ "formattedLastRelease": "3 months ago"
},
- "graphql-python/gql": {
+ "ghostdogpr/caliban": {
"hasCommitsInLast3Months": false,
- "stars": 1653,
- "formattedStars": "2k",
- "license": "MIT License",
- "lastRelease": "2025-09-05T14:22:54Z",
- "formattedLastRelease": "2 months ago"
+ "stars": 977,
+ "formattedStars": "1k",
+ "license": "Apache License 2.0",
+ "lastRelease": "2025-07-14T00:24:20Z",
+ "formattedLastRelease": "4 months ago"
},
- "denisart/graphql-query": {
+ "ohler55/agoo": {
"hasCommitsInLast3Months": false,
- "stars": 66,
- "formattedStars": "66",
+ "stars": 924,
+ "formattedStars": "1k",
"license": "MIT License",
- "lastRelease": "2024-07-31T10:54:53Z",
- "formattedLastRelease": "1 year ago"
+ "lastRelease": "2025-09-24T22:20:23Z",
+ "formattedLastRelease": "2 months ago"
},
- "prisma-labs/python-graphql-client": {
+ "rmosolgo/graphql-ruby": {
"hasCommitsInLast3Months": false,
- "stars": 156,
- "formattedStars": "156",
+ "stars": 5427,
+ "formattedStars": "5k",
"license": "MIT License",
- "lastRelease": "",
- "formattedLastRelease": ""
- },
- "dsal3389/ql": {
- "hasCommitsInLast3Months": false,
- "stars": 9,
- "formattedStars": "9",
- "license": "Unknown",
- "lastRelease": "2025-02-04T17:36:51Z",
- "formattedLastRelease": "9 months ago"
+ "lastRelease": "2025-07-19T17:15:49Z",
+ "formattedLastRelease": "4 months ago"
},
- "qlient-org/python-qlient": {
+ "virtualshield/rails-graphql": {
"hasCommitsInLast3Months": false,
- "stars": 46,
- "formattedStars": "46",
+ "stars": 187,
+ "formattedStars": "187",
"license": "MIT License",
- "lastRelease": "2022-07-29T16:10:08Z",
- "formattedLastRelease": "3 years ago"
+ "lastRelease": "2025-08-25T17:53:38Z",
+ "formattedLastRelease": "3 months ago"
},
- "profusion/sgqlc": {
+ "sangria-graphql/sangria": {
"hasCommitsInLast3Months": false,
- "stars": 546,
- "formattedStars": "1k",
- "license": "ISC License",
- "lastRelease": "",
- "formattedLastRelease": ""
+ "stars": 1957,
+ "formattedStars": "2k",
+ "license": "Apache License 2.0",
+ "lastRelease": "2025-10-20T11:40:30Z",
+ "formattedLastRelease": "1 month ago"
},
"mirumee/ariadne": {
"hasCommitsInLast3Months": false,
- "stars": 2309,
+ "stars": 2310,
"formattedStars": "2k",
"license": "BSD 3-Clause \"New\" or \"Revised\" License",
"lastRelease": "2025-04-18T08:27:47Z",
@@ -1017,7 +1161,7 @@
},
"graphql-python/graphene": {
"hasCommitsInLast3Months": false,
- "stars": 8239,
+ "stars": 8240,
"formattedStars": "8k",
"license": "MIT License",
"lastRelease": "2024-11-09T20:43:58Z",
@@ -1025,203 +1169,131 @@
},
"strawberry-graphql/strawberry": {
"hasCommitsInLast3Months": false,
- "stars": 4569,
+ "stars": 4571,
"formattedStars": "5k",
"license": "MIT License",
- "lastRelease": "2025-11-22T13:00:06Z",
- "formattedLastRelease": "6 days ago"
+ "lastRelease": "2025-12-12T11:49:36Z",
+ "formattedLastRelease": "9 hours ago"
},
"tartiflette/tartiflette": {
"hasCommitsInLast3Months": false,
- "stars": 856,
+ "stars": 854,
"formattedStars": "1k",
"license": "MIT License",
"lastRelease": "2021-11-15T11:05:03Z",
"formattedLastRelease": "4 years ago"
},
- "apollographql/apollo-kotlin": {
- "hasCommitsInLast3Months": false,
- "stars": 3929,
- "formattedStars": "4k",
- "license": "MIT License",
- "lastRelease": "2025-11-13T17:33:51Z",
- "formattedLastRelease": "2 weeks ago"
- },
- "ExpediaGroup/graphql-kotlin": {
- "hasCommitsInLast3Months": false,
- "stars": 1795,
- "formattedStars": "2k",
- "license": "Apache License 2.0",
- "lastRelease": "2025-06-16T17:02:18Z",
- "formattedLastRelease": "5 months ago"
- },
- "americanexpress/nodes": {
- "hasCommitsInLast3Months": false,
- "stars": 307,
- "formattedStars": "307",
- "license": "Apache License 2.0",
- "lastRelease": "2019-07-13T22:47:01Z",
- "formattedLastRelease": "6 years ago"
- },
- "graphql-calculator/graphql-calculator": {
- "hasCommitsInLast3Months": false,
- "stars": 112,
- "formattedStars": "112",
- "license": "Apache License 2.0",
- "lastRelease": "2021-09-03T01:56:25Z",
- "formattedLastRelease": "4 years ago"
- },
- "graphql-java-kickstart/graphql-spring-boot": {
- "hasCommitsInLast3Months": false,
- "stars": 1513,
- "formattedStars": "2k",
- "license": "MIT License",
- "lastRelease": "2023-12-07T11:07:47Z",
- "formattedLastRelease": "1 year ago"
- },
- "graphql-java/graphql-java": {
- "hasCommitsInLast3Months": false,
- "stars": 6226,
- "formattedStars": "6k",
- "license": "MIT License",
- "lastRelease": "2025-11-10T01:21:35Z",
- "formattedLastRelease": "2 weeks ago"
- },
- "babyfish-ct/jimmer": {
+ "obmarg/cynic": {
"hasCommitsInLast3Months": false,
- "stars": 1565,
- "formattedStars": "2k",
- "license": "Apache License 2.0",
- "lastRelease": "2025-11-26T13:05:27Z",
- "formattedLastRelease": "2 days ago"
+ "stars": 443,
+ "formattedStars": "443",
+ "license": "Mozilla Public License 2.0",
+ "lastRelease": "2025-08-19T19:37:22Z",
+ "formattedLastRelease": "3 months ago"
},
- "aPureBase/KGraphQL": {
+ "arthurkhlghatyan/gql-client-rs": {
"hasCommitsInLast3Months": false,
- "stars": 308,
- "formattedStars": "308",
+ "stars": 51,
+ "formattedStars": "51",
"license": "MIT License",
- "lastRelease": "2023-01-27T10:09:55Z",
- "formattedLastRelease": "2 years ago"
+ "lastRelease": "2025-06-07T14:31:10Z",
+ "formattedLastRelease": "6 months ago"
},
- "eclipse/microprofile-graphql": {
+ "async-graphql/async-graphql": {
"hasCommitsInLast3Months": false,
- "stars": 101,
- "formattedStars": "101",
+ "stars": 3606,
+ "formattedStars": "4k",
"license": "Apache License 2.0",
- "lastRelease": "2022-03-21T18:26:51Z",
- "formattedLastRelease": "3 years ago"
+ "lastRelease": "",
+ "formattedLastRelease": ""
},
- "netflix/dgs-framework": {
+ "graphql-rust/juniper": {
"hasCommitsInLast3Months": false,
- "stars": 3280,
- "formattedStars": "3k",
- "license": "Apache License 2.0",
- "lastRelease": "2025-11-24T21:04:55Z",
- "formattedLastRelease": "4 days ago"
+ "stars": 5920,
+ "formattedStars": "6k",
+ "license": "Other",
+ "lastRelease": "2025-09-08T23:23:40Z",
+ "formattedLastRelease": "3 months ago"
},
- "spring-projects/spring-graphql": {
+ "apollographql/router": {
"hasCommitsInLast3Months": false,
- "stars": 1578,
- "formattedStars": "2k",
- "license": "Apache License 2.0",
- "lastRelease": "2025-11-18T10:05:26Z",
- "formattedLastRelease": "1 week ago"
+ "stars": 941,
+ "formattedStars": "1k",
+ "license": "Other",
+ "lastRelease": "2025-12-12T12:42:12Z",
+ "formattedLastRelease": "8 hours ago"
},
- "graphql-java-generator/graphql-gradle-plugin-project": {
+ "eerimoq/gqt": {
"hasCommitsInLast3Months": false,
- "stars": 57,
- "formattedStars": "57",
+ "stars": 470,
+ "formattedStars": "470",
"license": "MIT License",
"lastRelease": "",
"formattedLastRelease": ""
},
- "ropensci/ghql": {
- "hasCommitsInLast3Months": false,
- "stars": 149,
- "formattedStars": "149",
- "license": "Other",
- "lastRelease": "2025-09-08T08:41:00Z",
- "formattedLastRelease": "2 months ago"
- },
- "ohler55/agoo": {
+ "Escape-Technologies/graphql-armor": {
"hasCommitsInLast3Months": false,
- "stars": 924,
+ "stars": 561,
"formattedStars": "1k",
"license": "MIT License",
- "lastRelease": "2025-09-24T22:20:23Z",
- "formattedLastRelease": "2 months ago"
+ "lastRelease": "2025-08-22T13:32:40Z",
+ "formattedLastRelease": "3 months ago"
},
- "rmosolgo/graphql-ruby": {
+ "ldebruijn/graphql-protect": {
"hasCommitsInLast3Months": false,
- "stars": 5428,
- "formattedStars": "5k",
+ "stars": 34,
+ "formattedStars": "34",
"license": "MIT License",
- "lastRelease": "2025-07-19T17:15:49Z",
- "formattedLastRelease": "4 months ago"
+ "lastRelease": "2025-11-25T14:27:46Z",
+ "formattedLastRelease": "2 weeks ago"
},
- "virtualshield/rails-graphql": {
+ "graphql-hive/gateway": {
"hasCommitsInLast3Months": false,
- "stars": 187,
- "formattedStars": "187",
+ "stars": 69,
+ "formattedStars": "69",
"license": "MIT License",
- "lastRelease": "2025-08-25T17:53:38Z",
- "formattedLastRelease": "3 months ago"
+ "lastRelease": "2025-12-10T19:27:44Z",
+ "formattedLastRelease": "2 days ago"
},
- "obmarg/cynic": {
+ "microcks/microcks": {
"hasCommitsInLast3Months": false,
- "stars": 442,
- "formattedStars": "442",
- "license": "Mozilla Public License 2.0",
- "lastRelease": "2025-08-19T19:37:22Z",
- "formattedLastRelease": "3 months ago"
+ "stars": 1762,
+ "formattedStars": "2k",
+ "license": "Apache License 2.0",
+ "lastRelease": "2025-12-08T15:51:55Z",
+ "formattedLastRelease": "4 days ago"
},
- "arthurkhlghatyan/gql-client-rs": {
+ "schemathesis/schemathesis": {
"hasCommitsInLast3Months": false,
- "stars": 51,
- "formattedStars": "51",
+ "stars": 2891,
+ "formattedStars": "3k",
"license": "MIT License",
- "lastRelease": "2025-06-07T14:31:10Z",
- "formattedLastRelease": "5 months ago"
+ "lastRelease": "2025-12-10T19:22:47Z",
+ "formattedLastRelease": "2 days ago"
},
- "async-graphql/async-graphql": {
+ "glideapps/quicktype": {
"hasCommitsInLast3Months": false,
- "stars": 3594,
- "formattedStars": "4k",
+ "stars": 13505,
+ "formattedStars": "14k",
"license": "Apache License 2.0",
"lastRelease": "",
"formattedLastRelease": ""
},
- "graphql-rust/juniper": {
- "hasCommitsInLast3Months": false,
- "stars": 5911,
- "formattedStars": "6k",
- "license": "Other",
- "lastRelease": "2025-09-08T23:23:40Z",
- "formattedLastRelease": "2 months ago"
- },
- "ghostdogpr/caliban": {
+ "wundergraph/cosmo": {
"hasCommitsInLast3Months": false,
- "stars": 976,
+ "stars": 1126,
"formattedStars": "1k",
"license": "Apache License 2.0",
- "lastRelease": "2025-07-14T00:24:20Z",
- "formattedLastRelease": "4 months ago"
- },
- "sangria-graphql/sangria": {
- "hasCommitsInLast3Months": false,
- "stars": 1957,
- "formattedStars": "2k",
- "license": "Apache License 2.0",
- "lastRelease": "2025-10-20T11:40:30Z",
- "formattedLastRelease": "1 month ago"
+ "lastRelease": "2025-12-10T12:36:49Z",
+ "formattedLastRelease": "2 days ago"
},
"apollographql/apollo-ios": {
"hasCommitsInLast3Months": false,
- "stars": 4011,
+ "stars": 4018,
"formattedStars": "4k",
"license": "MIT License",
- "lastRelease": "2025-11-05T23:30:57Z",
- "formattedLastRelease": "3 weeks ago"
+ "lastRelease": "2025-12-03T20:34:39Z",
+ "formattedLastRelease": "1 week ago"
},
"nerdsupremacist/Graphaello": {
"hasCommitsInLast3Months": false,
@@ -1249,7 +1321,7 @@
},
"GraphQLSwift/Graphiti": {
"hasCommitsInLast3Months": false,
- "stars": 553,
+ "stars": 554,
"formattedStars": "1k",
"license": "MIT License",
"lastRelease": "2025-08-21T19:30:20Z",
@@ -1262,77 +1334,5 @@
"license": "MIT License",
"lastRelease": "2021-05-17T12:51:10Z",
"formattedLastRelease": "4 years ago"
- },
- "apollographql/router": {
- "hasCommitsInLast3Months": false,
- "stars": 939,
- "formattedStars": "1k",
- "license": "Other",
- "lastRelease": "2025-11-28T17:47:44Z",
- "formattedLastRelease": "18 hours ago"
- },
- "Escape-Technologies/graphql-armor": {
- "hasCommitsInLast3Months": false,
- "stars": 558,
- "formattedStars": "1k",
- "license": "MIT License",
- "lastRelease": "2025-08-22T13:32:40Z",
- "formattedLastRelease": "3 months ago"
- },
- "eerimoq/gqt": {
- "hasCommitsInLast3Months": false,
- "stars": 470,
- "formattedStars": "470",
- "license": "MIT License",
- "lastRelease": "",
- "formattedLastRelease": ""
- },
- "ldebruijn/graphql-protect": {
- "hasCommitsInLast3Months": false,
- "stars": 34,
- "formattedStars": "34",
- "license": "MIT License",
- "lastRelease": "2025-11-25T14:27:46Z",
- "formattedLastRelease": "3 days ago"
- },
- "graphql-hive/gateway": {
- "hasCommitsInLast3Months": false,
- "stars": 69,
- "formattedStars": "69",
- "license": "MIT License",
- "lastRelease": "2025-11-24T15:40:17Z",
- "formattedLastRelease": "4 days ago"
- },
- "microcks/microcks": {
- "hasCommitsInLast3Months": false,
- "stars": 1754,
- "formattedStars": "2k",
- "license": "Apache License 2.0",
- "lastRelease": "2025-10-25T15:08:00Z",
- "formattedLastRelease": "1 month ago"
- },
- "schemathesis/schemathesis": {
- "hasCommitsInLast3Months": false,
- "stars": 2871,
- "formattedStars": "3k",
- "license": "MIT License",
- "lastRelease": "2025-11-28T16:13:29Z",
- "formattedLastRelease": "19 hours ago"
- },
- "glideapps/quicktype": {
- "hasCommitsInLast3Months": false,
- "stars": 13464,
- "formattedStars": "13k",
- "license": "Apache License 2.0",
- "lastRelease": "",
- "formattedLastRelease": ""
- },
- "wundergraph/cosmo": {
- "hasCommitsInLast3Months": false,
- "stars": 1120,
- "formattedStars": "1k",
- "license": "Apache License 2.0",
- "lastRelease": "2025-11-27T13:47:56Z",
- "formattedLastRelease": "1 day ago"
}
}
\ No newline at end of file
diff --git a/scripts/get-github-info/last-success.isodate b/scripts/get-github-info/last-success.isodate
index 817c2fe4de..5d68e2023c 100644
--- a/scripts/get-github-info/last-success.isodate
+++ b/scripts/get-github-info/last-success.isodate
@@ -1 +1 @@
-2025-11-29T12:04:08.519Z
\ No newline at end of file
+2025-12-12T20:51:22.323Z
\ No newline at end of file
diff --git a/scripts/sync-working-groups/package.json b/scripts/sync-working-groups/package.json
index b470821c56..7659901d21 100644
--- a/scripts/sync-working-groups/package.json
+++ b/scripts/sync-working-groups/package.json
@@ -7,6 +7,6 @@
"start": "node ./sync-working-groups.ts"
},
"dependencies": {
- "arktype": "^2.1.27"
+ "arktype": "2.1.28"
}
}
diff --git a/scripts/sync-working-groups/working-group-events.ndjson b/scripts/sync-working-groups/working-group-events.ndjson
index 012d82306e..962f16860f 100644
--- a/scripts/sync-working-groups/working-group-events.ndjson
+++ b/scripts/sync-working-groups/working-group-events.ndjson
@@ -30,8 +30,12 @@
{"kind":"calendar#event","etag":"\"3524923696926750\"","id":"56uko3hh68be4q73tttdicg7l2_20251225T183000Z","status":"confirmed","htmlLink":"https://www.google.com/calendar/event?eid=NTZ1a28zaGg2OGJlNHE3M3R0dGRpY2c3bDJfMjAyNTEyMjVUMTgzMDAwWiBsaW51eGZvdW5kYXRpb24ub3JnX2lrNzl0OXV1ajJwMzJpM3IyMDNkZ3Y1bW84QGc","created":"2025-10-16T15:10:58.000Z","updated":"2025-11-06T20:44:08.463Z","summary":"GraphQL AI Working Group","description":"Sign up and view agenda at https://github.com/graphql/ai-wg Zoom password: aiwg","location":"https://zoom.us/j/92302442188","creator":{"email":"benjie@graphile.com"},"organizer":{"email":"linuxfoundation.org_ik79t9uuj2p32i3r203dgv5mo8@group.calendar.google.com","displayName":"GraphQL Foundation - Public","self":true},"start":"2025-12-11T13:30:00-05:00","end":"2025-12-11T14:30:00-05:00","recurringEventId":"56uko3hh68be4q73tttdicg7l2","originalStartTime":{"dateTime":"2025-12-25T13:30:00-05:00","timeZone":"America/New_York"},"transparency":"transparent","iCalUID":"56uko3hh68be4q73tttdicg7l2@google.com","sequence":1,"eventType":"default"}
{"kind":"calendar#event","etag":"\"3516415120288286\"","id":"h9erafl4rc1jjor9i6akokm5ec_20251218T160000Z","status":"confirmed","htmlLink":"https://www.google.com/calendar/event?eid=aDllcmFmbDRyYzFqam9yOWk2YWtva201ZWNfMjAyNTEyMThUMTYwMDAwWiBsaW51eGZvdW5kYXRpb24ub3JnX2lrNzl0OXV1ajJwMzJpM3IyMDNkZ3Y1bW84QGc","created":"2023-12-08T21:32:03.000Z","updated":"2025-09-18T14:59:20.144Z","summary":"GraphQL Governing Board Meeting","creator":{"email":"jburson@linuxfoundation.org"},"organizer":{"email":"linuxfoundation.org_ik79t9uuj2p32i3r203dgv5mo8@group.calendar.google.com","displayName":"GraphQL Foundation - Public","self":true},"start":"2025-12-18T11:00:00-05:00","end":"2025-12-18T12:00:00-05:00","recurringEventId":"h9erafl4rc1jjor9i6akokm5ec","originalStartTime":{"dateTime":"2025-12-18T11:00:00-05:00","timeZone":"America/New_York"},"iCalUID":"h9erafl4rc1jjor9i6akokm5ec@google.com","sequence":3,"eventType":"default"}
{"kind":"calendar#event","etag":"\"3462003372886000\"","id":"kkc5tt01ovrjv8fki1lo31g5hj_20251218T170000Z","status":"confirmed","htmlLink":"https://www.google.com/calendar/event?eid=a2tjNXR0MDFvdnJqdjhma2kxbG8zMWc1aGpfMjAyNTEyMThUMTcwMDAwWiBsaW51eGZvdW5kYXRpb24ub3JnX2lrNzl0OXV1ajJwMzJpM3IyMDNkZ3Y1bW84QGc","created":"2024-01-12T09:55:37.000Z","updated":"2024-11-07T17:48:06.443Z","summary":"Composite schemas WG - Weekly 3","description":"The weekly \"secondary\" meeting of the composite schemas WG: https://github.com/graphql/composite-schemas-wg Meeting password is \"composite\" Live notes are at https://docs.google.com/document/d/1hJO6U7daYvcNcQ3FBKnh3v4R256ers6M8IGyqRpY_kE/edit?usp=sharing ","location":"https://zoom.us/j/91078840351","creator":{"email":"benjie@graphile.com"},"organizer":{"email":"linuxfoundation.org_ik79t9uuj2p32i3r203dgv5mo8@group.calendar.google.com","displayName":"GraphQL Foundation - Public","self":true},"start":"2025-12-18T12:00:00-05:00","end":"2025-12-18T13:00:00-05:00","recurringEventId":"kkc5tt01ovrjv8fki1lo31g5hj","originalStartTime":{"dateTime":"2025-12-18T12:00:00-05:00","timeZone":"Europe/Berlin"},"iCalUID":"kkc5tt01ovrjv8fki1lo31g5hj@google.com","sequence":1,"eventType":"default"}
-{"kind":"calendar#event","etag":"\"3500694996844990\"","id":"2ffd8o32sh77kd3mtccrtg887n_20251218T183000Z","status":"confirmed","htmlLink":"https://www.google.com/calendar/event?eid=MmZmZDhvMzJzaDc3a2QzbXRjY3J0Zzg4N25fMjAyNTEyMThUMTgzMDAwWiBsaW51eGZvdW5kYXRpb24ub3JnX2lrNzl0OXV1ajJwMzJpM3IyMDNkZ3Y1bW84QGc","created":"2025-05-01T19:23:48.000Z","updated":"2025-06-19T15:38:18.422Z","summary":"GraphQL WG - Secondary (EU)","description":"Zoom password: graphqlwg","location":"https://zoom.us/j/593263740","creator":{"email":"benjie@graphile.com"},"organizer":{"email":"linuxfoundation.org_ik79t9uuj2p32i3r203dgv5mo8@group.calendar.google.com","displayName":"GraphQL Foundation - Public","self":true},"start":"2025-12-18T13:30:00-05:00","end":"2025-12-18T15:00:00-05:00","recurringEventId":"2ffd8o32sh77kd3mtccrtg887n","originalStartTime":{"dateTime":"2025-12-18T13:30:00-05:00","timeZone":"America/Los_Angeles"},"iCalUID":"2ffd8o32sh77kd3mtccrtg887n@google.com","sequence":0,"eventType":"default"}
+{"kind":"calendar#event","etag":"\"3529763944050462\"","id":"2ffd8o32sh77kd3mtccrtg887n_20251218T183000Z","status":"confirmed","htmlLink":"https://www.google.com/calendar/event?eid=MmZmZDhvMzJzaDc3a2QzbXRjY3J0Zzg4N25fMjAyNTEyMThUMTgzMDAwWiBsaW51eGZvdW5kYXRpb24ub3JnX2lrNzl0OXV1ajJwMzJpM3IyMDNkZ3Y1bW84QGc","created":"2025-05-01T19:23:48.000Z","updated":"2025-12-04T20:59:32.025Z","summary":"GraphQL WG - Secondary (EU)","description":"Zoom password: graphqlwg","location":"https://zoom.us/j/593263740","creator":{"email":"benjie@graphile.com"},"organizer":{"email":"linuxfoundation.org_ik79t9uuj2p32i3r203dgv5mo8@group.calendar.google.com","displayName":"GraphQL Foundation - Public","self":true},"start":"2025-12-18T13:30:00-05:00","end":"2025-12-18T15:00:00-05:00","recurringEventId":"2ffd8o32sh77kd3mtccrtg887n","originalStartTime":{"dateTime":"2025-12-18T13:30:00-05:00","timeZone":"America/Los_Angeles"},"iCalUID":"2ffd8o32sh77kd3mtccrtg887n@google.com","sequence":0,"eventType":"default"}
{"kind":"calendar#event","etag":"\"3517067971709790\"","id":"f7cvs5ala9jtt147l3mik2mlvl_20251222T160000Z","status":"confirmed","htmlLink":"https://www.google.com/calendar/event?eid=ZjdjdnM1YWxhOWp0dDE0N2wzbWlrMm1sdmxfMjAyNTEyMjJUMTYwMDAwWiBsaW51eGZvdW5kYXRpb24ub3JnX2lrNzl0OXV1ajJwMzJpM3IyMDNkZ3Y1bW84QGc","created":"2024-01-29T15:14:17.000Z","updated":"2025-09-22T09:39:45.854Z","summary":"Conference & Community Committee Meeting - Fortnightly Recurring","description":"\nYou have been invited to a recurring meeting for GraphQL Foundation\n\nWeekly Sync and Coordination Meeting for Conference Committee participants. Notes Document: https://docs.google.com/document/d/19-alP5jywnXzgN_1zYLBTRWh-4CaXzGakEZdTBFwNAc/edit \n\nWays to join meeting:\n\n1. Join from PC, Mac, iPad, or Android\n\nhttps://zoom-lfx.platform.linuxfoundation.org/meeting/96286151238?password=ff267735-efbd-4be4-a89c-b927b596190a \n\n2. Join via audio\n\nOne tap mobile:\nUS: +12532158782,,96286151238# or +13462487799,,96286151238\n\nOr dial:\nUS: +1 253 215 8782 or +1 346 248 7799 or +1 669 900 6833 or +1 301 715 8592 or +1 312 626 6799 or +1 646 374 8656 or 877 369 0926 (Toll Free) or 855 880 1246 (Toll Free)\nCanada: +1 647 374 4685 or +1 647 558 0588 or +1 778 907 2071 or +1 204 272 7920 or +1 438 809 7799 or +1 587 328 1099 or 855 703 8985 (Toll Free)\n\nMeeting ID: 96286151238\n\nMeeting Passcode: 986182\n\n\nInternational numbers: https://zoom.us/u/alwnPIaVT \n","location":"https://zoom-lfx.platform.linuxfoundation.org/meeting/96286151238?password=ff267735-efbd-4be4-a89c-b927b596190a","creator":{"email":"benjie@graphile.com"},"organizer":{"email":"linuxfoundation.org_ik79t9uuj2p32i3r203dgv5mo8@group.calendar.google.com","displayName":"GraphQL Foundation - Public","self":true},"start":"2025-12-22T11:00:00-05:00","end":"2025-12-22T12:00:00-05:00","recurringEventId":"f7cvs5ala9jtt147l3mik2mlvl","originalStartTime":{"dateTime":"2025-12-22T11:00:00-05:00","timeZone":"America/New_York"},"iCalUID":"f7cvs5ala9jtt147l3mik2mlvl@google.com","sequence":2,"guestsCanInviteOthers":false,"eventType":"default"}
{"kind":"calendar#event","etag":"\"3524923598591262\"","id":"s9agipg1r702pfngano7pol2h5_20251225T170000Z","status":"confirmed","htmlLink":"https://www.google.com/calendar/event?eid=czlhZ2lwZzFyNzAycGZuZ2Fubzdwb2wyaDVfMjAyNTEyMjVUMTcwMDAwWiBsaW51eGZvdW5kYXRpb24ub3JnX2lrNzl0OXV1ajJwMzJpM3IyMDNkZ3Y1bW84QGc","created":"2024-01-12T09:56:07.000Z","updated":"2025-11-06T20:43:19.295Z","summary":"Composite schemas WG - Weekly 4","description":"The weekly "secondary" meeting of the composite schemas WG: https://github.com/graphql/composite-schemas-wg Meeting password is "composite" Live notes are at https://docs.google.com/document/d/1hJO6U7daYvcNcQ3FBKnh3v4R256ers6M8IGyqRpY_kE/edit?usp=sharing ","location":"https://zoom.us/j/91078840351","creator":{"email":"benjie@graphile.com"},"organizer":{"email":"linuxfoundation.org_ik79t9uuj2p32i3r203dgv5mo8@group.calendar.google.com","displayName":"GraphQL Foundation - Public","self":true},"start":"2025-12-25T12:00:00-05:00","end":"2025-12-25T13:00:00-05:00","recurringEventId":"s9agipg1r702pfngano7pol2h5","originalStartTime":{"dateTime":"2025-12-25T12:00:00-05:00","timeZone":"Europe/Berlin"},"iCalUID":"s9agipg1r702pfngano7pol2h5@google.com","sequence":1,"eventType":"default"}
{"kind":"calendar#event","etag":"\"3524923550687710\"","id":"4igp67o2j2nkso49c1d6nbv040_20251225T180000Z","status":"confirmed","htmlLink":"https://www.google.com/calendar/event?eid=NGlncDY3bzJqMm5rc280OWMxZDZuYnYwNDBfMjAyNTEyMjVUMTgwMDAwWiBsaW51eGZvdW5kYXRpb24ub3JnX2lrNzl0OXV1ajJwMzJpM3IyMDNkZ3Y1bW84QGc","created":"2025-04-15T10:29:33.000Z","updated":"2025-11-06T20:42:55.343Z","summary":"GraphQL OTel WG","description":"Zoom password: otel https://github.com/graphql/otel-wg ","location":"https://zoom.us/j/93594710848?pwd=meEB8rd5g69r5DF8zFaL8VIWO2Il1v.1","creator":{"email":"benjie@graphile.com"},"organizer":{"email":"linuxfoundation.org_ik79t9uuj2p32i3r203dgv5mo8@group.calendar.google.com","displayName":"GraphQL Foundation - Public","self":true},"start":"2025-12-25T13:00:00-05:00","end":"2025-12-25T14:00:00-05:00","recurringEventId":"4igp67o2j2nkso49c1d6nbv040","originalStartTime":{"dateTime":"2025-12-25T13:00:00-05:00","timeZone":"America/Los_Angeles"},"iCalUID":"4igp67o2j2nkso49c1d6nbv040@google.com","sequence":0,"eventType":"default"}
{"kind":"calendar#event","etag":"\"3524923616454910\"","id":"pag44b4o3k87r90laj5vf5t67v_20251225T190000Z","status":"confirmed","htmlLink":"https://www.google.com/calendar/event?eid=cGFnNDRiNG8zazg3cjkwbGFqNXZmNXQ2N3ZfMjAyNTEyMjVUMTkwMDAwWiBsaW51eGZvdW5kYXRpb24ub3JnX2lrNzl0OXV1ajJwMzJpM3IyMDNkZ3Y1bW84QGc","created":"2023-12-04T10:48:14.000Z","updated":"2025-11-06T20:43:28.227Z","summary":"GraphQL-over-HTTP WG","description":"Zoom password: httpwg","location":"https://zoom.us/j/92781382543","creator":{"email":"benjie@graphile.com"},"organizer":{"email":"linuxfoundation.org_ik79t9uuj2p32i3r203dgv5mo8@group.calendar.google.com","displayName":"GraphQL Foundation - Public","self":true},"start":"2025-12-25T14:00:00-05:00","end":"2025-12-25T15:00:00-05:00","recurringEventId":"pag44b4o3k87r90laj5vf5t67v","originalStartTime":{"dateTime":"2025-12-25T14:00:00-05:00","timeZone":"America/Los_Angeles"},"iCalUID":"pag44b4o3k87r90laj5vf5t67v@google.com","sequence":3,"eventType":"default"}
+{"kind":"calendar#event","etag":"\"3530925154728702\"","id":"f7cvs5ala9jtt147l3mik2mlvl_20260105T160000Z","status":"confirmed","htmlLink":"https://www.google.com/calendar/event?eid=ZjdjdnM1YWxhOWp0dDE0N2wzbWlrMm1sdmxfMjAyNjAxMDVUMTYwMDAwWiBsaW51eGZvdW5kYXRpb24ub3JnX2lrNzl0OXV1ajJwMzJpM3IyMDNkZ3Y1bW84QGc","created":"2024-01-29T15:14:17.000Z","updated":"2025-12-11T14:16:17.364Z","summary":"Conference & Community Committee Meeting - Fortnightly Recurring","description":"\nYou have been invited to a recurring meeting for GraphQL Foundation\n\nWeekly Sync and Coordination Meeting for Conference Committee participants. Notes Document: https://docs.google.com/document/d/19-alP5jywnXzgN_1zYLBTRWh-4CaXzGakEZdTBFwNAc/edit \n\nWays to join meeting:\n\n1. Join from PC, Mac, iPad, or Android\n\nhttps://zoom-lfx.platform.linuxfoundation.org/meeting/96286151238?password=ff267735-efbd-4be4-a89c-b927b596190a \n\n2. Join via audio\n\nOne tap mobile:\nUS: +12532158782,,96286151238# or +13462487799,,96286151238\n\nOr dial:\nUS: +1 253 215 8782 or +1 346 248 7799 or +1 669 900 6833 or +1 301 715 8592 or +1 312 626 6799 or +1 646 374 8656 or 877 369 0926 (Toll Free) or 855 880 1246 (Toll Free)\nCanada: +1 647 374 4685 or +1 647 558 0588 or +1 778 907 2071 or +1 204 272 7920 or +1 438 809 7799 or +1 587 328 1099 or 855 703 8985 (Toll Free)\n\nMeeting ID: 96286151238\n\nMeeting Passcode: 986182\n\n\nInternational numbers: https://zoom.us/u/alwnPIaVT \n","location":"https://zoom-lfx.platform.linuxfoundation.org/meeting/96286151238?password=ff267735-efbd-4be4-a89c-b927b596190a","creator":{"email":"benjie@graphile.com"},"organizer":{"email":"linuxfoundation.org_ik79t9uuj2p32i3r203dgv5mo8@group.calendar.google.com","displayName":"GraphQL Foundation - Public","self":true},"start":"2026-01-05T11:00:00-05:00","end":"2026-01-05T12:00:00-05:00","recurringEventId":"f7cvs5ala9jtt147l3mik2mlvl","originalStartTime":{"dateTime":"2026-01-05T11:00:00-05:00","timeZone":"America/New_York"},"iCalUID":"f7cvs5ala9jtt147l3mik2mlvl@google.com","sequence":2,"guestsCanInviteOthers":false,"eventType":"default"}
+{"kind":"calendar#event","etag":"\"3526826931784574\"","id":"q3qul35gpekign7gc8cvr6bap1_20260108T140000Z","status":"confirmed","htmlLink":"https://www.google.com/calendar/event?eid=cTNxdWwzNWdwZWtpZ243Z2M4Y3ZyNmJhcDFfMjAyNjAxMDhUMTQwMDAwWiBsaW51eGZvdW5kYXRpb24ub3JnX2lrNzl0OXV1ajJwMzJpM3IyMDNkZ3Y1bW84QGc","created":"2023-12-08T21:20:13.000Z","updated":"2025-11-17T21:04:25.892Z","summary":"Marketing & Content Subcommittee Meeting","location":"https://zoom-lfx.platform.linuxfoundation.org/meeting/91228653788?password=0745533d-9a7a-42bb-8c72-3b823f679384","creator":{"email":"benjie@graphile.com"},"organizer":{"email":"linuxfoundation.org_ik79t9uuj2p32i3r203dgv5mo8@group.calendar.google.com","displayName":"GraphQL Foundation - Public","self":true},"start":"2026-01-08T09:00:00-05:00","end":"2026-01-08T10:00:00-05:00","recurringEventId":"q3qul35gpekign7gc8cvr6bap1_R20251030T130000","originalStartTime":{"dateTime":"2026-01-08T09:00:00-05:00","timeZone":"America/New_York"},"iCalUID":"q3qul35gpekign7gc8cvr6bap1_R20251030T130000@google.com","sequence":1,"eventType":"default"}
+{"kind":"calendar#event","etag":"\"3512629578532638\"","id":"1ae8m39lvqtigc4ao1p670g8il_20260108T160000Z","status":"confirmed","htmlLink":"https://www.google.com/calendar/event?eid=MWFlOG0zOWx2cXRpZ2M0YW8xcDY3MGc4aWxfMjAyNjAxMDhUMTYwMDAwWiBsaW51eGZvdW5kYXRpb24ub3JnX2lrNzl0OXV1ajJwMzJpM3IyMDNkZ3Y1bW84QGc","created":"2025-02-04T14:14:16.000Z","updated":"2025-08-27T17:13:09.266Z","summary":"GraphQL Community WG","description":"Meeting password: communityhttps://github.com/graphql/community-wg/tree/main/agendas Please be aware that meetings are recorded and/or live-streamed.","location":"https://zoom.us/j/93104287544","creator":{"email":"benjie@graphile.com"},"organizer":{"email":"linuxfoundation.org_ik79t9uuj2p32i3r203dgv5mo8@group.calendar.google.com","displayName":"GraphQL Foundation - Public","self":true},"start":"2026-01-08T11:00:00-05:00","end":"2026-01-08T12:00:00-05:00","recurringEventId":"1ae8m39lvqtigc4ao1p670g8il_R20250313T150000","originalStartTime":{"dateTime":"2026-01-08T11:00:00-05:00","timeZone":"America/Los_Angeles"},"iCalUID":"1ae8m39lvqtigc4ao1p670g8il_R20250313T150000@google.com","sequence":1,"eventType":"default"}
+{"kind":"calendar#event","etag":"\"3462003293658000\"","id":"lvqspfdh491rrdmvl7k1mruqd8_20260108T170000Z","status":"confirmed","htmlLink":"https://www.google.com/calendar/event?eid=bHZxc3BmZGg0OTFycmRtdmw3azFtcnVxZDhfMjAyNjAxMDhUMTcwMDAwWiBsaW51eGZvdW5kYXRpb24ub3JnX2lrNzl0OXV1ajJwMzJpM3IyMDNkZ3Y1bW84QGc","created":"2024-01-12T09:56:35.000Z","updated":"2024-11-07T17:47:26.829Z","summary":"Composite schemas WG - Weekly 2","description":"The weekly "secondary" meeting of the composite schemas WG: https://github.com/graphql/composite-schemas-wg Meeting password is "composite" Live notes are at https://docs.google.com/document/d/1hJO6U7daYvcNcQ3FBKnh3v4R256ers6M8IGyqRpY_kE/edit?usp=sharing ","location":"https://zoom.us/j/91078840351","creator":{"email":"benjie@graphile.com"},"organizer":{"email":"linuxfoundation.org_ik79t9uuj2p32i3r203dgv5mo8@group.calendar.google.com","displayName":"GraphQL Foundation - Public","self":true},"start":"2026-01-08T12:00:00-05:00","end":"2026-01-08T13:00:00-05:00","recurringEventId":"lvqspfdh491rrdmvl7k1mruqd8","originalStartTime":{"dateTime":"2026-01-08T12:00:00-05:00","timeZone":"Europe/Berlin"},"iCalUID":"lvqspfdh491rrdmvl7k1mruqd8@google.com","sequence":1,"eventType":"default"}
diff --git a/src/_design-system/breadcrumbs.tsx b/src/_design-system/breadcrumbs.tsx
index 4b42a7554a..a394874b38 100644
--- a/src/_design-system/breadcrumbs.tsx
+++ b/src/_design-system/breadcrumbs.tsx
@@ -26,9 +26,10 @@ export const Breadcrumbs = ({
const title = extractStringsFromReactNode(item.title)
const className = clsx(
- "text-neu-700 dark:text-neu-400 min-w-6 last:text-neu-800 dark:last:text-neu-800 leading-none",
+ "text-neu-700 dark:text-neu-400 min-w-6 last:text-neu-800 dark:last:text-neu-800 leading-none whitespace-pre",
href &&
"gql-focus-visible ring-inset hover:text-neu-900 hover:underline underline-offset-2",
+ item.title.length > 8 ? "overflow-hidden truncate" : "shrink-0",
)
return (
diff --git a/src/app/(main)/community/events/page.tsx b/src/app/(main)/community/events/page.tsx
index efd83a1580..cf8ee46fcd 100644
--- a/src/app/(main)/community/events/page.tsx
+++ b/src/app/(main)/community/events/page.tsx
@@ -140,7 +140,7 @@ function Stripes() {
+
+
+
Blog posts
+
{title}
+
+ {description}
+
+
+
+ {readAllLabel}
+
+
+
+
+ {posts.map(post => (
+
+ ))}
+
+
+ )
+}
diff --git a/src/app/(main)/resources/[category]/blur-corner.webp b/src/app/(main)/resources/[category]/blur-corner.webp
new file mode 100644
index 0000000000..50e58292e7
Binary files /dev/null and b/src/app/(main)/resources/[category]/blur-corner.webp differ
diff --git a/src/app/(main)/resources/[category]/cards-section.tsx b/src/app/(main)/resources/[category]/cards-section.tsx
new file mode 100644
index 0000000000..296f1d25a3
--- /dev/null
+++ b/src/app/(main)/resources/[category]/cards-section.tsx
@@ -0,0 +1,102 @@
+import { Kind, ResourceMetadata, Topic } from "@/resources/types"
+import { Eyebrow } from "@/_design-system/eyebrow"
+import { Button } from "@/app/conf/_design-system/button"
+
+import { ResourceHubCard } from "../resource-hub-card"
+
+import { texts, sectionKindNames, sectionIds } from "./texts"
+
+function sectionLabel(kind: Kind) {
+ return sectionKindNames[kind] ?? `${kind[0].toUpperCase()}${kind.slice(1)}`
+}
+
+export function CardsSection({
+ section,
+ category,
+}: {
+ section: { kind: Kind; resources: ResourceMetadata[] }
+ category: Topic
+}) {
+ const sectionData = texts[category].sections[section.kind]
+ const heading = sectionData?.heading ?? sectionLabel(section.kind)
+ const text = sectionData?.text
+
+ let cta: React.ReactNode | undefined
+
+ if (section.kind === "video") {
+ cta = (
+
+ Go to Video Resources Library
+
+ )
+ } else if (section.kind === "docs") {
+ cta = (
+
+ Go to Documentation
+
+ )
+ }
+
+ return (
+
+
+
+
{sectionKindNames[section.kind]}
+
{heading}
+ {text && (
+
+ {text}
+
+ )}
+
+ {cta}
+
+
+
+ {section.resources.slice(0, 6).map(resource => (
+
+ tag !== section.kind)}
+ duration={resource.duration}
+ />
+
+ ))}
+
+ {section.resources.length > 6 && (
+
+ {/* we're using for SEO and Cmd+F support */}
+
+
+ Load more
+
+
+
+ {section.resources.slice(6).map(resource => {
+ return (
+
+ tag !== section.kind)}
+ duration={resource.duration}
+ />
+
+ )
+ })}
+
+
+ )}
+
+ )
+}
diff --git a/src/app/(main)/resources/[category]/category-tools-libraries-section.tsx b/src/app/(main)/resources/[category]/category-tools-libraries-section.tsx
new file mode 100644
index 0000000000..56d4843136
--- /dev/null
+++ b/src/app/(main)/resources/[category]/category-tools-libraries-section.tsx
@@ -0,0 +1,265 @@
+import path from "node:path"
+import { glob } from "node:fs/promises"
+import { readFile } from "node:fs/promises"
+import matter from "gray-matter"
+import type { CSSProperties } from "react"
+
+import { Button } from "@/app/conf/_design-system/button"
+import blurCorner from "./blur-corner.webp"
+import { Eyebrow } from "@/_design-system/eyebrow"
+import slugMap from "@/code/slug-map.json"
+import { type Topic } from "@/resources/types"
+import { StripesDecoration } from "@/app/conf/_design-system/stripes-decoration"
+
+import { IconSpritesheet, IconName } from "./spritesheet"
+import CaretDown from "@/app/conf/_design-system/pixelarticons/caret-down.svg?svgr"
+
+interface LibraryEntry {
+ name: string
+ href?: string
+ group: string
+ tags: string[]
+}
+
+const librariesPromise = loadLibraries()
+
+async function loadLibraries(): Promise
{
+ const entries: LibraryEntry[] = []
+
+ for await (const file of glob("src/code/**/*.md")) {
+ const relative = path.relative("src/code", file)
+ const segments = relative.split(path.sep)
+ const top = segments[0]
+ const group =
+ top === "language-support" ? (segments[1] ?? "language-support") : top
+ if (!group) continue
+
+ const raw = await readFile(file, "utf8")
+ const { data } = matter(raw)
+ const tags: string[] = Array.isArray(data.tags) ? data.tags : []
+ if (!tags.includes("tools-and-libraries")) continue
+
+ const name: string | undefined = data.name
+ if (!name) continue
+
+ const href: string | undefined =
+ data.url ??
+ (data.github ? `https://github.com/${data.github}` : undefined) ??
+ (data.npm ? `https://npmjs.com/package/${data.npm}` : undefined)
+
+ entries.push({ name, href, group, tags })
+ }
+
+ const deduped = entries.filter(
+ (item, index, self) =>
+ index ===
+ self.findIndex(t => t.name.toLowerCase() === item.name.toLowerCase()),
+ )
+
+ return deduped
+}
+
+function displayName(id: string) {
+ const key = id as keyof typeof slugMap
+ return slugMap[key] ?? id
+}
+
+export async function CategoryToolsLibrariesSection({
+ category,
+}: {
+ category: Topic
+}) {
+ const libraries = await librariesPromise
+ const filtered = libraries.filter(item => item.tags.includes(category))
+
+ const sortedGroups = Array.from(
+ filtered.reduce>((acc, item) => {
+ const list = acc.get(item.group) ?? []
+ list.push(item)
+ acc.set(item.group, list)
+ return acc
+ }, new Map()),
+ )
+ .map(([group, items]) => ({
+ id: group,
+ name: displayName(group),
+ items: items
+ .sort((a, b) =>
+ a.name.localeCompare(b.name, "en", { sensitivity: "base" }),
+ )
+ .slice(0, 20),
+ }))
+ .sort((a, b) => b.items.length - a.items.length)
+
+ const grouped: GroupData[] = sortedGroups.map((group, index) => {
+ const nextLength = sortedGroups[index + 1]?.items.length ?? 0
+ const columns =
+ nextLength > 0 && group.items.length >= nextLength * 1.9 ? 2 : 1
+ const breakIndex = columns === 2 ? Math.ceil(group.items.length / 2) : 0
+ return { ...group, columns, breakIndex }
+ })
+
+ if (grouped.length === 0) {
+ return null
+ }
+
+ return (
+
+ )
+}
+
+interface GroupData {
+ id: string
+ name: string
+ items: LibraryEntry[]
+ columns: 1 | 2
+ breakIndex: number
+}
+
+function distributeToColumns(groups: GroupData[]): [GroupData[], GroupData[]] {
+ const left: GroupData[] = []
+ const right: GroupData[] = []
+
+ let leftHeight = 0
+ let rightHeight = 0
+
+ for (const group of groups) {
+ const itemRows =
+ group.columns === 2
+ ? Math.ceil(group.items.length / 2)
+ : group.items.length
+ const height = itemRows + 1
+ if (leftHeight <= rightHeight) {
+ left.push(group)
+ leftHeight += height
+ } else {
+ right.push(group)
+ rightHeight += height
+ }
+ }
+
+ return [left, right]
+}
+
+function Group({ group }: { group: GroupData }) {
+ return (
+
+
+
+
+
+
+ {group.name}
+
+
+
+
+
+ {group.items.map((item, i) => (
+ = group.breakIndex ? "1px" : "",
+ }
+ : {}
+ }
+ >
+ {item.href ? (
+
+ {item.name}
+
+ ) : (
+
+ {item.name}
+
+ )}
+
+ ))}
+
+
+ )
+}
+
+function Stripes() {
+ return (
+
+
+
+ )
+}
diff --git a/src/app/(main)/resources/[category]/page.tsx b/src/app/(main)/resources/[category]/page.tsx
new file mode 100644
index 0000000000..844d01e193
--- /dev/null
+++ b/src/app/(main)/resources/[category]/page.tsx
@@ -0,0 +1,164 @@
+import { Metadata } from "next"
+import { notFound } from "next/navigation"
+import type { Item } from "nextra/normalize-pages"
+
+import { NavbarFixed } from "@/components/navbar/navbar-fixed"
+import { getResourcesByTag } from "@/resources/data"
+import {
+ Kind,
+ kinds,
+ topics,
+ type ResourceMetadata,
+ type Topic,
+} from "@/resources/types"
+
+import { ResourcesHero } from "../resources-hero"
+import { TocHeroContents } from "@/components/toc-hero"
+import { BlogPostsSection } from "./blog-posts-section"
+import { CategoryToolsLibrariesSection } from "./category-tools-libraries-section"
+import { Breadcrumbs } from "@/_design-system/breadcrumbs"
+
+import { sectionKindNames, texts } from "./texts"
+import { CardsSection } from "./cards-section"
+
+interface PageParams {
+ category: string
+}
+
+export async function generateStaticParams() {
+ return topics.map(category => ({ category }))
+}
+
+export async function generateMetadata({
+ params,
+}: {
+ params: PageParams
+}): Promise {
+ const category = params.category as Topic
+ if (!topics.includes(category)) return {}
+
+ const title = `${texts[category].heading} Resources`
+ const description = texts[category].subtitle
+
+ return { title, description }
+}
+
+export default async function CategoryPage({ params }: { params: PageParams }) {
+ const category = params.category as Topic
+ if (!topics.includes(category)) return notFound()
+
+ const resources = await getResourcesByTag(category)
+ const deduped = uniqueByTitle(resources)
+ const grouped = groupByKind(deduped)
+
+ const activePath: Item[] = [
+ {
+ name: "Home",
+ route: "/",
+ },
+ {
+ name: "Resource Hub",
+ route: "/resources",
+ },
+ {
+ name: texts[category].heading,
+ route: "",
+ },
+ ].map(item => ({
+ ...item,
+ title: item.name,
+ type: "page",
+ children: [],
+ frontMatter: {},
+ }))
+
+ return (
+
+
+
+
+ sectionLabel(section.kind))}
+ className="max-w-[528px]"
+ />
+
+
+
+
+ {grouped.map(section => (
+
+ ))}
+
+ )
+}
+
+function uniqueByTitle(resources: ResourceMetadata[]) {
+ const seen = new Set()
+ return resources.filter(resource => {
+ const key = resource.title.trim().toLowerCase()
+ if (seen.has(key)) return false
+ seen.add(key)
+ return true
+ })
+}
+
+function groupByKind(resources: ResourceMetadata[]) {
+ return kinds
+ .map(kind => ({
+ kind,
+ resources: resources.filter(
+ resource => (resource.kind ?? getKindFromTags(resource)) === kind,
+ ),
+ }))
+ .filter(section => section.resources.length > 0)
+}
+
+function getKindFromTags(resource: ResourceMetadata) {
+ return kinds.find(kind => resource.tags.includes(kind))
+}
+
+function sectionLabel(kind: Kind) {
+ return sectionKindNames[kind] ?? `${kind[0].toUpperCase()}${kind.slice(1)}`
+}
+
+function CategorySection({
+ section,
+ category,
+}: {
+ section: { kind: Kind; resources: ResourceMetadata[] }
+ category: Topic
+}) {
+ if (section.kind === "tools-and-libraries") {
+ return
+ }
+
+ if (section.kind === "blog") {
+ const blogSection = texts[category].sections["blog-or-newsletter"]
+ return (
+ ({
+ href: resource.url,
+ title: resource.title,
+ author: resource.author ?? "GraphQL Community",
+ tags: resource.tags.filter(tag => tag !== "blog" && tag !== category),
+ }))}
+ />
+ )
+ }
+
+ return
+}
diff --git a/src/app/(main)/resources/[category]/spritesheet/index.tsx b/src/app/(main)/resources/[category]/spritesheet/index.tsx
new file mode 100644
index 0000000000..47eef9a0d1
--- /dev/null
+++ b/src/app/(main)/resources/[category]/spritesheet/index.tsx
@@ -0,0 +1,40 @@
+import type { SVGProps } from "react"
+
+import sheet from "./sheet.svg?resource"
+
+export type IconName =
+ | "ballerina"
+ | "c-net"
+ | "clojure"
+ | "elixir"
+ | "elm"
+ | "flutter"
+ | "go"
+ | "haskell"
+ | "java"
+ | "javascript"
+ | "julia"
+ | "multiplatform"
+ | "php"
+ | "python"
+ | "ruby"
+ | "rust"
+ | "scala"
+ | "swift"
+
+interface IconSpritesheetProps extends SVGProps {
+ sprite: IconName
+}
+
+export function IconSpritesheet({ sprite, ...props }: IconSpritesheetProps) {
+ return (
+ <>
+
+
+
+ >
+ )
+}
diff --git a/src/app/(main)/resources/[category]/spritesheet/sheet.svg b/src/app/(main)/resources/[category]/spritesheet/sheet.svg
new file mode 100644
index 0000000000..33545c32f1
--- /dev/null
+++ b/src/app/(main)/resources/[category]/spritesheet/sheet.svg
@@ -0,0 +1,178 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/app/(main)/resources/[category]/texts.ts b/src/app/(main)/resources/[category]/texts.ts
new file mode 100644
index 0000000000..b8ad42f0ed
--- /dev/null
+++ b/src/app/(main)/resources/[category]/texts.ts
@@ -0,0 +1,155 @@
+import { Kind, Topic } from "@/resources/types"
+
+export const texts: {
+ [key in Topic]: {
+ heading: string
+ subtitle: string
+ sections: {
+ [key in Kind]?: {
+ heading: string
+ text: string
+ }
+ }
+ }
+} = {
+ frontend: {
+ heading: "Frontend",
+ subtitle: "Learn how to use GraphQL on the frontend.",
+ sections: {
+ video: {
+ heading: "Master GraphQL on the frontend",
+ text: "Watch talks and tutorials from GraphQL Conf and community experts. See how teams integrate GraphQL on the frontend and learn from real-world case studies.",
+ },
+ "tools-and-libraries": {
+ heading: "Frontend tools & libraries",
+ text: "Explore the most popular GraphQL client libraries and frameworks for frontend. These tools help you fetch and manage data with GraphQL.",
+ },
+ "blog-or-newsletter": {
+ heading: "Insights for frontend devs",
+ text: "Stay up to date with insights from the GraphQL community.",
+ },
+ },
+ },
+ backend: {
+ heading: "Backend",
+ subtitle:
+ "Build powerful GraphQL backends with the right tools, libraries and expert insights.",
+ sections: {
+ video: {
+ heading: "Master GraphQL on the backend",
+ text: "Discover videos and tutorials to help you build, deploy and scale your GraphQL backend.",
+ },
+ "tools-and-libraries": {
+ heading: "Backend tools & libraries",
+ text: "Find the right GraphQL backend stack — from JavaScript to Rust and beyond.",
+ },
+ "blog-or-newsletter": {
+ heading: "Build better GraphQL infrastructure",
+ text: "Dive into articles on server architecture, schema design and best practices for running GraphQL at scale.",
+ },
+ },
+ },
+ federation: {
+ heading: "Federation",
+ subtitle: "Learn how to build and compose GraphQL graphs with federation.",
+ sections: {
+ video: {
+ heading: "Master GraphQL federation",
+ text: "Watch talks and tutorials from GraphQL Conf and community experts. See how teams build and compose GraphQL graphs with federation.",
+ },
+ "tools-and-libraries": {
+ heading: "Tools & libraries for federated graphs",
+ text: "Run federated GraphQL graphs at scale with the right tools — from open-source routers to managed platforms.",
+ },
+ "blog-or-newsletter": {
+ heading: "Latest updates on federation & composition",
+ text: "Read the latest announcements and technical deep dives.",
+ },
+ },
+ },
+ ai: {
+ heading: "Artificial Intelligence",
+ subtitle: "Explore how to use GraphQL for AI systems.",
+ sections: {
+ "tools-and-libraries": {
+ heading: "GraphQL tools for AI",
+ text: "Discover the best tools for building AI systems with GraphQL.",
+ },
+ "blog-or-newsletter": {
+ heading: "Latest insights on AI & GraphQL",
+ text: "Read the latest announcements and technical deep dives.",
+ },
+ },
+ },
+ security: {
+ heading: "Security",
+ subtitle: "Learn how to secure your GraphQL APIs.",
+ sections: {
+ "tools-and-libraries": {
+ heading: "GraphQL security tools",
+ text: "Find resources to help secure GraphQL APIs across various languages and frameworks.",
+ },
+ docs: {
+ heading: "Security in practice",
+ text: "Follow proven patterns to delegate authorization correctly and protect your GraphQL APIs from malicious operations.",
+ },
+ },
+ },
+ monitoring: {
+ heading: "Monitoring",
+ subtitle:
+ "Stay ahead of issues by monitoring queries and watching error trends.",
+ sections: {
+ "tools-and-libraries": {
+ heading: "GraphQL monitoring tools",
+ text: "Connect GraphQL tracing and alerting systems to reduce blind spots in production.",
+ },
+ docs: {
+ heading: "Monitoring in practice",
+ text: "Learn how to track the right signals in your GraphQL ecosystem — from latency and error rates to resolver-level performance.",
+ },
+ "blog-or-newsletter": {
+ heading: "Latest insights on monitoring",
+ text: "Read the latest announcements and technical deep dives.",
+ },
+ },
+ },
+ "schema-design": {
+ heading: "Schema Design",
+ subtitle: "Learn how to design and maintain GraphQL schemas.",
+ sections: {},
+ },
+ "api-platform-and-gateways": {
+ heading: "API Platform and Gateways",
+ subtitle: "Learn how to build and deploy API Gateways and Supergraphs.",
+ sections: {},
+ },
+ "developer-experience": {
+ heading: "Developer Experience",
+ subtitle: "Learn how to improve your developer experience.",
+ sections: {},
+ },
+ tools: {
+ heading: "Tools",
+ subtitle: "Discover the best tools for GraphQL development.",
+ sections: {},
+ },
+}
+
+export const sectionKindNames: Record = {
+ video: "Featured videos",
+ blog: "Blog posts",
+ "tools-and-libraries": "Tools & Libraries",
+ guide: "Guides",
+ book: "Books",
+ "blog-or-newsletter": "Blogs & Newsletters",
+ docs: "Documentation",
+}
+
+export function slugify(name: string): string {
+ return name.toLowerCase().replace(/ & /g, "-and-").replace(/ /g, "-")
+}
+
+export const sectionIds: Record = Object.fromEntries(
+ Object.entries(sectionKindNames).map(([kind, name]) => [kind, slugify(name)]),
+) as Record
diff --git a/src/app/(main)/resources/assets/ai.svg b/src/app/(main)/resources/assets/ai.svg
new file mode 100644
index 0000000000..1a74a15a0e
--- /dev/null
+++ b/src/app/(main)/resources/assets/ai.svg
@@ -0,0 +1,18 @@
+
+
+
+
+
diff --git a/src/app/(main)/resources/assets/archive.svg b/src/app/(main)/resources/assets/archive.svg
new file mode 100644
index 0000000000..bb98c0c8e5
--- /dev/null
+++ b/src/app/(main)/resources/assets/archive.svg
@@ -0,0 +1,14 @@
+
+
+
+
diff --git a/src/app/(main)/resources/assets/backend.svg b/src/app/(main)/resources/assets/backend.svg
new file mode 100644
index 0000000000..2477c25f58
--- /dev/null
+++ b/src/app/(main)/resources/assets/backend.svg
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+
diff --git a/src/app/(main)/resources/assets/bookmark.svg b/src/app/(main)/resources/assets/bookmark.svg
new file mode 100644
index 0000000000..422dc51a2a
--- /dev/null
+++ b/src/app/(main)/resources/assets/bookmark.svg
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
diff --git a/src/app/(main)/resources/assets/federation.svg b/src/app/(main)/resources/assets/federation.svg
new file mode 100644
index 0000000000..e16b30d1a2
--- /dev/null
+++ b/src/app/(main)/resources/assets/federation.svg
@@ -0,0 +1,11 @@
+
+
+
diff --git a/src/app/(main)/resources/assets/frontend.svg b/src/app/(main)/resources/assets/frontend.svg
new file mode 100644
index 0000000000..135bf7899a
--- /dev/null
+++ b/src/app/(main)/resources/assets/frontend.svg
@@ -0,0 +1,14 @@
+
+
+
+
diff --git a/src/app/(main)/resources/assets/monitoring.svg b/src/app/(main)/resources/assets/monitoring.svg
new file mode 100644
index 0000000000..aa9ce67345
--- /dev/null
+++ b/src/app/(main)/resources/assets/monitoring.svg
@@ -0,0 +1,14 @@
+
+
+
+
diff --git a/src/app/(main)/resources/assets/newspaper.svg b/src/app/(main)/resources/assets/newspaper.svg
new file mode 100644
index 0000000000..0bc736695f
--- /dev/null
+++ b/src/app/(main)/resources/assets/newspaper.svg
@@ -0,0 +1,15 @@
+
+
+
+
+
diff --git a/src/app/(main)/resources/assets/security.svg b/src/app/(main)/resources/assets/security.svg
new file mode 100644
index 0000000000..c999cc8256
--- /dev/null
+++ b/src/app/(main)/resources/assets/security.svg
@@ -0,0 +1,20 @@
+
+
+
+
+
+
diff --git a/src/app/(main)/resources/assets/tools.svg b/src/app/(main)/resources/assets/tools.svg
new file mode 100644
index 0000000000..e22e782c3d
--- /dev/null
+++ b/src/app/(main)/resources/assets/tools.svg
@@ -0,0 +1,20 @@
+
+
+
+
+
+
diff --git a/src/app/(main)/resources/assets/video-player.svg b/src/app/(main)/resources/assets/video-player.svg
new file mode 100644
index 0000000000..6a01b6f929
--- /dev/null
+++ b/src/app/(main)/resources/assets/video-player.svg
@@ -0,0 +1,6 @@
+
+
+
+
diff --git a/src/app/(main)/resources/assets/write-note.svg b/src/app/(main)/resources/assets/write-note.svg
new file mode 100644
index 0000000000..6c75219572
--- /dev/null
+++ b/src/app/(main)/resources/assets/write-note.svg
@@ -0,0 +1,16 @@
+
+
+
+
+
+
diff --git a/src/app/(main)/resources/blog-category-links.tsx b/src/app/(main)/resources/blog-category-links.tsx
new file mode 100644
index 0000000000..4ce389ef9c
--- /dev/null
+++ b/src/app/(main)/resources/blog-category-links.tsx
@@ -0,0 +1,46 @@
+"use client"
+
+import Link from "next/link"
+import { Collapsible } from "@base-ui-components/react/collapsible"
+
+import CaretDownIcon from "@/app/conf/_design-system/pixelarticons/caret-down.svg?svgr"
+import { BlogTags } from "@/components/blog-page/blog-tags"
+import { blogTagColors } from "@/components/blog-page/blog-tag-colors"
+
+const categories = Object.keys(blogTagColors)
+
+/**
+ * Shows tags on desktop and a collapsible on mobile.
+ */
+export function BlogCategoryLinks() {
+ return (
+ <>
+
+
+ categories
+
+
+
+ {categories.map(category => (
+
+ {category.replace(/-/g, " ")}
+
+ ))}
+
+
+
+
+ >
+ )
+}
diff --git a/src/app/(main)/resources/blog-post-list-item.tsx b/src/app/(main)/resources/blog-post-list-item.tsx
new file mode 100644
index 0000000000..9dcf236926
--- /dev/null
+++ b/src/app/(main)/resources/blog-post-list-item.tsx
@@ -0,0 +1,53 @@
+import Link from "next/link"
+
+import ArrowRightIcon from "@/app/conf/_design-system/pixelarticons/arrow-down.svg?svgr"
+import { BlogTags } from "@/components/blog-page/blog-tags"
+
+export interface BlogPostRowProps {
+ date: string
+ category: string
+ title: string
+ href: string
+ author: string
+}
+
+export function BlogPostListItem({
+ date,
+ category,
+ title,
+ href,
+ author,
+}: BlogPostRowProps) {
+ const formattedDate = new Date(date)
+ .toLocaleDateString("en-GB", {
+ day: "2-digit",
+ month: "2-digit",
+ year: "numeric",
+ })
+ .replaceAll("/", "-")
+
+ return (
+
+
+
+
+
+ {title}
+
+
+
+
+ {formattedDate}
+
+
+ {author}
+
+
+
+
+
+ )
+}
diff --git a/src/app/(main)/resources/blog-section.tsx b/src/app/(main)/resources/blog-section.tsx
new file mode 100644
index 0000000000..91a6546f9a
--- /dev/null
+++ b/src/app/(main)/resources/blog-section.tsx
@@ -0,0 +1,103 @@
+import fs from "node:fs/promises"
+import path from "node:path"
+import Link from "next/link"
+import grayMatter from "gray-matter"
+
+import { Button } from "@/app/conf/_design-system/button"
+import PlayIcon from "@/app/conf/_design-system/pixelarticons/play.svg?svgr"
+
+import { blogTagColors } from "@/components/blog-page/blog-tag-colors"
+import { BlogCategoryLinks } from "./blog-category-links"
+import { BlogPostListItem } from "./blog-post-list-item"
+
+interface BlogFrontMatter {
+ title: string
+ tags?: string[]
+ byline: string
+ date: string | number | Date
+}
+
+type BlogFrontMatterWithFile = BlogFrontMatter & {
+ fileName: string
+ date: Date
+}
+
+let cachedBlogFrontMatters: BlogFrontMatterWithFile[] | null = null
+
+async function getBlogFrontMatters() {
+ if (cachedBlogFrontMatters) return cachedBlogFrontMatters
+
+ const files = await fs.readdir("./src/pages/blog")
+
+ const blogs = await Promise.all(
+ files
+ .filter(filename => /\.mdx?$/.test(filename))
+ .map(async (filename: string) => {
+ const filePath = path.join("./src/pages/blog", filename)
+ const content = await fs.readFile(filePath, "utf8")
+ const { data } = grayMatter(content)
+ const frontMatter = data as BlogFrontMatter
+
+ return {
+ ...frontMatter,
+ fileName: path.parse(filePath).name,
+ date: new Date(frontMatter.date),
+ }
+ }),
+ )
+
+ cachedBlogFrontMatters = blogs
+ .filter((blog): blog is BlogFrontMatterWithFile =>
+ Boolean(blog.fileName && blog.title && blog.date),
+ )
+ .sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime())
+
+ return cachedBlogFrontMatters
+}
+
+export async function BlogSection() {
+ const blogs = await getBlogFrontMatters()
+ const blogPosts = Object.keys(blogTagColors)
+ .flatMap(tag => blogs.filter(blog => blog.tags?.includes(tag)).slice(0, 5))
+ .reduce((unique, blog) => {
+ if (!unique.some(item => item.fileName === blog.fileName)) {
+ unique.push(blog)
+ }
+ return unique
+ }, [])
+ .sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime())
+ .slice(0, 5)
+
+ return (
+
+
+
+
+
+
+ {blogPosts.map(post => (
+
+ ))}
+
+
+
+ Read all posts
+
+
+ )
+}
diff --git a/src/app/(main)/resources/categories-section.tsx b/src/app/(main)/resources/categories-section.tsx
new file mode 100644
index 0000000000..ebaa6df132
--- /dev/null
+++ b/src/app/(main)/resources/categories-section.tsx
@@ -0,0 +1,120 @@
+import Link from "next/link"
+
+import ArrowDownIcon from "@/app/conf/_design-system/pixelarticons/arrow-down.svg?svgr"
+
+import { Eyebrow } from "@/_design-system/eyebrow"
+import { type Topic } from "@/resources/types"
+
+import FrontendIcon from "./assets/frontend.svg?svgr"
+import BackendIcon from "./assets/backend.svg?svgr"
+import FederationIcon from "./assets/federation.svg?svgr"
+import SecurityIcon from "./assets/security.svg?svgr"
+import AIIcon from "./assets/ai.svg?svgr"
+import MonitoringIcon from "./assets/monitoring.svg?svgr"
+
+interface Category {
+ id: Topic
+ name: string
+ description: string
+ icon: React.ReactNode
+}
+
+const categories: Category[] = [
+ {
+ id: "frontend",
+ name: "Frontend",
+ description:
+ "Build better queries and optimize UI performance with the right client tools.",
+ icon: (
+
+ ),
+ },
+ {
+ id: "backend",
+ name: "Backend",
+ description:
+ "From resolvers to execution — everything you need to run a GraphQL server in production.",
+ icon: (
+
+ ),
+ },
+ {
+ id: "federation",
+ name: "Federation",
+ description:
+ "Design and manage distributed graphs that scale across teams and services.",
+ icon: (
+
+ ),
+ },
+ {
+ id: "security",
+ name: "Security",
+ description:
+ "Secure your GraphQL API with query limits and schema protection.",
+ icon: (
+
+ ),
+ },
+ {
+ id: "ai",
+ name: "AI",
+ description:
+ "Use GraphQL to power AI systems — patterns, tools and implementations.",
+ icon: (
+
+ ),
+ },
+ {
+ id: "monitoring",
+ name: "Monitoring",
+ description:
+ "Track performance, usage and schema changes to keep your graph healthy.",
+ icon: (
+
+ ),
+ },
+]
+
+export function CategoriesSection() {
+ return (
+
+
+
+
+ {categories.map(category => (
+
+ ))}
+
+
+ )
+}
+
+function CategoryCard({ category }: { category: Category }) {
+ return (
+
+
+
+
{category.name}
+
+ {category.description}
+
+
+
+ )
+}
diff --git a/src/app/(main)/resources/keep-learning.tsx b/src/app/(main)/resources/keep-learning.tsx
new file mode 100644
index 0000000000..178afc20ca
--- /dev/null
+++ b/src/app/(main)/resources/keep-learning.tsx
@@ -0,0 +1,55 @@
+import { clsx } from "clsx"
+
+import ArrowDownIcon from "@/app/conf/_design-system/pixelarticons/arrow-down.svg?svgr"
+import { StripesDecoration } from "@/app/conf/_design-system/stripes-decoration"
+import { Anchor } from "@/app/conf/_design-system/anchor"
+
+import blurCorner from "./[category]/blur-corner.webp"
+
+export function KeepLearning({
+ title,
+ href,
+ stripes,
+ className,
+}: {
+ title: string
+ href: string
+ stripes: string
+ className?: string
+}) {
+ return (
+
+ Keep Learning
+
+
+ Next
+
+
+ {title}
+
+
+
+ )
+}
+
+function Stripes({ stripes }: { stripes: string }) {
+ return (
+
+
+
+ )
+}
diff --git a/src/app/(main)/resources/page.tsx b/src/app/(main)/resources/page.tsx
new file mode 100644
index 0000000000..8cb5aa3652
--- /dev/null
+++ b/src/app/(main)/resources/page.tsx
@@ -0,0 +1,47 @@
+import { NavbarFixed } from "@/components/navbar/navbar-fixed"
+import { LookingForMore } from "@/components/looking-for-more"
+
+import { ResourcesHero } from "./resources-hero"
+import { CategoriesSection } from "./categories-section"
+import { ToolsLibrariesSection } from "./tools-libraries-section"
+import { SpecificationSection } from "./specification-section"
+import { BlogSection } from "./blog-section"
+import { VideoResourcesSection } from "./video-resources-section"
+import { ReadingResourcesSection } from "./reading-resources-section"
+
+export const metadata = {
+ title: "Resource Hub",
+ description:
+ "Explore curated GraphQL resources by topic. Find tools, videos, blog posts, and more to help you build with GraphQL.",
+}
+
+export default function ResourcesPage() {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ )
+}
diff --git a/src/app/(main)/resources/reading-resources-section.tsx b/src/app/(main)/resources/reading-resources-section.tsx
new file mode 100644
index 0000000000..daf6d6227c
--- /dev/null
+++ b/src/app/(main)/resources/reading-resources-section.tsx
@@ -0,0 +1,71 @@
+import Link from "next/link"
+
+import { Button } from "@/app/conf/_design-system/button"
+import { Eyebrow } from "@/_design-system/eyebrow"
+import ArrowDownIcon from "@/app/conf/_design-system/pixelarticons/arrow-down.svg?svgr"
+
+import NewspaperIcon from "./assets/newspaper.svg?svgr"
+import WriteIcon from "./assets/write-note.svg?svgr"
+import BookIcon from "./assets/bookmark.svg?svgr"
+
+export function ReadingResourcesSection() {
+ return (
+
+
+
+
reading resources library
+
+
+ Dive into GraphQL content
+
+
+ Browse reading materials to learn best practices and stay up to date
+ with the ecosystem.
+
+
+ Explore reading resources
+
+
+
+
+ }
+ label="Blogs and newsletters"
+ />
+ }
+ label="Individual posts"
+ />
+ }
+ label="Books"
+ />
+
+
+
+ )
+}
+
+function ReadingLink({
+ href,
+ icon,
+ label,
+}: {
+ href: string
+ icon: React.ReactNode
+ label: string
+}) {
+ return (
+
+ {icon}
+ {label}
+
+
+ )
+}
diff --git a/src/app/(main)/resources/reading/[subcategory]/page.tsx b/src/app/(main)/resources/reading/[subcategory]/page.tsx
new file mode 100644
index 0000000000..12ca5b273f
--- /dev/null
+++ b/src/app/(main)/resources/reading/[subcategory]/page.tsx
@@ -0,0 +1,32 @@
+import { notFound } from "next/navigation"
+
+import { ReadingLibraryPage, readingMetadata } from "../reading-page"
+import { subcategories, type Subcategory } from "../reading-page-categories"
+
+interface PageParams {
+ subcategory: string
+}
+
+function isSubcategory(value: string): value is Subcategory {
+ return subcategories.includes(value as Subcategory)
+}
+
+export function generateStaticParams() {
+ return subcategories.map(subcategory => ({ subcategory }))
+}
+
+export function generateMetadata({ params }: { params: PageParams }) {
+ const subcategory = params.subcategory
+ if (!isSubcategory(subcategory)) return {}
+ return readingMetadata(subcategory)
+}
+
+export default function ReadingSubcategoryPage({
+ params,
+}: {
+ params: PageParams
+}) {
+ const subcategory = params.subcategory
+ if (!isSubcategory(subcategory)) return notFound()
+ return
+}
diff --git a/src/app/(main)/resources/reading/page.tsx b/src/app/(main)/resources/reading/page.tsx
new file mode 100644
index 0000000000..a997ace6c3
--- /dev/null
+++ b/src/app/(main)/resources/reading/page.tsx
@@ -0,0 +1,7 @@
+import { ReadingLibraryPage, readingMetadata } from "./reading-page"
+
+export const metadata = readingMetadata("all")
+
+export default function ReadingPage() {
+ return
+}
diff --git a/src/app/(main)/resources/reading/reading-page-categories.tsx b/src/app/(main)/resources/reading/reading-page-categories.tsx
new file mode 100644
index 0000000000..2ce2207968
--- /dev/null
+++ b/src/app/(main)/resources/reading/reading-page-categories.tsx
@@ -0,0 +1,43 @@
+import NewspaperIcon from "../assets/newspaper.svg?svgr"
+import WriteIcon from "../assets/write-note.svg?svgr"
+import BookIcon from "../assets/bookmark.svg?svgr"
+
+export const subcategories = [
+ "blogs-and-newsletters",
+ "individual-posts",
+ "books",
+] as const
+
+export type Subcategory = (typeof subcategories)[number]
+
+export type ReadingPageTab = Subcategory | "all"
+
+export const tabs: {
+ label: string
+ href: string
+ variant: ReadingPageTab
+ color: string
+ icon: React.ReactNode
+}[] = [
+ {
+ label: "Blogs & newsletters",
+ href: "/resources/reading/blogs-and-newsletters",
+ variant: "blogs-and-newsletters",
+ color: "hsl(var(--color-pri-base))",
+ icon: ,
+ },
+ {
+ label: "Individual posts",
+ href: "/resources/reading/individual-posts",
+ variant: "individual-posts",
+ color: "#FF8800",
+ icon: ,
+ },
+ {
+ label: "Books",
+ href: "/resources/reading/books",
+ variant: "books",
+ color: "#00C6AC",
+ icon: ,
+ },
+]
diff --git a/src/app/(main)/resources/reading/reading-page.tsx b/src/app/(main)/resources/reading/reading-page.tsx
new file mode 100644
index 0000000000..81d970a33d
--- /dev/null
+++ b/src/app/(main)/resources/reading/reading-page.tsx
@@ -0,0 +1,222 @@
+import Link from "next/link"
+import { notFound } from "next/navigation"
+import { clsx } from "clsx"
+
+import { NavbarFixed } from "@/components/navbar/navbar-fixed"
+import { Breadcrumbs } from "@/_design-system/breadcrumbs"
+import { readResources } from "@/resources/data"
+import { topics, type ResourceMetadata, type Topic } from "@/resources/types"
+import { LookingForMore } from "@/components/looking-for-more"
+import { KeepLearning } from "../keep-learning"
+import { Button } from "@/app/conf/_design-system/button"
+
+import { tabs, type ReadingPageTab } from "./reading-page-categories"
+
+import { ResourcesHero } from "../resources-hero"
+import { ReadingResourcesCard } from "./reading-resources-card"
+
+const topicSet = new Set(topics)
+
+const variants: Record<
+ ReadingPageTab,
+ {
+ title: string
+ description: string
+ eyebrow: string
+ filter: (resource: ResourceMetadata) => boolean
+ }
+> = {
+ all: {
+ title: "Reading Resources Library",
+ description:
+ "Browse reading materials to learn best practices and stay up to date with the ecosystem.",
+ eyebrow: "Reading resources",
+ filter: resource =>
+ resource.tags.some(
+ tag =>
+ tag === "blog-or-newsletter" || tag === "guide" || tag === "book",
+ ),
+ },
+ "blogs-and-newsletters": {
+ title: "Blogs & Newsletters",
+ description:
+ "Popular sources to learn and keep track of the GraphQL ecosystem.",
+ eyebrow: "Stay informed",
+ filter: resource => resource.tags.includes("blog-or-newsletter"),
+ },
+ "individual-posts": {
+ title: "Individual Posts",
+ description: "Notable posts from the community.",
+ eyebrow: "Deep dives",
+ filter: resource =>
+ resource.tags.some(
+ tag =>
+ tag === "guide" ||
+ (tag === "blog" && !resource.url.startsWith("/blog")),
+ ),
+ },
+ books: {
+ title: "Books",
+ description:
+ "Books to help you level up your GraphQL knowledge and practice.",
+ eyebrow: "Read and learn",
+ filter: resource => resource.tags.includes("book"),
+ },
+}
+
+export function readingMetadata(variant: ReadingPageTab) {
+ const config = variants[variant]
+ if (!config) return {}
+ return {
+ title: config.title,
+ description: config.description,
+ }
+}
+
+function hasTopicTag(resource: ResourceMetadata) {
+ return resource.tags.some(tag => topicSet.has(tag as Topic))
+}
+
+function uniqueByTitle(resources: ResourceMetadata[]) {
+ const seen = new Set()
+ return resources.filter(resource => {
+ const key = resource.title.trim().toLowerCase()
+ if (seen.has(key)) return false
+ seen.add(key)
+ return true
+ })
+}
+
+export async function ReadingLibraryPage({
+ variant,
+}: {
+ variant: ReadingPageTab
+}) {
+ const config = variants[variant]
+ if (!config) return notFound()
+
+ const resources = await readResources()
+ const filtered = uniqueByTitle(resources)
+ .filter(config.filter)
+ .sort((a, b) =>
+ a.title.localeCompare(b.title, "en", { sensitivity: "base" }),
+ )
+
+ const activePath = [
+ {
+ name: "Home",
+ route: "/",
+ },
+ {
+ name: "Resource Hub",
+ route: "/resources",
+ },
+ {
+ name: "Reading Resources Library",
+ route: "/resources/reading",
+ },
+ ].map(item => ({
+ ...item,
+ title: item.name,
+ type: "page",
+ children: [],
+ frontMatter: {},
+ }))
+
+ return (
+
+
+
+
+
+
+
+ Select a category
+
+
+ {tabs.map(tab => {
+ const active = tab.variant === variant
+ return (
+
+ {tab.label}
+
+ )
+ })}
+
+
+ {filtered.slice(0, 9).map(resource => (
+
+
+
+ ))}
+
+ {filtered.length > 9 && (
+
+ {/* we're using for SEO and Cmd+F support */}
+
+
+ Load more
+
+
+
+ {filtered.slice(6).map(resource => {
+ return (
+
+
+
+ )
+ })}
+
+
+ )}
+
+
+
+
+
+
+ )
+}
diff --git a/src/app/(main)/resources/reading/reading-resources-card.tsx b/src/app/(main)/resources/reading/reading-resources-card.tsx
new file mode 100644
index 0000000000..4c5d5a1daf
--- /dev/null
+++ b/src/app/(main)/resources/reading/reading-resources-card.tsx
@@ -0,0 +1,66 @@
+import type { ComponentType, SVGProps } from "react"
+
+import BookmarkIcon from "../assets/bookmark.svg?svgr"
+import InfoIcon from "@/app/conf/_design-system/pixelarticons/info.svg?svgr"
+import NotesIcon from "@/app/conf/_design-system/pixelarticons/notes.svg?svgr"
+import { ResourceHubCard } from "../resource-hub-card"
+import type { ResourceMetadata } from "@/resources/types"
+
+type CornerIcon = ComponentType>
+
+type ReadingKind = "book" | "blog-or-newsletter" | "blog" | "guide"
+
+const readingKindConfig: Record<
+ ReadingKind,
+ { label: string; color: string; Icon: CornerIcon }
+> = {
+ book: { label: "books", color: "#00C6AC", Icon: BookmarkIcon },
+ "blog-or-newsletter": {
+ label: "blogs & newsletters",
+ color: "hsl(var(--color-pri-base))",
+ Icon: NotesIcon,
+ },
+ blog: { label: "blog posts", color: "#FF8800", Icon: NotesIcon },
+ guide: { label: "guides", color: "#FF8800", Icon: InfoIcon },
+}
+
+function pickReadingKind(resource: ResourceMetadata): ReadingKind | undefined {
+ const candidates: ReadingKind[] = [
+ "book",
+ "blog-or-newsletter",
+ "guide",
+ "blog",
+ ]
+ return candidates.find((candidate): candidate is ReadingKind =>
+ resource.tags.includes(candidate),
+ )
+}
+
+export function ReadingResourcesCard({
+ resource,
+}: {
+ resource: ResourceMetadata
+}) {
+ const kind = pickReadingKind(resource)
+ const config = kind ? readingKindConfig[kind] : undefined
+
+ return (
+
+
+
+ ) : null
+ }
+ />
+ )
+}
diff --git a/src/app/(main)/resources/resource-hub-card.tsx b/src/app/(main)/resources/resource-hub-card.tsx
new file mode 100644
index 0000000000..dfa9948d51
--- /dev/null
+++ b/src/app/(main)/resources/resource-hub-card.tsx
@@ -0,0 +1,147 @@
+import Link from "next/link"
+import type { ReactNode } from "react"
+import { clsx } from "clsx"
+
+import ArrowDownIcon from "@/app/conf/_design-system/pixelarticons/arrow-down.svg?svgr"
+import ClockIcon from "@/app/conf/_design-system/pixelarticons/clock.svg?svgr"
+import { Tag } from "@/app/conf/_design-system/tag"
+import { Topic } from "@/resources/types"
+import { blogTagColors } from "@/components/blog-page/blog-tag-colors"
+
+export const tagColors: Record = {
+ ...blogTagColors,
+ backend: "#36C1A0",
+ "defies-categorization": "#894545",
+ "developer-experience": "#6fc9af",
+ "federation-and-composite-schemas": "#cbc749",
+ "graphql-clients": "#ca78fc",
+ "graphql-in-production": "#e4981f",
+ "graphql-security": "#CC6BB0",
+ "graphql-spec": "#6B73CC",
+ scaling: "#8D8D8D",
+ frontend: "violet",
+ documentation: "salmon",
+ "schema-evolution": "thistle",
+ security: "cornflowerblue",
+ "case-studies": "#B36B00",
+ "federation-and-distributed-systems": "#FF8F70",
+ federation: "#5C7CFA",
+ tools: "#0FA3B1",
+ "api-platform-and-gateways": "#F4B400",
+ "schema-design": "#7E57C2",
+ ai: "#FF5FA2",
+ monitoring: "#2D9CDB",
+ "blog-or-newsletter": "hsl(var(--color-pri-base))",
+ book: "#00C6AC",
+ guide: "#FF8800",
+}
+
+interface ResourceHubCardProps {
+ href: string
+ title: string
+ author?: string
+ duration?: string
+ authorPlacement?: "body" | "footer"
+ tags?: string[]
+ className?: string
+ icon?: ReactNode
+}
+
+export function ResourceHubCard({
+ href,
+ title,
+ author,
+ duration,
+ authorPlacement = "footer",
+ tags,
+ className,
+ icon,
+}: ResourceHubCardProps) {
+ return (
+
+
+
+ {tags?.length ? (
+
+ {tags.map(tag => (
+
+ {formatTag(tag)}
+
+ ))}
+
+ ) : null}
+
+ {authorPlacement === "body" && author ? (
+ {author}
+ ) : null}
+
100
+ ? "typography-body-lg"
+ : "typography-h4 md:typography-h3",
+ )}
+ >
+ {title}
+
+
+
+ {icon ? (
+
+ {icon}
+
+ ) : null}
+
+
+ {(authorPlacement === "footer" || !!duration) && (
+
+ {authorPlacement === "footer" && author ? (
+ {author}
+ ) : null}
+ {duration ? (
+
+
+ {duration}
+
+ ) : null}
+
+ )}
+
+
+
+ )
+}
+
+function formatTag(tag: string) {
+ if (tag === "blog-or-newsletter") return "Blogs & Newsletters"
+ if (tag === "book") return "Books"
+ if (tag === "guide") return "Individual posts"
+
+ return tag.replaceAll("-", " ")
+}
diff --git a/src/app/(main)/resources/resources-hero.tsx b/src/app/(main)/resources/resources-hero.tsx
new file mode 100644
index 0000000000..29ac61e94c
--- /dev/null
+++ b/src/app/(main)/resources/resources-hero.tsx
@@ -0,0 +1,28 @@
+import { LearnHeroStripes } from "@/components/learn-aggregator/learn-hero-stripes"
+
+export function ResourcesHero({
+ heading,
+ text,
+ children,
+}: {
+ heading: string
+ text: string
+ children?: React.ReactNode
+}) {
+ return (
+
+
+
+
{heading}
+
{text}
+ {children}
+
+
+ )
+}
diff --git a/src/app/(main)/resources/specification-section.tsx b/src/app/(main)/resources/specification-section.tsx
new file mode 100644
index 0000000000..45fc32edfe
--- /dev/null
+++ b/src/app/(main)/resources/specification-section.tsx
@@ -0,0 +1,54 @@
+import { Button } from "@/app/conf/_design-system/button"
+import ArchiveIcon from "./assets/archive.svg?svgr"
+import { StripesDecoration } from "@/app/conf/_design-system/stripes-decoration"
+import { Eyebrow } from "@/_design-system/eyebrow"
+
+export function SpecificationSection() {
+ return (
+
+
+
+
specification
+
+
+
+
+
+
+ Read the GraphQL Specification
+
+
+ The specification defines the core structure of GraphQL. It's the
+ foundation for building consistent servers, clients, and tools.
+ Read the spec to better understand how GraphQL works.
+
+
+ Go to specification
+
+
+
+
+
+ )
+}
+
+function Stripes() {
+ return (
+
+
+
+ )
+}
diff --git a/src/app/(main)/resources/subtitles.ts b/src/app/(main)/resources/subtitles.ts
new file mode 100644
index 0000000000..f546c51b99
--- /dev/null
+++ b/src/app/(main)/resources/subtitles.ts
@@ -0,0 +1,30 @@
+import { type Topic } from "@/resources/types"
+
+export const categoryNames: Record = {
+ frontend: "Frontend",
+ backend: "Backend",
+ federation: "Federation",
+ security: "Security",
+ ai: "AI",
+ monitoring: "Monitoring",
+ "api-platform-and-gateways": "API Platform and Gateways",
+ "developer-experience": "Developer Experience",
+ "schema-design": "Schema Design",
+ tools: "Tools",
+}
+
+export const categorySubtitles: Record = {
+ frontend: "Learn how to integrate GraphQL on the frontend.",
+ backend:
+ "Build powerful GraphQL backends with the right tools, libraries and expert insights.",
+ federation: "Learn how to build and compose GraphQL graphs with federation.",
+ ai: "Explore how to use GraphQL for AI systems.",
+ security: "Learn how to secure your GraphQL APIs.",
+ monitoring:
+ "Stay ahead of performance issues by monitoring queries and watching error trends.",
+ "api-platform-and-gateways":
+ "Learn how to build and deploy API Gateways and Supergraphs.",
+ "developer-experience": "Learn how to improve your developer experience.",
+ "schema-design": "Learn how to design and maintain GraphQL schemas.",
+ tools: "Discover the best tools for GraphQL development.",
+}
diff --git a/src/app/(main)/resources/tools-libraries-section.tsx b/src/app/(main)/resources/tools-libraries-section.tsx
new file mode 100644
index 0000000000..4c69c86096
--- /dev/null
+++ b/src/app/(main)/resources/tools-libraries-section.tsx
@@ -0,0 +1,34 @@
+import { Button } from "@/app/conf/_design-system/button"
+import ToolsIcon from "./assets/tools.svg?svgr"
+import { Eyebrow } from "@/_design-system/eyebrow"
+
+export function ToolsLibrariesSection() {
+ return (
+
+ )
+}
diff --git a/src/app/(main)/resources/video-resources-section.tsx b/src/app/(main)/resources/video-resources-section.tsx
new file mode 100644
index 0000000000..7816a8ff83
--- /dev/null
+++ b/src/app/(main)/resources/video-resources-section.tsx
@@ -0,0 +1,31 @@
+import { Button } from "@/app/conf/_design-system/button"
+import { Eyebrow } from "@/_design-system/eyebrow"
+import VideoPlayerIcon from "./assets/video-player.svg?svgr"
+
+export function VideoResourcesSection() {
+ return (
+
+ video resources library
+
+
+
+
+
+
+
+
+ Watch and learn GraphQL
+
+
+ Build your skills with featured videos from GraphQL Conf, global
+ meetups, and expert engineers — keeping you up to date in a
+ fast-moving ecosystem.
+
+
+ Go to Video Resources
+
+
+
+
+ )
+}
diff --git a/src/app/(main)/resources/video/page.tsx b/src/app/(main)/resources/video/page.tsx
new file mode 100644
index 0000000000..b845357422
--- /dev/null
+++ b/src/app/(main)/resources/video/page.tsx
@@ -0,0 +1,95 @@
+import { NavbarFixed } from "@/components/navbar/navbar-fixed"
+import { getResourcesByTag } from "@/resources/data"
+import { ResourcesHero } from "../resources-hero"
+import { VideoLibrary } from "./video-library"
+import { KeepLearning } from "../keep-learning"
+import { LookingForMore } from "@/components/looking-for-more"
+import { Breadcrumbs } from "@/_design-system/breadcrumbs"
+
+export const metadata = {
+ title: "Video Resources Library",
+ description:
+ "Expand your expertise with curated videos to help you master GraphQL and stay up to date with its evolving ecosystem.",
+}
+
+export default async function VideoResourcesPage() {
+ const resources = await getResourcesByTag("video")
+ const seen = new Set()
+ const unique = resources.filter(resource => {
+ const key = resource.title.trim().toLowerCase()
+ if (seen.has(key)) return false
+ seen.add(key)
+ return true
+ })
+ unique.sort((a, b) =>
+ a.title.localeCompare(b.title, "en", { sensitivity: "base" }),
+ )
+
+ return (
+
+
+
+
+
+
+
+
+ )
+}
diff --git a/src/app/(main)/resources/video/video-library.tsx b/src/app/(main)/resources/video/video-library.tsx
new file mode 100644
index 0000000000..b646764128
--- /dev/null
+++ b/src/app/(main)/resources/video/video-library.tsx
@@ -0,0 +1,222 @@
+"use client"
+
+import { useMemo, useState } from "react"
+import { clsx } from "clsx"
+
+import { Button } from "@/app/conf/_design-system/button"
+import { Tag } from "@/app/conf/_design-system/tag"
+import CaretDownIcon from "@/app/conf/_design-system/pixelarticons/caret-down.svg?svgr"
+import { type ResourceMetadata, topics, type Topic } from "@/resources/types"
+
+import { ResourceHubCard, tagColors } from "../resource-hub-card"
+
+interface VideoLibraryProps {
+ resources: ResourceMetadata[]
+ className?: string
+}
+
+type SortOrder = "az" | "za"
+
+export function VideoLibrary({ resources, className }: VideoLibraryProps) {
+ const [selectedTopics, setSelectedTopics] = useState([])
+ const [sortOrder, setSortOrder] = useState("az")
+
+ const topicOptions = useMemo(() => {
+ const allowed = new Set(topics)
+ const found = new Set()
+ resources.forEach(resource => {
+ resource.tags.forEach(tag => {
+ if (allowed.has(tag as Topic)) {
+ found.add(tag)
+ }
+ })
+ })
+ return Array.from(found).sort((a, b) =>
+ a.localeCompare(b, "en", { sensitivity: "base" }),
+ )
+ }, [resources])
+
+ const filtered = useMemo(() => {
+ const filteredByTopic =
+ selectedTopics.length === 0
+ ? resources
+ : resources.filter(resource =>
+ resource.tags.some(tag => selectedTopics.includes(tag)),
+ )
+
+ const sorted = [...filteredByTopic].sort((a, b) =>
+ sortOrder === "az"
+ ? a.title.localeCompare(b.title, "en", { sensitivity: "base" })
+ : b.title.localeCompare(a.title, "en", { sensitivity: "base" }),
+ )
+
+ return sorted
+ }, [resources, selectedTopics, sortOrder])
+
+ return (
+
+
+
+
+
+
+
+ Sort
+
+
+
+ setSortOrder(event.target.value as SortOrder)
+ }
+ className="typography-body-sm w-full appearance-none bg-transparent px-3 py-2 outline-none"
+ >
+ Title A–Z
+ Title Z–A
+
+
+
+
+
+
+
+
+ {filtered.slice(0, 6).map(resource => {
+ return (
+
+ tag !== "video")}
+ duration={resource.duration}
+ />
+
+ )
+ })}
+
+ {filtered.length > 6 && (
+
+ {/* we're using for SEO and Cmd+F support */}
+
+
+ Load more
+
+
+
+ {filtered.slice(6).map(resource => {
+ return (
+
+ tag !== "video")}
+ duration={resource.duration}
+ />
+
+ )
+ })}
+
+
+ )}
+
+ )
+}
+
+function TopicsFilter({
+ label,
+ options,
+ resources,
+ value,
+ onChange,
+}: {
+ label: string
+ options: string[]
+ resources: ResourceMetadata[]
+ value: string[]
+ onChange: (next: string[]) => void
+}) {
+ const topicCounts = useMemo(() => {
+ const counts: Record = {}
+ resources.forEach(resource => {
+ resource.tags.forEach(tag => {
+ if (options.includes(tag)) {
+ counts[tag] = (counts[tag] || 0) + 1
+ }
+ })
+ })
+ return counts
+ }, [resources, options])
+
+ const toggleTopic = (topic: string) => {
+ if (value.includes(topic)) {
+ onChange(value.filter(t => t !== topic))
+ } else {
+ onChange([...value, topic])
+ }
+ }
+
+ const hasSelection = value.length > 0
+
+ return (
+
+
+ {label}
+
+
+ {options.map((topic, i) => {
+ const isSelected = value.includes(topic)
+ const count = topicCounts[topic] || 0
+ return (
+
+ toggleTopic(topic)}
+ data-active={isSelected ? "" : undefined}
+ tabIndex={i === 0 ? 0 : -1}
+ className={clsx(
+ "gql-focus-visible -m-1 flex p-1 !outline-offset-0 ring-inset ring-neu-400 transition-opacity duration-75 hover:opacity-100 hover:ring dark:ring-neu-50",
+ hasSelection && !isSelected && "opacity-50",
+ )}
+ onKeyDown={arrowsMoveSideways}
+ >
+
+ {topic.replaceAll("-", " ")} ({count})
+
+
+
+ )
+ })}
+
+
+ )
+}
+
+function arrowsMoveSideways(event: React.KeyboardEvent) {
+ if (event.key === "ArrowLeft" || event.key === "ArrowRight") {
+ const target = event.currentTarget as HTMLElement
+ const sibling =
+ event.key === "ArrowLeft"
+ ? target.parentElement?.previousElementSibling?.querySelector("button")
+ : target.parentElement?.nextElementSibling?.querySelector("button")
+ if (sibling instanceof HTMLElement) {
+ sibling.focus()
+ }
+ }
+}
diff --git a/src/app/conf/_design-system/pixelarticons/caret-down.svg b/src/app/conf/_design-system/pixelarticons/caret-down.svg
index 428cdbbaaf..deb68afbea 100644
--- a/src/app/conf/_design-system/pixelarticons/caret-down.svg
+++ b/src/app/conf/_design-system/pixelarticons/caret-down.svg
@@ -3,12 +3,11 @@
width="25"
height="24"
viewBox="0 0 25 24"
- fill="none"
+ fill="#A0A88A"
>
diff --git a/src/app/conf/_design-system/stripes-decoration.tsx b/src/app/conf/_design-system/stripes-decoration.tsx
index 8d03825612..f895baeada 100644
--- a/src/app/conf/_design-system/stripes-decoration.tsx
+++ b/src/app/conf/_design-system/stripes-decoration.tsx
@@ -1,21 +1,27 @@
import clsx from "clsx"
const maskEven =
- "repeating-linear-gradient(to right, transparent, transparent var(--stripe-width), black var(--stripe-width), black calc(var(--stripe-width) * 2))"
+ "repeating-linear-gradient(var(--angle), transparent, transparent var(--stripe-width), black var(--stripe-width), black calc(var(--stripe-width) * 2))"
const maskOdd =
- "repeating-linear-gradient(to right, black, black var(--stripe-width), transparent var(--stripe-width), transparent calc(var(--stripe-width) * 2))"
+ "repeating-linear-gradient(var(--angle), black, black var(--stripe-width), transparent var(--stripe-width), transparent calc(var(--stripe-width) * 2))"
export interface StripesDecorationProps {
evenClassName?: string
oddClassName?: string
stripeWidth?: string
+ /**
+ * @default "90deg" to right,
+ * use "-90deg" to align with right side of the container
+ */
+ angle?: string
}
export function StripesDecoration({
stripeWidth = "12px",
evenClassName,
oddClassName,
+ angle = "90deg",
}: StripesDecorationProps) {
return (
<>
@@ -23,7 +29,10 @@ export function StripesDecoration({
= {
spec: "#00C6AC",
grants: "#84BD01",
"in-the-news": "#3F3A3D",
+ "developer-experience": "#6fc9af",
}
diff --git a/src/components/blog-page/blog-tags.tsx b/src/components/blog-page/blog-tags.tsx
index 6c1a509535..618aa8282f 100644
--- a/src/components/blog-page/blog-tags.tsx
+++ b/src/components/blog-page/blog-tags.tsx
@@ -35,7 +35,7 @@ export function BlogTags({
key={tag}
// yes, the page lives under /tags, not /blog/tags
href={`/tags/${tag}`}
- className="-m-1 flex p-1 ring-inset ring-neu-400 transition-opacity duration-75 hover:ring focus:!outline-offset-0 dark:ring-neu-50 [:has(>:hover)>&:not(:hover)]:opacity-70"
+ className="gql-focus-visible -m-1 flex p-1 ring-inset ring-neu-400 transition-opacity duration-75 hover:ring focus:!outline-offset-0 dark:ring-neu-50 [:has(>:hover)>&:not(:hover)]:opacity-70"
>
{tagElement}
diff --git a/src/components/blog-page/featured-blog-posts.tsx b/src/components/blog-page/featured-blog-posts.tsx
index fc6c7f08cc..d017dfd39b 100644
--- a/src/components/blog-page/featured-blog-posts.tsx
+++ b/src/components/blog-page/featured-blog-posts.tsx
@@ -47,7 +47,7 @@ export function FeaturedBlogPosts({
byline={firstFeatured.frontMatter.byline}
date={firstFeatured.frontMatter.date}
/>
-
+
diff --git a/src/components/blog-page/index.tsx b/src/components/blog-page/index.tsx
index 0d597d3b7e..4f11d56fe2 100644
--- a/src/components/blog-page/index.tsx
+++ b/src/components/blog-page/index.tsx
@@ -5,9 +5,10 @@ import { Tag } from "@/app/conf/_design-system/tag"
import { arrowsMoveSideways } from "@/app/conf/_design-system/utils/arrows-move-sideways"
import { StripesDecoration } from "@/app/conf/_design-system/stripes-decoration"
+import { LookingForMore } from "@/components/looking-for-more"
+
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"
@@ -51,14 +52,14 @@ export function BlogPage({
-
+
{currentTag || "All Posts"}
Categories
-
+
{Object.entries(tags)
.sort((a, b) => b[1] - a[1])
.map(([tag, count], i) => (
@@ -78,7 +79,7 @@ export function BlogPage({
-
+
{blogs.map(
page =>
(!currentTag || page.frontMatter.tags.includes(currentTag)) && (
@@ -87,7 +88,13 @@ export function BlogPage({
)}
-
+
)
diff --git a/src/components/blog-page/looking-for-more.tsx b/src/components/blog-page/looking-for-more.tsx
deleted file mode 100644
index 2441ade4e7..0000000000
--- a/src/components/blog-page/looking-for-more.tsx
+++ /dev/null
@@ -1,36 +0,0 @@
-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/learn-aggregator/learn-hero-stripes.tsx b/src/components/learn-aggregator/learn-hero-stripes.tsx
index 29d2a4e37f..b7692117a3 100644
--- a/src/components/learn-aggregator/learn-hero-stripes.tsx
+++ b/src/components/learn-aggregator/learn-hero-stripes.tsx
@@ -1,13 +1,24 @@
+import { clsx } from "clsx"
import { StripesDecoration } from "@/app/conf/_design-system/stripes-decoration"
import blurBean from "./learn-blur-bean.webp"
-export function LearnHeroStripes() {
+export function LearnHeroStripes({
+ className,
+ style,
+ ...rest
+}: {
+ className?: string
+ style?: React.CSSProperties
+}) {
return (
) {
- return (
-
-
-
-
Looking for more?
-
- Learning is just the beginning. Discover tools and other resources —
- or connect with the GraphQL community around the world.
-
-
-
-
-
- )
-}
diff --git a/src/components/looking-for-more.tsx b/src/components/looking-for-more.tsx
new file mode 100644
index 0000000000..db174b300d
--- /dev/null
+++ b/src/components/looking-for-more.tsx
@@ -0,0 +1,46 @@
+import { clsx } from "clsx"
+import { Anchor } from "@/app/conf/_design-system/anchor"
+import ArrowDownIcon from "@/app/conf/_design-system/pixelarticons/arrow-down.svg?svgr"
+
+type LinkItem = { href: string; label: string }
+
+interface LookingForMoreProps extends React.HTMLAttributes {
+ description: string
+ links: [LinkItem, LinkItem]
+}
+
+export function LookingForMore({
+ description,
+ links,
+ ...props
+}: LookingForMoreProps) {
+ return (
+
+
+
+
Looking for more?
+
{description}
+
+
+
+ {links[0].label}
+
+
+
+ {links[1].label}
+
+
+
+
+
+ )
+}
diff --git a/src/components/nav-links/arrow-nav-links.tsx b/src/components/nav-links/arrow-nav-links.tsx
index 9e70684c1f..6563ad511b 100644
--- a/src/components/nav-links/arrow-nav-links.tsx
+++ b/src/components/nav-links/arrow-nav-links.tsx
@@ -36,7 +36,7 @@ export function ArrowNavLinks({
title={prev.title}
className="gql-focus-visible typography-link flex max-w-[50%] items-center gap-2 border border-neu-200 pr-2 text-left text-base no-underline hover:bg-neu-50 hover:ring hover:ring-neu-100 dark:border-neu-100 dark:hover:bg-neu-50/50 dark:hover:ring-neu-50"
>
-
+
{prev.title}
@@ -49,7 +49,7 @@ export function ArrowNavLinks({
className="gql-focus-visible typography-link ml-auto flex max-w-[50%] items-center gap-2 border border-neu-200 pl-2 text-left text-base no-underline hover:bg-neu-50 hover:ring hover:ring-neu-100 dark:border-neu-100 dark:hover:bg-neu-50/50 dark:hover:ring-neu-50"
>
{next.title}
-
+
diff --git a/src/components/navbar/navbar.tsx b/src/components/navbar/navbar.tsx
index d0e9d8fcbd..64b826850e 100644
--- a/src/components/navbar/navbar.tsx
+++ b/src/components/navbar/navbar.tsx
@@ -18,6 +18,8 @@ import { Flexsearch } from "../flexsearch"
import { NavLink, navLinkClasses } from "./nav-link"
import { useMenu } from "../use-menu"
+import ArrowDownIcon from "@/app/conf/_design-system/pixelarticons/arrow-down.svg?svgr"
+
type Item = normalizePages.PageItem | normalizePages.MenuItem
export interface NavBarProps {
items: Item[]
@@ -33,6 +35,7 @@ function NavbarMenu({
const routes = Object.fromEntries(
(menu.children || []).map(route => [route.name, route]),
)
+
return (
- {Object.entries(menu.items || {}).map(([key, item]) => (
- ,
- state: NavigationMenu.Link.State,
- ) => (
-
-
- {item.title}
-
-
- )}
- />
- ))}
+ {Object.entries(menu.items || {}).map(([key, item]) => {
+ if (typeof item === "string") item = { title: item }
+ if (!item.title) item.title = key
+ const href =
+ item.href ||
+ routes[key]?.route ||
+ (key === "index" ? menu.route : `${menu.route}/${key}`)
+
+ if (key === "index") {
+ return (
+ ,
+ state: NavigationMenu.Link.State,
+ ) => (
+
+
+ Explore {menu.title}
+
+
+
+ )}
+ />
+ )
+ }
+
+ return (
+ ,
+ state: NavigationMenu.Link.State,
+ ) => (
+
+
+ {item.title}
+
+
+ )}
+ />
+ )
+ })}
)
diff --git a/src/components/sidebar/index.tsx b/src/components/sidebar/index.tsx
index bd126b6d5e..d4917affee 100644
--- a/src/components/sidebar/index.tsx
+++ b/src/components/sidebar/index.tsx
@@ -31,6 +31,7 @@ import {
import ArrowBarLeft from "@/app/conf/_design-system/pixelarticons/arrow-bar-left.svg?svgr"
import { Anchor } from "@/app/conf/_design-system/anchor"
+import ArrowDownIcon from "@/app/conf/_design-system/pixelarticons/arrow-down.svg?svgr"
import { renderComponent } from "../utils/render-component"
import { ThemeSwitch } from "../theme-switch"
@@ -57,7 +58,7 @@ const Folder = memo(function FolderInner(props: FolderProps) {
const classes = {
link: cn(
- "flex px-2 py-1.5 text-sm transition-colors [word-break:break-word]",
+ "flex px-2 capitalize py-1.5 text-sm transition-colors [word-break:break-word]",
"cursor-pointer contrast-more:border contrast-more:hover:underline gql-focus-visible focus-visible:outline-offset-1",
),
inactive: cn(
@@ -143,10 +144,16 @@ function FolderImpl({ item, anchors, onFocus }: FolderProps): ReactElement {
(menu.children || []).map(route => [route.name, route]),
)
item.children = Object.entries(menu.items || {}).map(([key, item]) => {
+ if (typeof item === "string") item = { title: item }
+ if (!item.title) item.title = key
+
const route = routes[key] || {
name: key,
route: menu.route + "/" + key,
}
+
+ if (key === "index") route.route = menu.route
+
return {
...route,
...item,
@@ -273,13 +280,24 @@ function File({
{
setMenu(false)
}}
onFocus={onFocus}
>
- {item.title}
+ {item.name !== "index" ? (
+ item.title
+ ) : (
+ <>
+ Explore {item.title}
+
+ >
+ )}
{active && anchors.length > 0 && (
diff --git a/src/components/toc-hero/toc-hero-contents.tsx b/src/components/toc-hero/toc-hero-contents.tsx
index 84f060efb3..bbdd207a85 100644
--- a/src/components/toc-hero/toc-hero-contents.tsx
+++ b/src/components/toc-hero/toc-hero-contents.tsx
@@ -1,6 +1,7 @@
import { clsx } from "clsx"
import { ChevronRight } from "@/app/conf/_design-system/pixelarticons/chevron-right"
+import { slugify } from "@/app/(main)/resources/[category]/texts"
export interface TocHeroContentsProps
extends React.HTMLAttributes {
@@ -34,7 +35,7 @@ export function TocHeroContents({
typeof section === "string"
? {
name: section,
- href: `#${section.toLowerCase().replace(/ /g, "-")}`,
+ href: `#${slugify(section)}`,
}
: section
diff --git a/src/env.d.ts b/src/env.d.ts
index a5309effaf..dced5d24c3 100644
--- a/src/env.d.ts
+++ b/src/env.d.ts
@@ -17,6 +17,11 @@ declare module "*?raw" {
export default content
}
+declare module "*.svg?resource" {
+ const url: string
+ export default url
+}
+
// We're importing a transitive dependency to avoid a bug.
declare module "next-themes" {
export function ThemeProvider(props: {
diff --git a/src/pages/_meta.tsx b/src/pages/_meta.tsx
index 115fb056f2..37511d0d8b 100644
--- a/src/pages/_meta.tsx
+++ b/src/pages/_meta.tsx
@@ -10,17 +10,34 @@ export default {
type: "page",
title: "Learn",
},
+ resources: {
+ type: "menu",
+ title: "Resource Hub",
+ route: "/resources",
+ items: {
+ index: "Resource Hub",
+ frontend: "",
+ backend: "",
+ federation: "",
+ ai: "AI",
+ security: "",
+ monitoring: "",
+ "tools-and-libraries": "Tools & Libraries",
+ spec: {
+ type: "page",
+ title: "Specification",
+ href: "https://spec.graphql.org",
+ newWindow: true,
+ },
+ video: "Video Resources Library",
+ reading: "Reading Resources Library",
+ },
+ },
community: {
type: "menu",
title: "Community",
items: {
- "tools-and-libraries": {
- title: "Tools and Libraries",
- },
- resources: {
- title: "Resources",
- href: "/community/resources/official-channels",
- },
+ // TODO: Set a redirect from /community/tools-and-libraries to /resources/tools-and-libraries
events: {
title: "Events",
type: "page",
@@ -35,15 +52,9 @@ export default {
},
},
faq: {
- type: "page",
+ type: "hidden",
title: "FAQ",
},
- spec: {
- type: "page",
- title: "Spec",
- href: "https://spec.graphql.org",
- newWindow: true,
- },
blog: {
type: "page",
title: "Blog",
diff --git a/src/pages/community/resources/blogs-and-newsletters.mdx b/src/pages/community/resources/blogs-and-newsletters.mdx
index 46331f19e2..1927c4b4ff 100644
--- a/src/pages/community/resources/blogs-and-newsletters.mdx
+++ b/src/pages/community/resources/blogs-and-newsletters.mdx
@@ -35,7 +35,6 @@ Here are a list of notable blog posts to help you better understand GraphQL:
- [Your First GraphQL Server](https://medium.com/the-graphqlhub/your-first-graphql-server-3c766ab4f0a2#.ovn0y19k4) - Clay Allsopp
- [Tutorial: Kick start a JS API with Apollo-server, Dataloader and Knex](https://bamtech.gitbook.io/dev-standards/backend/graphql-js/getting-started-with-apollo-server-dataloader-knex.mo) - Thomas Pucci
- [Tutorial: How to Build a GraphQL Server](https://medium.com/apollo-stack/tutorial-building-a-graphql-server-cddaa023c035#.bu6sdnst4) - Jonas Helfer
-- [Designing Powerful APIs with GraphQL Query Parameters](https://www.graph.cool/docs/tutorials/designing-powerful-apis-with-graphql-query-parameters-aing7uech3/) - Johannes Schickling
- [GraphQL and the amazing Apollo Client](https://medium.com/google-developer-experts/graphql-and-the-amazing-apollo-client-fe57e162a70c) - Gerard Sans
- [GraphQL Server Basics (Part I): The Schema](https://www.prisma.io/blog/graphql-server-basics-the-schema-ac5e2950214e) - Nikolas Burk
- [GraphQL Server Basics (Part II): The Network Layer](https://www.prisma.io/blog/graphql-server-basics-the-network-layer-51d97d21861) - Nikolas Burk
diff --git a/src/pages/learn/index.mdx b/src/pages/learn/index.mdx
index c26ab6d58e..a121f17c81 100644
--- a/src/pages/learn/index.mdx
+++ b/src/pages/learn/index.mdx
@@ -11,7 +11,7 @@ import { LearnHeroStripes } from '../../components/learn-aggregator/learn-hero-s
import { pagesBySection } from '../../components/learn-aggregator/learn-pages'
import { CommonQuestionsSection } from '../../components/learn-aggregator/common-questions'
import { TrainingCoursesSection } from '../../components/learn-aggregator/training-courses'
-import { LookingForMore } from "../../components/learn-aggregator/looking-for-more"
+import { LookingForMore } from "../../components/looking-for-more"
-
+
+
diff --git a/src/resources/data.ts b/src/resources/data.ts
new file mode 100644
index 0000000000..db37f4d1b8
--- /dev/null
+++ b/src/resources/data.ts
@@ -0,0 +1,127 @@
+import path from "node:path"
+import { glob } from "node:fs/promises"
+import { readFile } from "node:fs/promises"
+import { cache } from "react"
+import matter from "gray-matter"
+
+import { ResourceMetadata, type ResourceTag, topics } from "./types"
+
+const dataGlob = "src/resources/data/*.json"
+const codeGlob = "src/code/**/*.md"
+const blogGlob = "src/pages/blog/**/*.mdx"
+
+export const readResources = cache(async () => {
+ const resources: ResourceMetadata[] = []
+
+ for await (const file of glob(dataGlob)) {
+ const raw = await readFile(file, "utf8")
+ const parsed = JSON.parse(raw)
+ resources.push(ResourceMetadata.assert(parsed))
+ }
+
+ for await (const file of glob(blogGlob)) {
+ const raw = await readFile(file, "utf8")
+ const { data, content } = matter(raw)
+
+ const title: string | undefined = data.title
+ if (!title) continue
+
+ const slug = blogSlug(file)
+
+ const bodyLines = content
+ .split(/\r?\n/)
+ .map(line => line.trim())
+ .map(line =>
+ line
+ .replace(/!\[[^\]]*\]\([^)]+\)/g, "")
+ .replace(/\[([^\]]+)\]\([^)]+\)/g, "$1")
+ .replace(/`+/g, "")
+ .replace(/[*_~]+/g, "")
+ .replace(/^#+\s*/, "")
+ .replace(/<\/?[^>]+>/g, "")
+ .trim(),
+ )
+ .filter(line => line.length > 0)
+
+ const excerpt = bodyLines.slice(0, 2).join(" ")
+
+ const description =
+ typeof data.description === "string" && data.description.length > 0
+ ? data.description
+ : excerpt || undefined
+
+ const topicsFromFrontmatter: ResourceTag[] = Array.isArray(data.topics)
+ ? data.topics.filter((tag): tag is ResourceTag =>
+ topics.includes(tag as (typeof topics)[number]),
+ )
+ : []
+
+ const topicTagsFromTags: ResourceTag[] = Array.isArray(data.tags)
+ ? data.tags.filter((tag): tag is ResourceTag =>
+ topics.includes(tag as (typeof topics)[number]),
+ )
+ : []
+
+ const tags: ResourceTag[] = [
+ "blog",
+ ...topicsFromFrontmatter,
+ ...topicTagsFromTags,
+ ]
+
+ resources.push(
+ ResourceMetadata.assert({
+ title,
+ url: slug,
+ author: data.byline,
+ description,
+ kind: "blog",
+ tags,
+ }),
+ )
+ }
+
+ for await (const file of glob(codeGlob)) {
+ const raw = await readFile(file, "utf8")
+ const { data } = matter(raw)
+ const tags: ResourceMetadata["tags"] = Array.isArray(data.tags)
+ ? data.tags
+ : []
+
+ if (!tags.includes("tools-and-libraries")) {
+ tags.push("tools-and-libraries")
+ }
+
+ const url: string | undefined =
+ data.url ??
+ (data.github ? `https://github.com/${data.github}` : undefined) ??
+ (data.npm ? `https://npmjs.com/package/${data.npm}` : undefined)
+
+ const title = data.name ?? path.parse(file).name
+
+ resources.push(
+ ResourceMetadata.assert({
+ title,
+ url,
+ description: data.description,
+ tags,
+ }),
+ )
+ }
+
+ return resources
+})
+
+export async function getResourcesByTag(tag: ResourceTag) {
+ const resources = await readResources()
+ return resources.filter(resource => resource.tags.includes(tag))
+}
+
+function blogSlug(file: string) {
+ const relative = path.relative("src/pages", file)
+ const withoutExt = relative.replace(/\.mdx$/, "")
+ const normalized = withoutExt.split(path.sep).join("/")
+ const clean = normalized.endsWith("/index")
+ ? normalized.slice(0, -"index".length - 1)
+ : normalized
+ return `/${clean}`
+}
diff --git a/src/resources/data/a-beginner-s-guide-to-graphql.json b/src/resources/data/a-beginner-s-guide-to-graphql.json
new file mode 100644
index 0000000000..2552c02efd
--- /dev/null
+++ b/src/resources/data/a-beginner-s-guide-to-graphql.json
@@ -0,0 +1,6 @@
+{
+ "title": "A Beginner’s Guide to GraphQL",
+ "url": "https://www.freecodecamp.org/news/a-beginners-guide-to-graphql-86f849ce1bec/",
+ "author": "Leonardo Maldonado",
+ "tags": ["blog"]
+}
diff --git a/src/resources/data/a-graphql-framework-for-non-js-servers-syrus-akbary.json b/src/resources/data/a-graphql-framework-for-non-js-servers-syrus-akbary.json
new file mode 100644
index 0000000000..aac5f9768a
--- /dev/null
+++ b/src/resources/data/a-graphql-framework-for-non-js-servers-syrus-akbary.json
@@ -0,0 +1,7 @@
+{
+ "title": "A GraphQL Framework for Non-JS Servers",
+ "author": "Syrus Akbary",
+ "url": "https://www.youtube.com/watch?v=RNoyPSrQyPs",
+ "tags": ["video", "backend"],
+ "duration": "21:32"
+}
diff --git a/src/resources/data/a-postgresql-backed-graphql-baas.json b/src/resources/data/a-postgresql-backed-graphql-baas.json
new file mode 100644
index 0000000000..eacf30f518
--- /dev/null
+++ b/src/resources/data/a-postgresql-backed-graphql-baas.json
@@ -0,0 +1,7 @@
+{
+ "title": "A PostgreSQL backed GraphQL BaaS",
+ "author": "Tanmai Gopal",
+ "url": "https://www.youtube.com/watch?v=neIZcc8y3B0",
+ "tags": ["video", "backend"],
+ "duration": "14:27"
+}
diff --git a/src/resources/data/all-talks-from-graphql-europe.json b/src/resources/data/all-talks-from-graphql-europe.json
new file mode 100644
index 0000000000..6ab2b1477e
--- /dev/null
+++ b/src/resources/data/all-talks-from-graphql-europe.json
@@ -0,0 +1,5 @@
+{
+ "title": "All Talks from GraphQL Europe",
+ "url": "https://www.youtube.com/playlist?list=PLn2e1F9Rfr6n_WFm9fPE-_wYPrYvSTySt",
+ "tags": ["video", "frontend", "backend"]
+}
diff --git a/src/resources/data/apollo-client-put-graphql-data-in-your-ui.json b/src/resources/data/apollo-client-put-graphql-data-in-your-ui.json
new file mode 100644
index 0000000000..bfa3fdd38f
--- /dev/null
+++ b/src/resources/data/apollo-client-put-graphql-data-in-your-ui.json
@@ -0,0 +1,7 @@
+{
+ "title": "Apollo Client: Put GraphQL Data in Your UI",
+ "author": "Sashko Stubailo",
+ "url": "https://www.youtube.com/watch?v=u1E0CbGeICo",
+ "tags": ["video", "frontend"],
+ "duration": "18:27"
+}
diff --git a/src/resources/data/apollo-odyssey.json b/src/resources/data/apollo-odyssey.json
new file mode 100644
index 0000000000..aa11a294bd
--- /dev/null
+++ b/src/resources/data/apollo-odyssey.json
@@ -0,0 +1,5 @@
+{
+ "title": "Apollo Odyssey",
+ "url": "https://apollographql.com/tutorials",
+ "tags": ["guide"]
+}
diff --git a/src/resources/data/apollo-s-blog.json b/src/resources/data/apollo-s-blog.json
new file mode 100644
index 0000000000..e2b5979181
--- /dev/null
+++ b/src/resources/data/apollo-s-blog.json
@@ -0,0 +1,5 @@
+{
+ "title": "Apollo's Blog",
+ "url": "https://apollographql.com/blog",
+ "tags": ["blog-or-newsletter"]
+}
diff --git a/src/resources/data/architecture-of-a-high-performance-graphql-to-sql-engine.json b/src/resources/data/architecture-of-a-high-performance-graphql-to-sql-engine.json
new file mode 100644
index 0000000000..f107cf7911
--- /dev/null
+++ b/src/resources/data/architecture-of-a-high-performance-graphql-to-sql-engine.json
@@ -0,0 +1,6 @@
+{
+ "title": "Architecture of a high performance GraphQL to SQL engine",
+ "url": "https://blog.hasura.io/architecture-of-a-high-performance-graphql-to-sql-server-58d9944b8a87",
+ "author": "Sandip Devarkonda",
+ "tags": ["blog"]
+}
diff --git a/src/resources/data/awesome-graphql.json b/src/resources/data/awesome-graphql.json
new file mode 100644
index 0000000000..0befab9e45
--- /dev/null
+++ b/src/resources/data/awesome-graphql.json
@@ -0,0 +1,5 @@
+{
+ "title": "awesome-graphql",
+ "url": "https://github.com/chentsulin/awesome-graphql",
+ "tags": ["guide"]
+}
diff --git a/src/resources/data/brand-guidelines.json b/src/resources/data/brand-guidelines.json
new file mode 100644
index 0000000000..f32f7804f8
--- /dev/null
+++ b/src/resources/data/brand-guidelines.json
@@ -0,0 +1,5 @@
+{
+ "title": "brand guidelines",
+ "url": "/brand",
+ "tags": ["guide"]
+}
diff --git a/src/resources/data/build-a-full-graphql-backend-in-under-5-minutes-michael-paris.json b/src/resources/data/build-a-full-graphql-backend-in-under-5-minutes-michael-paris.json
new file mode 100644
index 0000000000..7fc5936027
--- /dev/null
+++ b/src/resources/data/build-a-full-graphql-backend-in-under-5-minutes-michael-paris.json
@@ -0,0 +1,7 @@
+{
+ "title": "Build a Full GraphQL Backend in Under 5 Minutes",
+ "author": "Michael Paris",
+ "url": "https://www.youtube.com/watch?v=bJ8pnYd6jPQ",
+ "tags": ["video", "backend"],
+ "duration": "4:55"
+}
diff --git a/src/resources/data/build-a-graphql-server-for-node-js-using-postgresql-mysql-lee-benson.json b/src/resources/data/build-a-graphql-server-for-node-js-using-postgresql-mysql-lee-benson.json
new file mode 100644
index 0000000000..d48a5744c9
--- /dev/null
+++ b/src/resources/data/build-a-graphql-server-for-node-js-using-postgresql-mysql-lee-benson.json
@@ -0,0 +1,7 @@
+{
+ "title": "Build a GraphQL server for Node.js, using PostgreSQL/MySQL",
+ "author": "Lee Benson",
+ "url": "https://www.youtube.com/watch?v=DNPVqK_woRQ",
+ "tags": ["video", "backend"],
+ "duration": "45:04"
+}
diff --git a/src/resources/data/building-native-mobile-apps-with-graphql-martjin-walraven-react-europe-2016.json b/src/resources/data/building-native-mobile-apps-with-graphql-martjin-walraven-react-europe-2016.json
new file mode 100644
index 0000000000..948a1ff738
--- /dev/null
+++ b/src/resources/data/building-native-mobile-apps-with-graphql-martjin-walraven-react-europe-2016.json
@@ -0,0 +1,7 @@
+{
+ "title": "Building Native Mobile Apps with GraphQL",
+ "author": "Martjin Walraven",
+ "url": "https://www.youtube.com/watch?v=z5rz3saDPJ8",
+ "tags": ["video", "frontend"],
+ "duration": "23:55"
+}
diff --git a/src/resources/data/building-the-f8-app-using-graphql-relay.json b/src/resources/data/building-the-f8-app-using-graphql-relay.json
new file mode 100644
index 0000000000..3c36df9045
--- /dev/null
+++ b/src/resources/data/building-the-f8-app-using-graphql-relay.json
@@ -0,0 +1,5 @@
+{
+ "title": "Building the f8 App: Using GraphQL & Relay",
+ "url": "http://makeitopen.com/docs/en/1-A2-relay.html",
+ "tags": ["blog"]
+}
diff --git a/src/resources/data/chillicream-s-blog.json b/src/resources/data/chillicream-s-blog.json
new file mode 100644
index 0000000000..68a2657d38
--- /dev/null
+++ b/src/resources/data/chillicream-s-blog.json
@@ -0,0 +1,5 @@
+{
+ "title": "ChilliCream's Blog",
+ "url": "https://chillicream.com/blog",
+ "tags": ["blog-or-newsletter"]
+}
diff --git a/src/resources/data/community-events-section.json b/src/resources/data/community-events-section.json
new file mode 100644
index 0000000000..2c3af619b8
--- /dev/null
+++ b/src/resources/data/community-events-section.json
@@ -0,0 +1,5 @@
+{
+ "title": "community events section",
+ "url": "/community/upcoming-events/#meetups",
+ "tags": ["guide"]
+}
diff --git a/src/resources/data/craft-graphql-apis-in-elixir-with-absinthe.json b/src/resources/data/craft-graphql-apis-in-elixir-with-absinthe.json
new file mode 100644
index 0000000000..b8c5cf6344
--- /dev/null
+++ b/src/resources/data/craft-graphql-apis-in-elixir-with-absinthe.json
@@ -0,0 +1,6 @@
+{
+ "title": "Craft GraphQL APIs in Elixir with Absinthe",
+ "url": "https://pragprog.com/titles/wwgraphql/craft-graphql-apis-in-elixir-with-absinthe/",
+ "author": "Bruce Williams & Ben Wilson",
+ "tags": ["book"]
+}
diff --git a/src/resources/data/designing-powerful-apis-with-graphql-query-parameters.json b/src/resources/data/designing-powerful-apis-with-graphql-query-parameters.json
new file mode 100644
index 0000000000..d3f99c4a56
--- /dev/null
+++ b/src/resources/data/designing-powerful-apis-with-graphql-query-parameters.json
@@ -0,0 +1,5 @@
+{
+ "title": "Designing Powerful APIs with GraphQL Query Parameters",
+ "url": "https://www.graph.cool/docs/tutorials/designing-powerful-apis-with-graphql-query-parameters-aing7uech3/",
+ "tags": ["blog"]
+}
diff --git a/src/resources/data/dev-to-graphql-tag.json b/src/resources/data/dev-to-graphql-tag.json
new file mode 100644
index 0000000000..7ce2241769
--- /dev/null
+++ b/src/resources/data/dev-to-graphql-tag.json
@@ -0,0 +1,5 @@
+{
+ "title": "DEV.to GraphQL tag",
+ "url": "https://dev.to/t/graphql",
+ "tags": ["blog-or-newsletter"]
+}
diff --git a/src/resources/data/development-of-real-time-apps-with-graphql-node-js-vince-ning-michael-paris-sf-node-meetup-february-2017.json b/src/resources/data/development-of-real-time-apps-with-graphql-node-js-vince-ning-michael-paris-sf-node-meetup-february-2017.json
new file mode 100644
index 0000000000..b8d5ed3bce
--- /dev/null
+++ b/src/resources/data/development-of-real-time-apps-with-graphql-node-js-vince-ning-michael-paris-sf-node-meetup-february-2017.json
@@ -0,0 +1,7 @@
+{
+ "title": "Development of real-time apps with GraphQL Node.js",
+ "author": "Vince Ning & Michael Paris",
+ "url": "https://youtu.be/yh_A6CEqsSM",
+ "tags": ["video", "backend"],
+ "duration": "22:33"
+}
diff --git a/src/resources/data/escape-security-blog.json b/src/resources/data/escape-security-blog.json
new file mode 100644
index 0000000000..a0190d21a3
--- /dev/null
+++ b/src/resources/data/escape-security-blog.json
@@ -0,0 +1,5 @@
+{
+ "title": "Escape Security Blog",
+ "url": "https://escape.tech/blog",
+ "tags": ["blog-or-newsletter"]
+}
diff --git a/src/resources/data/exploring-graphql.json b/src/resources/data/exploring-graphql.json
new file mode 100644
index 0000000000..1729969bb2
--- /dev/null
+++ b/src/resources/data/exploring-graphql.json
@@ -0,0 +1,7 @@
+{
+ "title": "Exploring GraphQL",
+ "url": "https://youtube.com/watch?v=WQLzZf34FJ8",
+ "author": "Lee Byron",
+ "tags": ["video"],
+ "duration": "27:18"
+}
diff --git a/src/resources/data/from-rest-to-graphql.json b/src/resources/data/from-rest-to-graphql.json
new file mode 100644
index 0000000000..b672e423aa
--- /dev/null
+++ b/src/resources/data/from-rest-to-graphql.json
@@ -0,0 +1,6 @@
+{
+ "title": "From REST to GraphQL",
+ "url": "https://0x2a.sh/from-rest-to-graphql-b4e95e94c26b#.tag7nzkrb",
+ "author": "Garen J. Torikian",
+ "tags": ["blog"]
+}
diff --git a/src/resources/data/from-zero-to-graphql-in-30-minutes.json b/src/resources/data/from-zero-to-graphql-in-30-minutes.json
new file mode 100644
index 0000000000..3460116995
--- /dev/null
+++ b/src/resources/data/from-zero-to-graphql-in-30-minutes.json
@@ -0,0 +1,7 @@
+{
+ "title": "From Zero to GraphQL in 30 Minutes",
+ "url": "https://youtube.com/watch?v=UBGzsb2UkeY",
+ "author": "Steven Luscher",
+ "tags": ["video"],
+ "duration": "30:13"
+}
diff --git a/src/resources/data/fullstack-graphql.json b/src/resources/data/fullstack-graphql.json
new file mode 100644
index 0000000000..0f163bf5ec
--- /dev/null
+++ b/src/resources/data/fullstack-graphql.json
@@ -0,0 +1,6 @@
+{
+ "title": "Fullstack GraphQL",
+ "url": "https://www.graphqladmin.com/books/fullstack-graphql",
+ "author": "Julian Mayorga",
+ "tags": ["book"]
+}
diff --git a/src/resources/data/graphql-and-the-amazing-apollo-client.json b/src/resources/data/graphql-and-the-amazing-apollo-client.json
new file mode 100644
index 0000000000..e74ff64ec1
--- /dev/null
+++ b/src/resources/data/graphql-and-the-amazing-apollo-client.json
@@ -0,0 +1,6 @@
+{
+ "title": "GraphQL and the amazing Apollo Client",
+ "url": "https://medium.com/google-developer-experts/graphql-and-the-amazing-apollo-client-fe57e162a70c",
+ "author": "Gerard Sans",
+ "tags": ["blog"]
+}
diff --git a/src/resources/data/graphql-apis.json b/src/resources/data/graphql-apis.json
new file mode 100644
index 0000000000..6c7365fdae
--- /dev/null
+++ b/src/resources/data/graphql-apis.json
@@ -0,0 +1,5 @@
+{
+ "title": "graphql-apis",
+ "url": "https://github.com/APIs-guru/graphql-apis",
+ "tags": ["guide"]
+}
diff --git a/src/resources/data/graphql-at-facebook.json b/src/resources/data/graphql-at-facebook.json
new file mode 100644
index 0000000000..ab864678b8
--- /dev/null
+++ b/src/resources/data/graphql-at-facebook.json
@@ -0,0 +1,7 @@
+{
+ "title": "GraphQL at Facebook",
+ "url": "https://youtube.com/watch?v=etax3aEe2dA",
+ "author": "Dan Schafer",
+ "tags": ["video"],
+ "duration": "26:30"
+}
diff --git a/src/resources/data/graphql-best-practices-hands-on-experience-with-schema-design-security-and-error-handling-for-developers.json b/src/resources/data/graphql-best-practices-hands-on-experience-with-schema-design-security-and-error-handling-for-developers.json
new file mode 100644
index 0000000000..c5fba326c9
--- /dev/null
+++ b/src/resources/data/graphql-best-practices-hands-on-experience-with-schema-design-security-and-error-handling-for-developers.json
@@ -0,0 +1,6 @@
+{
+ "title": "GraphQL Best Practices: Hands-on experience with schema design, security, and error handling for developers",
+ "url": "https://www.amazon.com/dp/B0D9H7MJQV",
+ "author": "Marc-André Giroux & Apoorva Pandey",
+ "tags": ["book"]
+}
diff --git a/src/resources/data/graphql-code-of-conduct.json b/src/resources/data/graphql-code-of-conduct.json
new file mode 100644
index 0000000000..64d708b1d2
--- /dev/null
+++ b/src/resources/data/graphql-code-of-conduct.json
@@ -0,0 +1,5 @@
+{
+ "title": "GraphQL Code of Conduct",
+ "url": "/codeofconduct/",
+ "tags": ["guide"]
+}
diff --git a/src/resources/data/graphql-concepts-visualized.json b/src/resources/data/graphql-concepts-visualized.json
new file mode 100644
index 0000000000..0e8e235f89
--- /dev/null
+++ b/src/resources/data/graphql-concepts-visualized.json
@@ -0,0 +1,6 @@
+{
+ "title": "GraphQL Concepts Visualized",
+ "url": "https://medium.com/apollo-stack/the-concepts-of-graphql-bc68bd819be3#.hfczgtdsj",
+ "author": "Dhaivat Pandya",
+ "tags": ["blog"]
+}
diff --git a/src/resources/data/graphql-editor-blog.json b/src/resources/data/graphql-editor-blog.json
new file mode 100644
index 0000000000..e178be9bcc
--- /dev/null
+++ b/src/resources/data/graphql-editor-blog.json
@@ -0,0 +1,5 @@
+{
+ "title": "GraphQL Editor Blog",
+ "url": "https://blog.graphqleditor.com",
+ "tags": ["blog-or-newsletter"]
+}
diff --git a/src/resources/data/graphql-explained.json b/src/resources/data/graphql-explained.json
new file mode 100644
index 0000000000..8f969374d4
--- /dev/null
+++ b/src/resources/data/graphql-explained.json
@@ -0,0 +1,6 @@
+{
+ "title": "GraphQL Explained",
+ "url": "https://medium.com/apollo-stack/graphql-explained-5844742f195e#.zdykxos6i",
+ "author": "JH",
+ "tags": ["blog"]
+}
diff --git a/src/resources/data/graphql-from-zero-to-scala.json b/src/resources/data/graphql-from-zero-to-scala.json
new file mode 100644
index 0000000000..16794d200c
--- /dev/null
+++ b/src/resources/data/graphql-from-zero-to-scala.json
@@ -0,0 +1,7 @@
+{
+ "title": "GraphQL: From Zero to Scala",
+ "author": "Jérémie Astori",
+ "url": "https://www.youtube.com/watch?v=6ttypoLyRaU",
+ "tags": ["video", "backend"],
+ "duration": "15:31"
+}
diff --git a/src/resources/data/graphql-future.json b/src/resources/data/graphql-future.json
new file mode 100644
index 0000000000..34e0edabf9
--- /dev/null
+++ b/src/resources/data/graphql-future.json
@@ -0,0 +1,7 @@
+{
+ "title": "GraphQL Future",
+ "url": "https://youtube.com/watch?v=ViXL0YQnioU",
+ "author": "Lee Byron",
+ "tags": ["video"],
+ "duration": "32:18"
+}
diff --git a/src/resources/data/graphql-in-native-applications.json b/src/resources/data/graphql-in-native-applications.json
new file mode 100644
index 0000000000..18112cdf2b
--- /dev/null
+++ b/src/resources/data/graphql-in-native-applications.json
@@ -0,0 +1,7 @@
+{
+ "title": "GraphQL in native applications",
+ "author": "Igor Canadi & Alex Langenfeld",
+ "url": "https://atscaleconference.com/videos/graphql-in-native-applications-at-scale/",
+ "tags": ["video", "frontend"],
+ "duration": "26:15"
+}
diff --git a/src/resources/data/graphql-in-production-backend-as-a-service-michael-paris-vince-ning-graphql-in-production-meetup-sf-august-2016.json b/src/resources/data/graphql-in-production-backend-as-a-service-michael-paris-vince-ning-graphql-in-production-meetup-sf-august-2016.json
new file mode 100644
index 0000000000..e4500dbfcd
--- /dev/null
+++ b/src/resources/data/graphql-in-production-backend-as-a-service-michael-paris-vince-ning-graphql-in-production-meetup-sf-august-2016.json
@@ -0,0 +1,7 @@
+{
+ "title": "GraphQL in Production: Backend as a Service",
+ "author": "Michael Paris & Vince Ning",
+ "url": "https://www.youtube.com/watch?v=U2NKoStGBvE",
+ "tags": ["video", "backend"],
+ "duration": "19:08"
+}
diff --git a/src/resources/data/graphql-js-tutorial.json b/src/resources/data/graphql-js-tutorial.json
new file mode 100644
index 0000000000..8ad1d6af05
--- /dev/null
+++ b/src/resources/data/graphql-js-tutorial.json
@@ -0,0 +1,5 @@
+{
+ "title": "GraphQL-JS tutorial",
+ "url": "/graphql-js",
+ "tags": ["guide"]
+}
diff --git a/src/resources/data/graphql-screencasts.json b/src/resources/data/graphql-screencasts.json
new file mode 100644
index 0000000000..7f21b01ad3
--- /dev/null
+++ b/src/resources/data/graphql-screencasts.json
@@ -0,0 +1,5 @@
+{
+ "title": "GraphQL Screencasts",
+ "url": "https://graphql.wtf",
+ "tags": ["guide"]
+}
diff --git a/src/resources/data/graphql-server-tutorial-for-node-js-with-sql-mongodb-and-rest-jonas-helfer.json b/src/resources/data/graphql-server-tutorial-for-node-js-with-sql-mongodb-and-rest-jonas-helfer.json
new file mode 100644
index 0000000000..9ba61ed60f
--- /dev/null
+++ b/src/resources/data/graphql-server-tutorial-for-node-js-with-sql-mongodb-and-rest-jonas-helfer.json
@@ -0,0 +1,7 @@
+{
+ "title": "GraphQL server tutorial for Node.js with SQL, MongoDB and REST",
+ "author": "Jonas Helfer",
+ "url": "https://www.youtube.com/watch?v=PHabPhgRUuU",
+ "tags": ["video", "backend"],
+ "duration": "1:03:01"
+}
diff --git a/src/resources/data/graphql-servers.json b/src/resources/data/graphql-servers.json
new file mode 100644
index 0000000000..95b1b633b3
--- /dev/null
+++ b/src/resources/data/graphql-servers.json
@@ -0,0 +1,7 @@
+{
+ "title": "GraphQL Servers",
+ "author": "Nick Schrock",
+ "url": "https://www.youtube.com/watch?v=KOudxKJXsjc",
+ "tags": ["video", "backend"],
+ "duration": "28:03"
+}
diff --git a/src/resources/data/graphql-source-code-overview.json b/src/resources/data/graphql-source-code-overview.json
new file mode 100644
index 0000000000..bc2f659367
--- /dev/null
+++ b/src/resources/data/graphql-source-code-overview.json
@@ -0,0 +1,7 @@
+{
+ "title": "GraphQL Source Code Overview",
+ "author": "Lee Byron",
+ "url": "https://youtube.com/watch?v=IqtYr6RX32Q",
+ "tags": ["video", "backend"],
+ "duration": "25:32"
+}
diff --git a/src/resources/data/graphql-tutorials.json b/src/resources/data/graphql-tutorials.json
new file mode 100644
index 0000000000..994d2ff091
--- /dev/null
+++ b/src/resources/data/graphql-tutorials.json
@@ -0,0 +1,5 @@
+{
+ "title": "GraphQL Tutorials",
+ "url": "https://hasura.io/learn",
+ "tags": ["guide"]
+}
diff --git a/src/resources/data/graphql-weekly.json b/src/resources/data/graphql-weekly.json
new file mode 100644
index 0000000000..2d90509692
--- /dev/null
+++ b/src/resources/data/graphql-weekly.json
@@ -0,0 +1,5 @@
+{
+ "title": "GraphQL Weekly",
+ "url": "https://graphqlweekly.com",
+ "tags": ["blog-or-newsletter"]
+}
diff --git a/src/resources/data/graphql-wtf-episodes-feed.json b/src/resources/data/graphql-wtf-episodes-feed.json
new file mode 100644
index 0000000000..f59eceabdb
--- /dev/null
+++ b/src/resources/data/graphql-wtf-episodes-feed.json
@@ -0,0 +1,5 @@
+{
+ "title": "GraphQL WTF Episodes Feed",
+ "url": "https://graphql.wtf",
+ "tags": ["blog-or-newsletter"]
+}
diff --git a/src/resources/data/hands-on-full-stack-web-development-with-graphql-and-react.json b/src/resources/data/hands-on-full-stack-web-development-with-graphql-and-react.json
new file mode 100644
index 0000000000..57c9172b6b
--- /dev/null
+++ b/src/resources/data/hands-on-full-stack-web-development-with-graphql-and-react.json
@@ -0,0 +1,6 @@
+{
+ "title": "Hands-on Full-Stack Web Development with GraphQL and React",
+ "url": "https://www.packtpub.com/en-us/product/hands-on-full-stack-web-development-with-graphql-and-react-9781789135763",
+ "author": "Sebastian Grebe",
+ "tags": ["guide"]
+}
diff --git a/src/resources/data/hasura-s-blog.json b/src/resources/data/hasura-s-blog.json
new file mode 100644
index 0000000000..7addc43662
--- /dev/null
+++ b/src/resources/data/hasura-s-blog.json
@@ -0,0 +1,5 @@
+{
+ "title": "Hasura's Blog",
+ "url": "https://hasura.io/blog",
+ "tags": ["blog-or-newsletter"]
+}
diff --git a/src/resources/data/inigo-s-security-blog.json b/src/resources/data/inigo-s-security-blog.json
new file mode 100644
index 0000000000..ef9f0f1c82
--- /dev/null
+++ b/src/resources/data/inigo-s-security-blog.json
@@ -0,0 +1,5 @@
+{
+ "title": "Inigo's Security Blog",
+ "url": "https://inigo.io/blog",
+ "tags": ["blog-or-newsletter"]
+}
diff --git a/src/resources/data/learning-graphql-and-relay.json b/src/resources/data/learning-graphql-and-relay.json
new file mode 100644
index 0000000000..a5b131f8f9
--- /dev/null
+++ b/src/resources/data/learning-graphql-and-relay.json
@@ -0,0 +1,6 @@
+{
+ "title": "Learning GraphQL and Relay",
+ "url": "https://www.amazon.com/Learning-GraphQL-Relay-Samer-Buna/dp/1786465752",
+ "author": "Samer Buna",
+ "tags": ["book"]
+}
diff --git a/src/resources/data/learning-graphql.json b/src/resources/data/learning-graphql.json
new file mode 100644
index 0000000000..ee063bd648
--- /dev/null
+++ b/src/resources/data/learning-graphql.json
@@ -0,0 +1,6 @@
+{
+ "title": "Learning GraphQL",
+ "url": "https://www.amazon.com/Learning-GraphQL-Declarative-Fetching-Modern/dp/1492030716/",
+ "author": "Eve Porcello & Alex Banks",
+ "tags": ["book"]
+}
diff --git a/src/resources/data/modernize-your-angular-app-with-graphql.json b/src/resources/data/modernize-your-angular-app-with-graphql.json
new file mode 100644
index 0000000000..e62c2b7c82
--- /dev/null
+++ b/src/resources/data/modernize-your-angular-app-with-graphql.json
@@ -0,0 +1,7 @@
+{
+ "title": "Modernize Your Angular App with GraphQL",
+ "author": "Uri Goldshtein",
+ "url": "https://www.youtube.com/watch?v=E8feZBidZcs",
+ "tags": ["video", "frontend"],
+ "duration": "37:38"
+}
diff --git a/src/resources/data/official-graphql-blog.json b/src/resources/data/official-graphql-blog.json
new file mode 100644
index 0000000000..63976c18d7
--- /dev/null
+++ b/src/resources/data/official-graphql-blog.json
@@ -0,0 +1,5 @@
+{
+ "title": "Official GraphQL Blog",
+ "url": "https://graphql.org/blog",
+ "tags": ["blog-or-newsletter"]
+}
diff --git a/src/resources/data/production-ready-graphql.json b/src/resources/data/production-ready-graphql.json
new file mode 100644
index 0000000000..48022997b1
--- /dev/null
+++ b/src/resources/data/production-ready-graphql.json
@@ -0,0 +1,6 @@
+{
+ "title": "Production Ready GraphQL",
+ "url": "https://book.productionreadygraphql.com/",
+ "author": "Marc-Andre Giroux",
+ "tags": ["guide"]
+}
diff --git a/src/resources/data/relay-2-simpler-faster-and-more-predictable-greg-hurrell.json b/src/resources/data/relay-2-simpler-faster-and-more-predictable-greg-hurrell.json
new file mode 100644
index 0000000000..f0a6960ed5
--- /dev/null
+++ b/src/resources/data/relay-2-simpler-faster-and-more-predictable-greg-hurrell.json
@@ -0,0 +1,7 @@
+{
+ "title": "Relay 2 - simpler, faster, and more predictable",
+ "author": "Greg Hurrell",
+ "url": "https://www.youtube.com/watch?v=OEfUBN9dAI8",
+ "tags": ["video", "frontend", "backend"],
+ "duration": "1:26:01"
+}
diff --git a/src/resources/data/relicensing-the-graphql-specification.json b/src/resources/data/relicensing-the-graphql-specification.json
new file mode 100644
index 0000000000..334cfdf4c0
--- /dev/null
+++ b/src/resources/data/relicensing-the-graphql-specification.json
@@ -0,0 +1,6 @@
+{
+ "title": "Relicensing the GraphQL specification",
+ "url": "https://medium.com/@leeb/relicensing-the-graphql-specification-e7d07a52301b",
+ "author": "Lee Byron",
+ "tags": ["blog"]
+}
diff --git a/src/resources/data/stepzen-s-blog.json b/src/resources/data/stepzen-s-blog.json
new file mode 100644
index 0000000000..208aca0ebb
--- /dev/null
+++ b/src/resources/data/stepzen-s-blog.json
@@ -0,0 +1,5 @@
+{
+ "title": "StepZen's Blog",
+ "url": "https://stepzen.com/blog",
+ "tags": ["blog-or-newsletter"]
+}
diff --git a/src/resources/data/the-graphql-guide.json b/src/resources/data/the-graphql-guide.json
new file mode 100644
index 0000000000..b34d66f420
--- /dev/null
+++ b/src/resources/data/the-graphql-guide.json
@@ -0,0 +1,6 @@
+{
+ "title": "The GraphQL Guide",
+ "url": "https://graphql.guide",
+ "author": "Loren Sands-Ramshaw",
+ "tags": ["guide"]
+}
diff --git a/src/resources/data/the-guild-s-blog.json b/src/resources/data/the-guild-s-blog.json
new file mode 100644
index 0000000000..461761c85b
--- /dev/null
+++ b/src/resources/data/the-guild-s-blog.json
@@ -0,0 +1,5 @@
+{
+ "title": "The Guild's Blog",
+ "url": "https://the-guild.dev/blog",
+ "tags": ["blog-or-newsletter"]
+}
diff --git a/src/resources/data/the-guild-s-newsletter.json b/src/resources/data/the-guild-s-newsletter.json
new file mode 100644
index 0000000000..c34b24e874
--- /dev/null
+++ b/src/resources/data/the-guild-s-newsletter.json
@@ -0,0 +1,5 @@
+{
+ "title": "The Guild's Newsletter",
+ "url": "https://getrevue.co/profile/TheGuild",
+ "tags": ["blog-or-newsletter"]
+}
diff --git a/src/resources/data/the-road-to-graphql.json b/src/resources/data/the-road-to-graphql.json
new file mode 100644
index 0000000000..a9a97f091f
--- /dev/null
+++ b/src/resources/data/the-road-to-graphql.json
@@ -0,0 +1,6 @@
+{
+ "title": "The Road to GraphQL",
+ "url": "https://www.robinwieruch.de/the-road-to-graphql-book/",
+ "author": "Robin Wieruch",
+ "tags": ["guide"]
+}
diff --git a/src/resources/data/trademark-policy.json b/src/resources/data/trademark-policy.json
new file mode 100644
index 0000000000..c51132fd8e
--- /dev/null
+++ b/src/resources/data/trademark-policy.json
@@ -0,0 +1,5 @@
+{
+ "title": "Linux Foundation Trademark Policy",
+ "url": "https://lfprojects.org/policies/trademark-policy/",
+ "tags": ["guide"]
+}
diff --git a/src/resources/data/tutorial-how-to-build-a-graphql-server.json b/src/resources/data/tutorial-how-to-build-a-graphql-server.json
new file mode 100644
index 0000000000..270598c7f5
--- /dev/null
+++ b/src/resources/data/tutorial-how-to-build-a-graphql-server.json
@@ -0,0 +1,6 @@
+{
+ "title": "Tutorial: How to Build a GraphQL Server",
+ "url": "https://medium.com/apollo-stack/tutorial-building-a-graphql-server-cddaa023c035#.bu6sdnst4",
+ "author": "Jonas Helfer & Johanna Griffin",
+ "tags": ["blog"]
+}
diff --git a/src/resources/data/tutorial-kick-start-a-js-api-with-apollo-server-dataloader-and-knex.json b/src/resources/data/tutorial-kick-start-a-js-api-with-apollo-server-dataloader-and-knex.json
new file mode 100644
index 0000000000..48eaef09bb
--- /dev/null
+++ b/src/resources/data/tutorial-kick-start-a-js-api-with-apollo-server-dataloader-and-knex.json
@@ -0,0 +1,6 @@
+{
+ "title": "Tutorial: Kick start a JS API with Apollo-server, Dataloader and Knex",
+ "url": "https://bamtech.gitbook.io/dev-standards/backend/graphql-js/getting-started-with-apollo-server-dataloader-knex.mo",
+ "author": "Thomas Pucci",
+ "tags": ["blog"]
+}
diff --git a/src/resources/data/unleashing-the-power-of-graphql-using-angular-2-gerard-sans-ng-be-2016.json b/src/resources/data/unleashing-the-power-of-graphql-using-angular-2-gerard-sans-ng-be-2016.json
new file mode 100644
index 0000000000..0da38a9a87
--- /dev/null
+++ b/src/resources/data/unleashing-the-power-of-graphql-using-angular-2-gerard-sans-ng-be-2016.json
@@ -0,0 +1,7 @@
+{
+ "title": "Unleashing the power of GraphQL using Angular 2",
+ "author": "Gerard Sans",
+ "url": "https://www.youtube.com/watch?v=VYpJ9pfugM8",
+ "tags": ["video", "frontend"],
+ "duration": "45:35"
+}
diff --git a/src/resources/data/wundergraph-s-blog.json b/src/resources/data/wundergraph-s-blog.json
new file mode 100644
index 0000000000..645953db5d
--- /dev/null
+++ b/src/resources/data/wundergraph-s-blog.json
@@ -0,0 +1,5 @@
+{
+ "title": "WunderGraph's Blog",
+ "url": "https://wundergraph.com/blog",
+ "tags": ["blog-or-newsletter"]
+}
diff --git a/src/resources/data/yoga-graphql-server-tutorial.json b/src/resources/data/yoga-graphql-server-tutorial.json
new file mode 100644
index 0000000000..c1bf2f20ae
--- /dev/null
+++ b/src/resources/data/yoga-graphql-server-tutorial.json
@@ -0,0 +1,5 @@
+{
+ "title": "Yoga GraphQL Server Tutorial",
+ "url": "https://the-guild.dev/graphql/yoga-server/tutorial",
+ "tags": ["guide"]
+}
diff --git a/src/resources/data/your-first-graphql-server.json b/src/resources/data/your-first-graphql-server.json
new file mode 100644
index 0000000000..0af9082ceb
--- /dev/null
+++ b/src/resources/data/your-first-graphql-server.json
@@ -0,0 +1,6 @@
+{
+ "title": "Your First GraphQL Server",
+ "url": "https://medium.com/the-graphqlhub/your-first-graphql-server-3c766ab4f0a2#.ovn0y19k4",
+ "author": "Clay Allsopp",
+ "tags": ["blog"]
+}
diff --git a/src/resources/types.ts b/src/resources/types.ts
new file mode 100644
index 0000000000..4b2cc25ef1
--- /dev/null
+++ b/src/resources/types.ts
@@ -0,0 +1,41 @@
+import { type } from "arktype"
+
+export const topics = [
+ "frontend",
+ "backend",
+ "federation",
+ "schema-design",
+ "api-platform-and-gateways",
+ "developer-experience",
+ "security",
+ "ai",
+ "monitoring",
+ "tools",
+] as const
+export type Topic = (typeof topics)[number]
+
+export const kinds = [
+ "video",
+ "blog",
+ "tools-and-libraries",
+ "guide",
+ "book",
+ "blog-or-newsletter",
+ "docs",
+] as const
+export type Kind = (typeof kinds)[number]
+
+export type ResourceTag = Topic | Kind
+
+export const ResourceMetadata = type({
+ title: "string>0",
+ url: type("string.url").or("/^\\/.+$/"),
+ "author?": "string",
+ "kind?": type.enumerated(...kinds),
+ "topics?": type.enumerated(...topics).array(),
+ "description?": "string>0",
+ "duration?": "string",
+ tags: type.enumerated(...topics, ...kinds).array(),
+})
+
+export type ResourceMetadata = typeof ResourceMetadata.inferOut
diff --git a/tailwind.config.ts b/tailwind.config.ts
index 16649f5949..1dbf34a0fe 100644
--- a/tailwind.config.ts
+++ b/tailwind.config.ts
@@ -128,9 +128,10 @@ const config: Config = {
plugin(({ addBase }) => {
// heading styles
addBase({
- ".typography-d1, .typography-h1, .typography-h2, .typography-h3": {
- lineHeight: "1.2",
- },
+ ".typography-d1, .typography-h1, .typography-h2, .typography-h3, .typography-h4":
+ {
+ lineHeight: "1.2",
+ },
".typography-d1": {
fontSize: "48px",
"@screen lg": {
@@ -155,6 +156,12 @@ const config: Config = {
fontSize: "32px",
},
},
+ ".typography-h4": {
+ fontSize: "20px",
+ "@screen md": {
+ fontSize: "28px",
+ },
+ },
})
// paragraph styles
diff --git a/test/e2e/resources-hub.spec.ts b/test/e2e/resources-hub.spec.ts
new file mode 100644
index 0000000000..bc1816f630
--- /dev/null
+++ b/test/e2e/resources-hub.spec.ts
@@ -0,0 +1,24 @@
+import { expect, test } from "@playwright/test"
+
+const pages = [
+ "/resources",
+ "/resources/frontend",
+ "/resources/backend",
+ "/resources/federation",
+ "/resources/ai",
+ "/resources/security",
+ "/resources/monitoring",
+ "/code",
+ "/conf",
+ "/resources/reading",
+ "/resources/video",
+]
+
+test.describe("Resource hub pages exist", () => {
+ for (const path of pages) {
+ test(`renders ${path}`, async ({ page }) => {
+ const response = await page.goto(path)
+ expect(response?.ok()).toBeTruthy()
+ })
+ }
+})
diff --git a/vercel.json b/vercel.json
index 244e7c6f2c..87a8d6f90a 100644
--- a/vercel.json
+++ b/vercel.json
@@ -400,6 +400,46 @@
"destination": "/blog/2020-10-15-newsletter-september-2020",
"permanent": true
},
+ {
+ "source": "/community/resources/official-channels",
+ "destination": "/resources/official-channels",
+ "permanent": true
+ },
+ {
+ "source": "/community/resources/training-courses",
+ "destination": "/resources/training-courses",
+ "permanent": true
+ },
+ {
+ "source": "/community/resources/community-channels",
+ "destination": "/resources/community-channels",
+ "permanent": true
+ },
+ {
+ "source": "/community/resources/blogs-and-newsletters",
+ "destination": "/resources/blogs-and-newsletters",
+ "permanent": true
+ },
+ {
+ "source": "/community/resources/videos",
+ "destination": "/resources/videos",
+ "permanent": true
+ },
+ {
+ "source": "/community/resources/vendor-channels",
+ "destination": "/resources/vendor-channels",
+ "permanent": true
+ },
+ {
+ "source": "/community/resources/books",
+ "destination": "/resources/books",
+ "permanent": true
+ },
+ {
+ "source": "/community/resources/more-resources",
+ "destination": "/resources",
+ "permanent": true
+ },
{
"source": "/blog/2020-10-15-graphql-foundation-monthly-newsletter-september-2020/",
"destination": "/blog/2020-10-15-newsletter-september-2020",