Skip to content

Commit

Permalink
make blog posts and docs page editable with Keystatic
Browse files Browse the repository at this point in the history
  • Loading branch information
simonswiss committed Jul 4, 2024
1 parent c58693f commit 323255e
Show file tree
Hide file tree
Showing 78 changed files with 13,090 additions and 13,792 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.
112 changes: 112 additions & 0 deletions docs/keystatic.config.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
// keystatic.config.ts
import { config, fields, collection } from '@keystatic/core'
import { superscriptIcon } from '@keystar/ui/icon/icons/superscriptIcon'

import { 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' }),
},
}),
},
}),

// 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)
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.
3 changes: 3 additions & 0 deletions docs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@
"@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:^",
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 } }
}
23 changes: 15 additions & 8 deletions docs/pages/docs/[...rest].tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import path from 'path'
import React from 'react'
import {
type GetStaticPathsResult,
Expand All @@ -7,11 +6,13 @@ import {
type InferGetStaticPropsType,
} from 'next'
import { useRouter } from 'next/router'
import { globby } from 'globby'
import { type DocsContent, readDocsContent } from '../../markdoc'
import { type DocsContent } from '../../markdoc'
import { extractHeadings, Markdoc } from '../../components/Markdoc'
import { DocsPage } from '../../components/Page'
import { Heading } from '../../components/docs/Heading'
import { reader } from '../../lib/keystatic-reader'
import { transform } from '@markdoc/markdoc'
import { baseMarkdocConfig } from '../../markdoc/config'

export default function DocPage (props: InferGetStaticPropsType<typeof getStaticProps>) {
const router = useRouter()
Expand All @@ -37,17 +38,23 @@ export default function DocPage (props: InferGetStaticPropsType<typeof getStatic
}

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

export async function getStaticProps (
args: GetStaticPropsContext<{ rest: string[] }>
): Promise<GetStaticPropsResult<DocsContent>> {
return { props: await readDocsContent(`pages/docs/${args.params!.rest.join('/')}.md`) }
const doc = await reader.collections.docs.read(args.params!.rest.join('/'), {
resolveLinkedFiles: true,
})

if (!doc) throw new Error(`Doc page not found: ${args.params!.rest.join('/')}`)

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

return { props: { ...doc, content: JSON.parse(JSON.stringify(transformedContent)) } }
}
20 changes: 12 additions & 8 deletions docs/tsconfig.json
Original file line number Diff line number Diff line change
@@ -1,20 +1,24 @@
{
"compilerOptions": {
"target": "esnext",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"strict": false,
"noEmit": true,
"esModuleInterop": true,
"incremental": true,
"module": "esnext",
"moduleResolution": "node",
"esModuleInterop": true,
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true
"plugins": [
{
"name": "next"
}
],
"strictNullChecks": true
},
"exclude": ["node_modules"],
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"]
"include": ["next-env.d.ts", ".next/types/**/*.ts", "**/*.ts", "**/*.tsx"],
"exclude": ["node_modules"]
}
Loading

0 comments on commit 323255e

Please sign in to comment.