diff --git a/docs/keystatic.config.tsx b/docs/keystatic.config.tsx
index fb6d786f3..c32c2dc27 100644
--- a/docs/keystatic.config.tsx
+++ b/docs/keystatic.config.tsx
@@ -89,7 +89,7 @@ export default config({
name: 'Keystatic Docs',
},
navigation: {
- Pages: ['pages', 'blog', 'projects'],
+ Pages: ['pages', 'blog', 'projects', 'resources'],
Config: ['authors', 'navigation'],
Experimental: ['pagesWithMarkdocField'],
},
@@ -261,6 +261,74 @@ export default config({
},
}),
+ // ------------------------------
+ // Resources
+ // ------------------------------
+ resources: collection({
+ label: 'Resources',
+ path: 'src/content/resources/*',
+ slugField: 'title',
+ schema: {
+ title: fields.slug({ name: { label: 'Title' } }),
+ type: fields.conditional(
+ fields.select({
+ label: 'Resource type',
+ options: [
+ { label: 'YouTube video', value: 'youtube-video' },
+ { label: 'Article', value: 'article' },
+ ],
+ defaultValue: 'youtube-video',
+ }),
+ {
+ 'youtube-video': fields.object({
+ videoId: fields.text({
+ label: 'Video ID',
+ description: 'The ID of the video (not the URL!)',
+ validation: { length: { min: 1 } },
+ }),
+ thumbnail: fields.cloudImage({
+ label: 'Video thumbnail',
+ description: 'A 16/9 thumbnail image for the video.',
+ }),
+ kind: fields.select({
+ label: 'Video kind',
+ options: [
+ { label: 'Talk', value: 'talk' },
+ { label: 'Screencast', value: 'screencast' },
+ ],
+ defaultValue: 'screencast',
+ }),
+ description: fields.text({
+ label: 'Video description',
+ multiline: true,
+ validation: { length: { min: 1 } },
+ }),
+ }),
+ article: fields.object({
+ url: fields.url({
+ label: 'Article URL',
+ validation: { isRequired: true },
+ }),
+ authorName: fields.text({
+ label: 'Author name',
+ validation: { length: { min: 1 } },
+ }),
+ description: fields.text({
+ label: 'Article description',
+ multiline: true,
+ }),
+ }),
+ }
+ ),
+ sortIndex: fields.integer({
+ label: 'Sort index',
+ description:
+ 'A number value to sort items (low to high) on the front end.',
+ defaultValue: 10,
+ }),
+ },
+ }),
+
// ------------------------------
// For testing purposes only
// ------------------------------
diff --git a/docs/src/app/(public)/resources/layout.tsx b/docs/src/app/(public)/resources/layout.tsx
new file mode 100644
index 000000000..080aec45f
--- /dev/null
+++ b/docs/src/app/(public)/resources/layout.tsx
@@ -0,0 +1,48 @@
+import { Main } from '../../../components/main';
+import Footer from '../../../components/footer';
+
+export const metadata = {
+ title: {
+ template: '%s - Resources | Keystatic',
+ default: 'Resources',
+ },
+ description:
+ 'A collection of videos, talks, articles and other resources to help you learn Keystatic and dig deeper.',
+ openGraph: {
+ title: 'Resources',
+ description:
+ 'A collection of videos, talks, articles and other resources to help you learn Keystatic and dig deeper.',
+ images: [
+ {
+ url: '/og?title=Resources',
+ },
+ ],
+ siteName: 'Keystatic',
+ type: 'website',
+ url: 'https://keystatic.com/resources',
+ },
+ twitter: {
+ card: 'summary_large_image',
+ title: 'Resources',
+ description:
+ 'A collection of videos, talks, articles and other resources to help you learn Keystatic and dig deeper.',
+ site: '@thekeystatic',
+ },
+};
+
+export default async function RootLayout({
+ children,
+}: {
+ children: React.ReactNode;
+}) {
+ return (
+ <>
+
+
+ >
+ );
+}
diff --git a/docs/src/app/(public)/resources/page.tsx b/docs/src/app/(public)/resources/page.tsx
new file mode 100644
index 000000000..5e01b397c
--- /dev/null
+++ b/docs/src/app/(public)/resources/page.tsx
@@ -0,0 +1,213 @@
+import Image from 'next/image';
+import Link from 'next/link';
+import { notFound } from 'next/navigation';
+
+import { ArrowRightIcon } from '../../../components/icons/arrow-right';
+import { reader } from '../../../utils/reader';
+import Button from '../../../components/button';
+import { Entry } from '@keystatic/core/reader';
+import keystaticConfig from '../../../../keystatic.config';
+
+type ResourceEntry = Entry<
+ (typeof keystaticConfig)['collections']['resources']
+>;
+
+type VideoProps = {
+ title: ResourceEntry['title'];
+} & Omit<
+ Extract['value'],
+ 'kind'
+>;
+
+type ArticleProps = {
+ title: ResourceEntry['title'];
+} & Omit<
+ Extract['value'],
+ 'kind'
+>;
+
+export default async function Resources() {
+ const resources = await reader().collections.resources.all();
+ if (!resources) notFound();
+
+ const sortedVideos = resources
+ .filter(
+ resource =>
+ resource.entry.type.discriminant === 'youtube-video' &&
+ resource.entry.type.value.kind === 'screencast'
+ )
+ .sort((a, b) => {
+ return (a.entry.sortIndex as number) - (b.entry.sortIndex as number);
+ })
+ .map(resource => ({
+ title: resource.entry.title,
+ sortIndex: resource.entry.sortIndex,
+ ...resource.entry.type.value,
+ })) as VideoProps[];
+
+ const sortedTalks = resources
+ .filter(
+ resource =>
+ resource.entry.type.discriminant === 'youtube-video' &&
+ resource.entry.type.value.kind === 'talk'
+ )
+ .sort((a, b) => {
+ return (a.entry.sortIndex as number) - (b.entry.sortIndex as number);
+ })
+ .map(resource => ({
+ title: resource.entry.title,
+ sortIndex: resource.entry.sortIndex,
+ ...resource.entry.type.value,
+ })) as VideoProps[];
+
+ const sortedArticles = resources
+ .filter(resource => resource.entry.type.discriminant === 'article')
+ .sort((a, b) => {
+ return (a.entry.sortIndex as number) - (b.entry.sortIndex as number);
+ })
+ .map(resource => ({
+ title: resource.entry.title,
+ sortIndex: resource.entry.sortIndex,
+ ...resource.entry.type.value,
+ })) as ArticleProps[];
+
+ return (
+
+
+
+
+
+
+ The{' '}
+
+ Thinkmill channel
+ {' '}
+ has a growing collection of content about Keystatic.
+
+
+ {sortedVideos.map(video => (
+
+ ))}
+
+
+
+
+ Recorded Keystatic talks from local meetups and conferences.
+
+ {sortedTalks.map(video => (
+
+ ))}
+
+
+
+
+ {sortedArticles.map(article => (
+
+
+
+ {article.title}
+
+
+
+ by {article.authorName}
+
+ {article.description && (
+ {article.description}
+ )}
+
+ ))}
+
+
+
+
+
+
⏳
+
+
+ This page is a work in progress — more resources coming soon!
+
+
+
+
+
+
+ );
+}
+
+function Video({ videoId, title, description, thumbnail }: VideoProps) {
+ const videoUrl = `https://www.youtube.com/watch?v=${videoId}`;
+ return (
+
+
+
+
+
+
+ {title}
+
+
+ {description}
+
+ );
+}
+
+function ResourceGrid(props: React.ComponentProps<'ul'>) {
+ return (
+
+ );
+}
+
+type SectionProps = {
+ title?: string | React.ReactNode;
+ introText?: string;
+ children: React.ReactNode;
+};
+
+function Section({ title, children }: SectionProps) {
+ return (
+
+ {title && {title}
}
+ {children}
+
+ );
+}
diff --git a/docs/src/app/(public)/showcase/layout.tsx b/docs/src/app/(public)/showcase/layout.tsx
index 3180c06aa..2ba85e228 100644
--- a/docs/src/app/(public)/showcase/layout.tsx
+++ b/docs/src/app/(public)/showcase/layout.tsx
@@ -6,6 +6,28 @@ export const metadata = {
template: '%s - Showcase | Keystatic',
default: 'Showcase',
},
+ description:
+ 'A collection of projects using Keystatic to manage parts of their codebase.',
+ openGraph: {
+ title: 'Showcase',
+ description:
+ 'A collection of projects using Keystatic to manage parts of their codebase.',
+ images: [
+ {
+ url: '/og?title=Showcase',
+ },
+ ],
+ siteName: 'Keystatic',
+ type: 'website',
+ url: 'https://keystatic.com/showcase',
+ },
+ twitter: {
+ card: 'summary_large_image',
+ title: 'Showcase',
+ description:
+ 'A collection of projects using Keystatic to manage parts of their codebase.',
+ site: '@thekeystatic',
+ },
};
export default async function RootLayout({
diff --git a/docs/src/content/navigation.yaml b/docs/src/content/navigation.yaml
index ff8b54359..59ce44e6b 100644
--- a/docs/src/content/navigation.yaml
+++ b/docs/src/content/navigation.yaml
@@ -11,6 +11,11 @@ navGroups:
discriminant: page
value: quick-start
isNew: false
+ - label: Resources
+ link:
+ discriminant: url
+ value: /resources
+ isNew: true
- groupName: Integration guides
items:
- label: Astro
diff --git a/docs/src/content/resources/7-things-i-about-keystatic.yaml b/docs/src/content/resources/7-things-i-about-keystatic.yaml
new file mode 100644
index 000000000..cb3eb9a82
--- /dev/null
+++ b/docs/src/content/resources/7-things-i-about-keystatic.yaml
@@ -0,0 +1,16 @@
+title: 7 Things I ❤️ About Keystatic
+type:
+ discriminant: youtube-video
+ value:
+ videoId: rUO7VdR9NPY
+ thumbnail:
+ src: >-
+ https://thinkmill-labs.keystatic.net/keystatic-site/images/nxq56kug7dua/7-things-about-keystatic
+ alt: YouTube video thumbnail
+ height: 720
+ width: 1280
+ kind: talk
+ description: >-
+ Simon Vrachliotis talking at SydJS about the little things he loves about
+ working with Keystatic.
+sortIndex: 15
diff --git a/docs/src/content/resources/codebase-editing-with-minimal-footprint.yaml b/docs/src/content/resources/codebase-editing-with-minimal-footprint.yaml
new file mode 100644
index 000000000..700b74c0f
--- /dev/null
+++ b/docs/src/content/resources/codebase-editing-with-minimal-footprint.yaml
@@ -0,0 +1,16 @@
+title: 'Codebase Editing With Minimal Footprint '
+type:
+ discriminant: youtube-video
+ value:
+ videoId: K_-yTaBWzbA
+ thumbnail:
+ src: >-
+ https://thinkmill-labs.keystatic.net/keystatic-site/images/inru19y6a8qq/keystatic-with-minimal-footprint
+ alt: YouTube video thumbnail
+ height: 900
+ width: 1600
+ kind: screencast
+ description: >-
+ An example of how lightweight Keystatic can be to make a specific part of
+ your codebase editable via an auto-generated Admin UI.
+sortIndex: 15
diff --git a/docs/src/content/resources/dinesh-pandiyan-markdoc-what-is-it-and-why-do-i-love-it.yaml b/docs/src/content/resources/dinesh-pandiyan-markdoc-what-is-it-and-why-do-i-love-it.yaml
new file mode 100644
index 000000000..29bbbd365
--- /dev/null
+++ b/docs/src/content/resources/dinesh-pandiyan-markdoc-what-is-it-and-why-do-i-love-it.yaml
@@ -0,0 +1,16 @@
+title: 'Dinesh Pandiyan - Markdoc: What is it and why do I love it?'
+type:
+ discriminant: youtube-video
+ value:
+ videoId: XIw-0fCpP_4
+ thumbnail:
+ src: >-
+ https://thinkmill-labs.keystatic.net/keystatic-site/images/enzs1rpkqjgu/markdoc
+ alt: YouTube video thumbnail
+ height: 720
+ width: 1280
+ kind: talk
+ description: >-
+ Dinesh walks SydJS through the progression from HTML to Markdown to MDX to
+ Markdoc.
+sortIndex: 20
diff --git a/docs/src/content/resources/keystatic-the-last-framework-hop.yaml b/docs/src/content/resources/keystatic-the-last-framework-hop.yaml
new file mode 100644
index 000000000..576f1665f
--- /dev/null
+++ b/docs/src/content/resources/keystatic-the-last-framework-hop.yaml
@@ -0,0 +1,10 @@
+title: 'Keystatic: The Last Framework Hop'
+type:
+ discriminant: article
+ value:
+ url: https://nibras.co/articles/keystatic-the-last-framework-hop
+ authorName: Griko Nibras
+ description: >-
+ Griko writes about his experience using Keystatic to rebuild the last
+ iteration of their personal website.
+sortIndex: 10
diff --git a/docs/src/content/resources/keystatic-with-astro-s-content-collections.yaml b/docs/src/content/resources/keystatic-with-astro-s-content-collections.yaml
new file mode 100644
index 000000000..758adf750
--- /dev/null
+++ b/docs/src/content/resources/keystatic-with-astro-s-content-collections.yaml
@@ -0,0 +1,16 @@
+title: Keystatic with Astro's Content Collections
+type:
+ discriminant: youtube-video
+ value:
+ videoId: 6l2YWCyPsWk
+ thumbnail:
+ src: >-
+ https://thinkmill-labs.keystatic.net/keystatic-site/images/ewuee6ly46km/tm-keystatic
+ alt: YouTube video thumbnnail
+ height: 900
+ width: 1600
+ kind: screencast
+ description: >-
+ An early-days experimental walk-through of using Keystatic with Astro's
+ content collections.
+sortIndex: 10
diff --git a/docs/src/content/resources/meet-keystatic-by-jed-watson.yaml b/docs/src/content/resources/meet-keystatic-by-jed-watson.yaml
new file mode 100644
index 000000000..3c39e778e
--- /dev/null
+++ b/docs/src/content/resources/meet-keystatic-by-jed-watson.yaml
@@ -0,0 +1,15 @@
+title: Meet Keystatic by Jed Watson
+type:
+ discriminant: youtube-video
+ value:
+ videoId: evO_uz5Ahek
+ thumbnail:
+ src: >-
+ https://thinkmill-labs.keystatic.net/keystatic-site/images/w5e53g9h00e0/meet-keystatic-by-jed-watson
+ alt: YouTube video thumbnail
+ height: 720
+ width: 1280
+ kind: talk
+ description:
+ 'Jed Watson introduces Keystatic at the React Sydney meetup in '
+sortIndex: 10
diff --git a/docs/src/content/resources/real-time-previews-with-next-js-draft-mode.yaml b/docs/src/content/resources/real-time-previews-with-next-js-draft-mode.yaml
new file mode 100644
index 000000000..6bc53af68
--- /dev/null
+++ b/docs/src/content/resources/real-time-previews-with-next-js-draft-mode.yaml
@@ -0,0 +1,17 @@
+title: Real-time previews with Next.js' draft mode
+type:
+ discriminant: youtube-video
+ value:
+ videoId: QuoADtLQVCE
+ thumbnail:
+ src: >-
+ https://thinkmill-labs.keystatic.net/keystatic-site/images/j8wdve6gqm87/live-previews
+ alt: >-
+ YouTube thumbnail for Real-time previews with Next.js' draft mode video
+ height: 900
+ width: 1600
+ kind: screencast
+ description: >-
+ A 2-minute walk-through of how to set up real-time content previews,
+ leveraging Next.js' draft mode feature.
+sortIndex: 5
diff --git a/docs/src/content/resources/using-keystatic-as-a-cms.yaml b/docs/src/content/resources/using-keystatic-as-a-cms.yaml
new file mode 100644
index 000000000..8403a03ae
--- /dev/null
+++ b/docs/src/content/resources/using-keystatic-as-a-cms.yaml
@@ -0,0 +1,10 @@
+title: Using Keystatic as a CMS
+type:
+ discriminant: article
+ value:
+ url: https://www.lukebennett.com.au/posts/using-keystatic-as-a-cms
+ authorName: Luke Bennett
+ description: >-
+ An honest and candid writeup about using Keystatic as a CMS, despite the
+ bias of being a Thinkmill employee.
+sortIndex: 20