Skip to content

Commit

Permalink
Add Keystatic content editing for docs/ (#9198)
Browse files Browse the repository at this point in the history
Co-authored-by: Daniel Cousens <[email protected]>
  • Loading branch information
simonswiss and dcousens committed Jul 8, 2024
1 parent c58693f commit 48a56e3
Show file tree
Hide file tree
Showing 85 changed files with 8,846 additions and 6,244 deletions.
6 changes: 6 additions & 0 deletions docs/app/api/keystatic/[...params]/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { makeRouteHandler } from '@keystatic/next/route-handler'
import config from '../../../../keystatic.config'

export const { POST, GET } = makeRouteHandler({
config,
})
3 changes: 3 additions & 0 deletions docs/app/keystatic/[[...params]]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export default function Page () {
return null
}
6 changes: 6 additions & 0 deletions docs/app/keystatic/keystatic.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
'use client'

import { makePage } from '@keystatic/next/ui/app'
import config from '../../keystatic.config'

export default makePage(config)
12 changes: 12 additions & 0 deletions docs/app/keystatic/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import KeystaticApp from './keystatic'

export default function Layout () {
return (
<html>
<head></head>
<body>
<KeystaticApp />
</body>
</html>
)
}
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
119 changes: 119 additions & 0 deletions docs/keystatic.config.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
// keystatic.config.ts
import { config, fields, collection } from '@keystatic/core'
import { superscriptIcon } from '@keystar/ui/icon/icons/superscriptIcon'

import { inline, mark, wrapper } from '@keystatic/core/content-components'
// import { WellPreview } from './keystatic/admin-previews'

export default config({
storage: {
kind: 'local',
},
ui: {
brand: {
name: 'Keystone Website',
},
},
collections: {
docs: collection({
label: 'Docs',
path: 'content/docs/**',
slugField: 'title',
columns: ['title'],
format: { contentField: 'content' },
schema: {
title: fields.slug({ name: { label: 'Title' } }),
description: fields.text({ label: 'Description' }),
content: fields.markdoc({
label: 'Content',
extension: 'md',
options: {
heading: {
levels: [1, 2, 3, 4],
schema: {
id: fields.text({ label: 'ID' }),
},
},
},
components: {
heading: wrapper({ label: 'Heading', schema: { id: fields.text({ label: 'ID' }) } }),
well: wrapper({
label: 'Well',
schema: {
heading: fields.text({ label: 'Heading' }),
grad: fields.select({
label: 'Gradient',
options: [
{ label: '1', value: 'grad1' },
{ label: '2', value: 'grad2' },
{ label: '3', value: 'grad3' },
{ label: '4', value: 'grad4' },
],
defaultValue: 'grad1',
}),
badge: fields.text({ label: 'Badge' }),
href: fields.text({ label: 'Link href', validation: { isRequired: true } }),
target: fields.select({
label: 'Link target',
description: 'Where should this link open?',
options: [
{ label: 'New tab', value: '_blank' },
{ label: 'Same tab', value: '' },
],
defaultValue: '',
}),
},
/*
Preview below doesn't work properly,
but you can try enable it :)
*/

// ContentView: (data) => <WellPreview {...data} />,
}),
hint: wrapper({
label: 'Hint',
schema: {
kind: fields.select({
label: 'Kind',
options: [
{ label: 'Tip', value: 'tip' },
{ label: 'Warning', value: 'warn' },
{ label: 'Error', value: 'error' },
],
defaultValue: 'tip',
}),
},
}),
'related-content': wrapper({ label: 'Related Content', schema: {} }),
sup: mark({ label: 'Superscript', schema: {}, icon: superscriptIcon, tag: 'sup' }),
emoji: inline({
label: 'Emoji',
schema: {
symbol: fields.text({ label: 'Symbol' }),
alt: fields.text({ label: 'Alt' }),
},
}),
},
}),
},
}),

// Blog
posts: collection({
path: 'content/blog/*',
label: 'Blog',
slugField: 'title',
columns: ['title'],
format: { contentField: 'content' },
schema: {
title: fields.slug({ name: { label: 'Title' } }),
description: fields.text({ label: 'Description' }),
publishDate: fields.date({ label: 'Publish Date', validation: { isRequired: true } }),
authorName: fields.text({ label: 'Author Name', validation: { isRequired: true } }),
authorHandle: fields.url({ label: 'Author Handle' }),
metaImageUrl: fields.url({ label: 'Meta Image URL' }),
content: fields.markdoc({ label: 'Content', extension: 'md' }),
},
}),
},
})
7 changes: 7 additions & 0 deletions docs/keystatic/admin-previews.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
'use client'

import { Well } from '../components/primitives/Well'

export function WellPreview (data) {
return <Well {...data.value}>{data.children}</Well>
}
4 changes: 4 additions & 0 deletions docs/lib/keystatic-reader.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { createReader } from '@keystatic/core/reader'
import keystaticConfig from '../keystatic.config'

export const reader = createReader(process.cwd(), keystaticConfig)
8 changes: 3 additions & 5 deletions docs/markdoc/index.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
import fs from 'fs/promises'
import { isMatch } from 'date-fns'
import Markdoc, { type Config, type Tag, type ValidateError } from '@markdoc/markdoc'
import { isNonEmptyArray } from 'emery/guards'
import { assert } from 'emery/assertions'
import { load } from 'js-yaml'
import { baseMarkdocConfig } from './config'
import { showNextReleaseWithoutReplacement } from './show-next-release'
Expand Down Expand Up @@ -65,12 +63,12 @@ const markdocConfig: Config = {
export function transformContent (errorReportingFilepath: string, content: string): Tag {
const node = Markdoc.parse(content, errorReportingFilepath)
const errors = Markdoc.validate(node, markdocConfig)
if (isNonEmptyArray(errors)) {
throw new MarkdocValidationFailure(errors, errorReportingFilepath)
if (errors.length >= 1) {
throw new MarkdocValidationFailure(errors as any, errorReportingFilepath)
}
const renderableNode = Markdoc.transform(node, markdocConfig)

assert(isTag(renderableNode))
if (!isTag(renderableNode)) throw new TypeError('Expected renderable node')

// Next is annoying about not plain objects
return JSON.parse(JSON.stringify(renderableNode)) as Tag
Expand Down
1 change: 1 addition & 0 deletions docs/next-env.d.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
/// <reference types="next/navigation-types/compat/navigation" />

// NOTE: This file should not be edited
// see https://nextjs.org/docs/basic-features/typescript for more information.
19 changes: 9 additions & 10 deletions docs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,38 +14,31 @@
"remove-conditionals": "tsx scripts/replace-show-next-release/index.ts"
},
"dependencies": {
"@babel/core": "^7.21.0",
"@babel/runtime": "^7.16.3",
"@codemod/core": "^2.0.1",
"@emotion/cache": "11.11.0",
"@emotion/css": "^11.7.1",
"@emotion/react": "^11.7.1",
"@emotion/server": "11.11.0",
"@emotion/weak-memoize": "^0.3.0",
"@keystar/ui": "^0.7.6",
"@keystatic/core": "^0.5.24",
"@keystatic/next": "^5.0.1",
"@keystone-6/fields-document": "workspace:^",
"@keystone-ui/core": "workspace:^",
"@keystone-ui/icons": "workspace:^",
"@markdoc/markdoc": "^0.4.0",
"@preconstruct/next": "^4.0.0",
"@sindresorhus/slugify": "^1.1.2",
"@types/babel__core": "7.20.5",
"@types/facepaint": "^1.2.2",
"@types/gtag.js": "^0.0.20",
"@types/js-yaml": "^4.0.5",
"@types/markdown-it": "^14.0.0",
"@types/react": "^18.0.9",
"@types/react-dom": "^18.0.4",
"@vercel/og": "^0.6.0",
"classnames": "^2.3.1",
"clipboard-copy": "^4.0.1",
"date-fns": "^3.0.0",
"dedent": "^1.0.0",
"emery": "^1.4.1",
"facepaint": "^1.2.1",
"globby": "^14.0.0",
"js-yaml": "^4.1.0",
"lodash.debounce": "^4.0.8",
"markdown-it": "^14.0.0",
"next": "^14.2.0",
"next-compose-plugins": "^2.2.1",
"prism-react-renderer": "^2.0.0",
Expand All @@ -56,6 +49,12 @@
"tsx": "^4.0.0"
},
"devDependencies": {
"@types/babel__core": "7.20.5",
"@types/facepaint": "^1.2.2",
"@types/gtag.js": "^0.0.20",
"@types/js-yaml": "^4.0.5",
"@types/react": "^18.0.9",
"@types/react-dom": "^18.0.4",
"@types/lodash.debounce": "^4.0.6",
"@types/rss": "^0.0.32",
"next-sitemap": "^4.0.0",
Expand Down
31 changes: 22 additions & 9 deletions docs/pages/blog/[post].tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/** @jsxRuntime classic */
/** @jsx jsx */
import path from 'path'
import { jsx } from '@emotion/react'
import { transform } from '@markdoc/markdoc'
import {
type GetStaticPathsResult,
type GetStaticPropsContext,
Expand All @@ -11,20 +11,22 @@ import {
import Link from 'next/link'
import { useRouter } from 'next/router'
import { parse, format } from 'date-fns'
import { globby } from 'globby'
import { type BlogContent, readBlogContent } from '../../markdoc'
import { type BlogContent } from '../../markdoc'
import { extractHeadings, Markdoc } from '../../components/Markdoc'
import { BlogPage } from '../../components/Page'
import { Heading } from '../../components/docs/Heading'
import { Type } from '../../components/primitives/Type'
import { getOgAbsoluteUrl } from '../../lib/og-util'
import { reader } from '../../lib/keystatic-reader'
import { baseMarkdocConfig } from '../../markdoc/config'

export default function Page (props: InferGetStaticPropsType<typeof getStaticProps>) {
const router = useRouter()
const headings = [
{ id: 'title', depth: 1, label: props.title },
...extractHeadings(props.content),
]

const publishedDate = props.publishDate
const parsedDate = parse(publishedDate, 'yyyy-M-d', new Date())
const formattedDateStr = format(parsedDate, 'MMMM do, yyyy')
Expand Down Expand Up @@ -81,17 +83,28 @@ export default function Page (props: InferGetStaticPropsType<typeof getStaticPro
}

export async function getStaticPaths (): Promise<GetStaticPathsResult> {
const files = await globby('**/*.md', {
cwd: path.join(process.cwd(), 'pages/blog'),
})
const posts = await reader.collections.posts.list()
return {
paths: files.map(file => ({ params: { post: file.replace(/\.md$/, '') } })),
paths: posts.map(post => ({ params: { post } })),
fallback: false,
}
}

type KeystaticPostsContent = Omit<BlogContent, 'authorHandle' | 'metaImageUrl'> & {
authorHandle: string | null
metaImageUrl: string | null
}

export async function getStaticProps (
args: GetStaticPropsContext<{ post: string }>
): Promise<GetStaticPropsResult<BlogContent>> {
return { props: await readBlogContent(`pages/blog/${args.params!.post}.md`) }
): Promise<GetStaticPropsResult<KeystaticPostsContent>> {
const keystaticPost = await reader.collections.posts.read(args.params!.post, {
resolveLinkedFiles: true,
})

if (!keystaticPost) throw new Error(`Post not found: ${args.params!.post}`)

const transformedContent = transform(keystaticPost.content.node, baseMarkdocConfig)

return { props: { ...keystaticPost, content: JSON.parse(JSON.stringify(transformedContent)) } }
}
34 changes: 10 additions & 24 deletions docs/pages/blog/index.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,5 @@
/** @jsxRuntime classic */
/** @jsx jsx */
import path from 'path'
import fs from 'fs/promises'
import { globby } from 'globby'
import { type InferGetStaticPropsType, type GetStaticPropsResult } from 'next'
import Link from 'next/link'
import { parse, format } from 'date-fns'
Expand All @@ -13,8 +10,10 @@ import { Page } from '../../components/Page'
import { Type } from '../../components/primitives/Type'
import { Highlight } from '../../components/primitives/Highlight'
import { useMediaQuery } from '../../lib/media'
import { type BlogFrontmatter, extractBlogFrontmatter } from '../../markdoc'
import { siteBaseUrl } from '../../lib/og-util'
import { reader } from '../../lib/keystatic-reader'
import { type Entry } from '@keystatic/core/reader'
import type keystaticConfig from '../../keystatic.config'

const today = new Date()
export default function Docs (props: InferGetStaticPropsType<typeof getStaticProps>) {
Expand Down Expand Up @@ -163,28 +162,15 @@ export async function getStaticProps (): Promise<
GetStaticPropsResult<{
posts: {
slug: string
frontmatter: BlogFrontmatter
frontmatter: Omit<Entry<typeof keystaticConfig['collections']['posts']>, 'content'>
}[]
}>
> {
const files = await globby('*.md', {
cwd: path.join(process.cwd(), 'pages/blog'),
})
const keystaticPosts = await reader.collections.posts.all()

return {
props: {
posts: await Promise.all(
files.map(async filename => {
const contents = await fs.readFile(
path.join(process.cwd(), 'pages/blog', filename),
'utf8'
)
return {
slug: filename.replace(/\.md$/, ''),
frontmatter: extractBlogFrontmatter(contents),
}
})
),
},
}
const postMeta = keystaticPosts.map(post => ({
slug: post.slug,
frontmatter: { ...post.entry, content: null },
}))
return { props: { posts: postMeta } }
}
Loading

0 comments on commit 48a56e3

Please sign in to comment.