Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Keystatic content editing for docs/ #9198

Merged
merged 6 commits into from
Jul 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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.
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
Loading