()
+ 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 (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {filtered.length} videos
+
+ {selectedTopics.length > 0 && (
+
+ )}
+
+
+
+
+ {filtered.map(resource => {
+ const tags = resource.tags.map(tag => ({
+ label: tag,
+ color: "hsl(var(--color-neu-500))",
+ }))
+
+ return (
+ -
+
+
+ )
+ })}
+
+
+ )
+}
+
+function TopicsCombobox({
+ label,
+ options,
+ value,
+ onChange,
+}: {
+ label: string
+ options: string[]
+ value: string[]
+ onChange: (next: string[]) => void
+}) {
+ const [query, setQuery] = useState("")
+
+ const filteredOptions =
+ query === ""
+ ? options
+ : options.filter(option =>
+ option.toLowerCase().includes(query.toLowerCase()),
+ )
+
+ return (
+
+
+
+
+
+
+
+ {filteredOptions.map(option => (
+
+ {({ active, selected }) => (
+
+ )}
+
+ ))}
+
+
+
+
+ )
+}
+
+function TopicOption({
+ active,
+ selected,
+ option,
+}: {
+ active: boolean
+ selected: boolean
+ option: string
+}) {
+ return (
+
+ )
+}
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/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..f5eeb1b82b
--- /dev/null
+++ b/src/resources/data/a-graphql-framework-for-non-js-servers-syrus-akbary.json
@@ -0,0 +1,6 @@
+{
+ "title": "A GraphQL Framework for Non-JS Servers",
+ "author": "Syrus Akbary",
+ "url": "https://www.youtube.com/watch?v=RNoyPSrQyPs",
+ "tags": ["video", "backend"]
+}
diff --git a/src/resources/data/a-graphql-framework-for-non-js-servers.json b/src/resources/data/a-graphql-framework-for-non-js-servers.json
new file mode 100644
index 0000000000..b6f39fbb01
--- /dev/null
+++ b/src/resources/data/a-graphql-framework-for-non-js-servers.json
@@ -0,0 +1,6 @@
+{
+ "title": "A GraphQL Framework for Non-JS Servers",
+ "author": "Syrus Akbary",
+ "url": "https://www.youtube.com/watch?v=RNoyPSrQyPs",
+ "tags": ["video"]
+}
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..8db0be4cd4
--- /dev/null
+++ b/src/resources/data/a-postgresql-backed-graphql-baas.json
@@ -0,0 +1,6 @@
+{
+ "title": "A PostgreSQL backed GraphQL BaaS",
+ "author": "Tanmai Gopal",
+ "url": "https://www.youtube.com/watch?v=neIZcc8y3B0",
+ "tags": ["video", "backend"]
+}
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..fbadd36641
--- /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"]
+}
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..080bf02325
--- /dev/null
+++ b/src/resources/data/apollo-client-put-graphql-data-in-your-ui.json
@@ -0,0 +1,6 @@
+{
+ "title": "Apollo Client: Put GraphQL Data in Your UI",
+ "author": "Sashko Stubailo",
+ "url": "https://www.youtube.com/watch?v=u1E0CbGeICo",
+ "tags": ["video", "frontend"]
+}
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..4fc31c17c3
--- /dev/null
+++ b/src/resources/data/build-a-full-graphql-backend-in-under-5-minutes-michael-paris.json
@@ -0,0 +1,6 @@
+{
+ "title": "Build a Full GraphQL Backend in Under 5 Minutes",
+ "author": "Michael Paris",
+ "url": "https://www.youtube.com/watch?v=bJ8pnYd6jPQ",
+ "tags": ["video", "backend"]
+}
diff --git a/src/resources/data/build-a-full-graphql-backend-in-under-5-minutes.json b/src/resources/data/build-a-full-graphql-backend-in-under-5-minutes.json
new file mode 100644
index 0000000000..ce02a20361
--- /dev/null
+++ b/src/resources/data/build-a-full-graphql-backend-in-under-5-minutes.json
@@ -0,0 +1,6 @@
+{
+ "title": "Build a Full GraphQL Backend in Under 5 Minutes",
+ "author": "Michael Paris",
+ "url": "https://www.youtube.com/watch?v=bJ8pnYd6jPQ",
+ "tags": ["video"]
+}
diff --git a/src/resources/data/build-a-graphql-backend-with-the-serverless-framework-ryan-brown.json b/src/resources/data/build-a-graphql-backend-with-the-serverless-framework-ryan-brown.json
new file mode 100644
index 0000000000..d744bed512
--- /dev/null
+++ b/src/resources/data/build-a-graphql-backend-with-the-serverless-framework-ryan-brown.json
@@ -0,0 +1,6 @@
+{
+ "title": "Build a GraphQL Backend with the Serverless Framework",
+ "author": "Ryan Brown",
+ "url": "https://acloud.guru/learn/serverless-with-graphql",
+ "tags": ["video", "backend"]
+}
diff --git a/src/resources/data/build-a-graphql-backend-with-the-serverless-framework.json b/src/resources/data/build-a-graphql-backend-with-the-serverless-framework.json
new file mode 100644
index 0000000000..cb2b1a0b88
--- /dev/null
+++ b/src/resources/data/build-a-graphql-backend-with-the-serverless-framework.json
@@ -0,0 +1,6 @@
+{
+ "title": "Build a GraphQL Backend with the Serverless Framework",
+ "author": "Ryan Brown",
+ "url": "https://acloud.guru/learn/serverless-with-graphql",
+ "tags": ["video"]
+}
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..499b02f370
--- /dev/null
+++ b/src/resources/data/build-a-graphql-server-for-node-js-using-postgresql-mysql-lee-benson.json
@@ -0,0 +1,6 @@
+{
+ "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"]
+}
diff --git a/src/resources/data/build-a-graphql-server-for-node-js-using-postgresql-mysql.json b/src/resources/data/build-a-graphql-server-for-node-js-using-postgresql-mysql.json
new file mode 100644
index 0000000000..86b3417360
--- /dev/null
+++ b/src/resources/data/build-a-graphql-server-for-node-js-using-postgresql-mysql.json
@@ -0,0 +1,6 @@
+{
+ "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"]
+}
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..4c2f0ea1e0
--- /dev/null
+++ b/src/resources/data/building-native-mobile-apps-with-graphql-martjin-walraven-react-europe-2016.json
@@ -0,0 +1,6 @@
+{
+ "title": "Building Native Mobile Apps with GraphQL",
+ "author": "Martjin Walraven",
+ "url": "https://www.youtube.com/watch?v=z5rz3saDPJ8",
+ "tags": ["video", "frontend"]
+}
diff --git a/src/resources/data/building-native-mobile-apps-with-graphql.json b/src/resources/data/building-native-mobile-apps-with-graphql.json
new file mode 100644
index 0000000000..810cb2a13b
--- /dev/null
+++ b/src/resources/data/building-native-mobile-apps-with-graphql.json
@@ -0,0 +1,6 @@
+{
+ "title": "Building Native Mobile Apps with GraphQL",
+ "author": "Martjin Walraven",
+ "url": "https://www.youtube.com/watch?v=z5rz3saDPJ8",
+ "tags": ["video"]
+}
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..94c31bff6d
--- /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,6 @@
+{
+ "title": "Development of real-time apps with GraphQL Node.js",
+ "author": "Vince Ning & Michael Paris",
+ "url": "https://youtu.be/yh_A6CEqsSM",
+ "tags": ["video", "backend"]
+}
diff --git a/src/resources/data/development-of-real-time-apps-with-graphql-node-js.json b/src/resources/data/development-of-real-time-apps-with-graphql-node-js.json
new file mode 100644
index 0000000000..2d94848143
--- /dev/null
+++ b/src/resources/data/development-of-real-time-apps-with-graphql-node-js.json
@@ -0,0 +1,6 @@
+{
+ "title": "Development of real-time apps with GraphQL Node.js",
+ "author": "Vince Ning & Michael Paris",
+ "url": "https://youtu.be/yh_A6CEqsSM",
+ "tags": ["video"]
+}
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..611d7a37dc
--- /dev/null
+++ b/src/resources/data/exploring-graphql.json
@@ -0,0 +1,6 @@
+{
+ "title": "Exploring GraphQL",
+ "url": "https://youtube.com/watch?v=WQLzZf34FJ8",
+ "author": "Lee Byron",
+ "tags": ["video"]
+}
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..b7897ec52b
--- /dev/null
+++ b/src/resources/data/from-zero-to-graphql-in-30-minutes.json
@@ -0,0 +1,6 @@
+{
+ "title": "From Zero to GraphQL in 30 Minutes",
+ "url": "https://youtube.com/watch?v=UBGzsb2UkeY",
+ "author": "Steven Luscher",
+ "tags": ["video"]
+}
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..8f5a73d96d
--- /dev/null
+++ b/src/resources/data/graphql-at-facebook.json
@@ -0,0 +1,6 @@
+{
+ "title": "GraphQL at Facebook",
+ "url": "https://youtube.com/watch?v=etax3aEe2dA",
+ "author": "Dan Schafer",
+ "tags": ["video"]
+}
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..f3a10cfedc
--- /dev/null
+++ b/src/resources/data/graphql-from-zero-to-scala.json
@@ -0,0 +1,6 @@
+{
+ "title": "GraphQL: From Zero to Scala",
+ "author": "Jérémie Astori",
+ "url": "https://www.youtube.com/watch?v=6ttypoLyRaU",
+ "tags": ["video", "backend"]
+}
diff --git a/src/resources/data/graphql-future.json b/src/resources/data/graphql-future.json
new file mode 100644
index 0000000000..7b789c35be
--- /dev/null
+++ b/src/resources/data/graphql-future.json
@@ -0,0 +1,6 @@
+{
+ "title": "GraphQL Future",
+ "url": "https://youtube.com/watch?v=ViXL0YQnioU",
+ "author": "Lee Byron",
+ "tags": ["video"]
+}
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..8da2e8cc2c
--- /dev/null
+++ b/src/resources/data/graphql-in-native-applications.json
@@ -0,0 +1,6 @@
+{
+ "title": "GraphQL in native applications",
+ "author": "Igor Canadi & Alex Langenfeld",
+ "url": "https://atscaleconference.com/videos/graphql-in-native-applications-at-scale/",
+ "tags": ["video", "frontend"]
+}
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..8d1469e85e
--- /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,6 @@
+{
+ "title": "GraphQL in Production: Backend as a Service",
+ "author": "Michael Paris & Vince Ning",
+ "url": "https://www.youtube.com/watch?v=U2NKoStGBvE",
+ "tags": ["video", "backend"]
+}
diff --git a/src/resources/data/graphql-in-production-backend-as-a-service.json b/src/resources/data/graphql-in-production-backend-as-a-service.json
new file mode 100644
index 0000000000..05dbe33642
--- /dev/null
+++ b/src/resources/data/graphql-in-production-backend-as-a-service.json
@@ -0,0 +1,6 @@
+{
+ "title": "GraphQL in Production: Backend as a Service",
+ "author": "Michael Paris & Vince Ning",
+ "url": "https://www.youtube.com/watch?v=U2NKoStGBvE",
+ "tags": ["video"]
+}
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..6a120eaf18
--- /dev/null
+++ b/src/resources/data/graphql-server-tutorial-for-node-js-with-sql-mongodb-and-rest-jonas-helfer.json
@@ -0,0 +1,6 @@
+{
+ "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"]
+}
diff --git a/src/resources/data/graphql-server-tutorial-for-node-js-with-sql-mongodb-and-rest.json b/src/resources/data/graphql-server-tutorial-for-node-js-with-sql-mongodb-and-rest.json
new file mode 100644
index 0000000000..c67dd937a2
--- /dev/null
+++ b/src/resources/data/graphql-server-tutorial-for-node-js-with-sql-mongodb-and-rest.json
@@ -0,0 +1,6 @@
+{
+ "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"]
+}
diff --git a/src/resources/data/graphql-servers.json b/src/resources/data/graphql-servers.json
new file mode 100644
index 0000000000..c5f8fa7806
--- /dev/null
+++ b/src/resources/data/graphql-servers.json
@@ -0,0 +1,6 @@
+{
+ "title": "GraphQL Servers",
+ "author": "Nick Schrock",
+ "url": "https://www.youtube.com/watch?v=KOudxKJXsjc",
+ "tags": ["video", "backend"]
+}
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..fdc3c0a830
--- /dev/null
+++ b/src/resources/data/graphql-source-code-overview.json
@@ -0,0 +1,6 @@
+{
+ "title": "GraphQL Source Code Overview",
+ "author": "Lee Byron",
+ "url": "https://youtube.com/watch?v=IqtYr6RX32Q",
+ "tags": ["video", "backend"]
+}
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/hands-on-graphql-for-better-restful-web-services-video-by-ashwin-hegde.json b/src/resources/data/hands-on-graphql-for-better-restful-web-services-video-by-ashwin-hegde.json
new file mode 100644
index 0000000000..99619acd0f
--- /dev/null
+++ b/src/resources/data/hands-on-graphql-for-better-restful-web-services-video-by-ashwin-hegde.json
@@ -0,0 +1,6 @@
+{
+ "title": "Hands-on GraphQL for Better RESTful Web Services (Video)",
+ "author": "Ashwin Hegde",
+ "url": "https://www.packtpub.com/application-development/hands-graphql-better-restful-web-services-video",
+ "tags": ["video", "federation", "backend"]
+}
diff --git a/src/resources/data/hands-on-graphql-for-better-restful-web-services-video.json b/src/resources/data/hands-on-graphql-for-better-restful-web-services-video.json
new file mode 100644
index 0000000000..4cd82b919f
--- /dev/null
+++ b/src/resources/data/hands-on-graphql-for-better-restful-web-services-video.json
@@ -0,0 +1,6 @@
+{
+ "title": "Hands-on GraphQL for Better RESTful Web Services (Video)",
+ "author": "Ashwin Hegde",
+ "url": "https://www.packtpub.com/application-development/hands-graphql-better-restful-web-services-video",
+ "tags": ["video"]
+}
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..a1f831ede2
--- /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": ["guide"]
+}
diff --git a/src/resources/data/learning-graphql-with-react-and-relay.json b/src/resources/data/learning-graphql-with-react-and-relay.json
new file mode 100644
index 0000000000..103f4f0cdd
--- /dev/null
+++ b/src/resources/data/learning-graphql-with-react-and-relay.json
@@ -0,0 +1,6 @@
+{
+ "title": "Learning GraphQL with React and Relay",
+ "author": "Divyendu Singh",
+ "url": "https://www.packtpub.com/application-development/learning-graphql-react-and-relay-video",
+ "tags": ["video", "frontend"]
+}
diff --git a/src/resources/data/learning-graphql.json b/src/resources/data/learning-graphql.json
new file mode 100644
index 0000000000..4287e2bdc7
--- /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": ["guide"]
+}
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..7f6364cb55
--- /dev/null
+++ b/src/resources/data/modernize-your-angular-app-with-graphql.json
@@ -0,0 +1,6 @@
+{
+ "title": "Modernize Your Angular App with GraphQL",
+ "author": "Uri Goldshtein",
+ "url": "https://www.youtube.com/watch?v=E8feZBidZcs",
+ "tags": ["video", "frontend"]
+}
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..5af112c51d
--- /dev/null
+++ b/src/resources/data/relay-2-simpler-faster-and-more-predictable-greg-hurrell.json
@@ -0,0 +1,6 @@
+{
+ "title": "Relay 2 - simpler, faster, and more predictable",
+ "author": "Greg Hurrell",
+ "url": "https://www.youtube.com/watch?v=OEfUBN9dAI8",
+ "tags": ["video", "frontend"]
+}
diff --git a/src/resources/data/relay-2-simpler-faster-and-more-predictable.json b/src/resources/data/relay-2-simpler-faster-and-more-predictable.json
new file mode 100644
index 0000000000..7bfb22a4b4
--- /dev/null
+++ b/src/resources/data/relay-2-simpler-faster-and-more-predictable.json
@@ -0,0 +1,6 @@
+{
+ "title": "Relay 2 - simpler, faster, and more predictable",
+ "author": "Greg Hurrell",
+ "url": "https://www.youtube.com/watch?v=OEfUBN9dAI8",
+ "tags": ["video"]
+}
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-community-discord-channel.json b/src/resources/data/the-community-discord-channel.json
new file mode 100644
index 0000000000..e694c5020e
--- /dev/null
+++ b/src/resources/data/the-community-discord-channel.json
@@ -0,0 +1,5 @@
+{
+ "title": "the community Discord channel",
+ "url": "/community/#official-channels",
+ "tags": ["guide"]
+}
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..415fe86826
--- /dev/null
+++ b/src/resources/data/trademark-policy.json
@@ -0,0 +1,5 @@
+{
+ "title": "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..af39944189
--- /dev/null
+++ b/src/resources/data/unleashing-the-power-of-graphql-using-angular-2-gerard-sans-ng-be-2016.json
@@ -0,0 +1,6 @@
+{
+ "title": "Unleashing the power of GraphQL using Angular 2",
+ "author": "Gerard Sans",
+ "url": "https://www.youtube.com/watch?v=VYpJ9pfugM8",
+ "tags": ["video", "frontend"]
+}
diff --git a/src/resources/data/webinar-series-graphql-around-the-world.json b/src/resources/data/webinar-series-graphql-around-the-world.json
new file mode 100644
index 0000000000..b723cb2ccc
--- /dev/null
+++ b/src/resources/data/webinar-series-graphql-around-the-world.json
@@ -0,0 +1,5 @@
+{
+ "title": "Webinar Series: GraphQL Around The World",
+ "url": "https://graphql-world.com/webinar",
+ "tags": ["video"]
+}
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..5bebea9c79
--- /dev/null
+++ b/src/resources/types.ts
@@ -0,0 +1,39 @@
+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",
+] 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",
+ 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",