Foundation + Schemas

This commit is contained in:
2026-09-21 16:59:08 +02:00
parent 3a27b60be4
commit f82d631ba6
25 changed files with 391 additions and 78 deletions

View File

@@ -1 +0,0 @@
export const collections = {}

View File

@@ -0,0 +1,12 @@
---
title: Prima Materia
status: draft
noindex: false
route:
parent: stages
summary:
cover: {}
dates:
published: 2026-09-21T12:35:00.000Z
tags: []
---

View File

@@ -0,0 +1,12 @@
---
title: Stages
status: draft
noindex: false
route:
parent: the-crucible
summary:
cover: {}
dates:
published: 2026-09-21T12:34:00.000Z
tags: []
---

View File

@@ -0,0 +1,15 @@
---
title: The Crucible
status: draft
noindex: false
route: {}
summary:
excerpt: >-
Where we explore the crucible – a materialistic alchemist Worldbuilding
Framework
cover: {}
dates:
published: 2026-09-21T12:33:00.000Z
tags:
- the-crucible
---

View File

@@ -0,0 +1,7 @@
---
title: Imprint
status: draft
noindex: false
route: {}
summary: {}
---

View File

@@ -0,0 +1,4 @@
{
"title": "The Crucible",
"description": "I AM JUST A ITSY-BITSY TEENY-WEENIE TEST"
}

View File

@@ -0,0 +1,30 @@
import { collection, fields } from '@keystatic/core'
import { base } from '@/keystatic/fields/content/base'
import { content } from '@/keystatic/fields/content/content'
import { dates } from '@/keystatic/fields/content/dates'
import { articleRoute } from '@/keystatic/fields/content/route'
import { articleSummary } from '@/keystatic/fields/content/summary'
import { ARTICLES, TAGS } from '@/lib/constants/collections'
export const articles = collection({
label: 'Articles',
slugField: 'title',
path: `src/content/general/${ARTICLES}/*`,
format: {
contentField: 'content',
},
entryLayout: 'content',
columns: ['title', 'status'],
schema: {
...base,
route: articleRoute,
summary: articleSummary(ARTICLES),
dates,
tags: fields.multiRelationship({
label: 'Tags',
collection: TAGS,
}),
content: content(ARTICLES),
},
})

View File

@@ -0,0 +1,11 @@
import { ARTICLES, PAGES, TAGS } from '@/lib/constants/collections'
import { articles } from './articles'
import { pages } from './pages'
import { tags } from './tags'
export const contentCollections = {
[ARTICLES]: articles,
[PAGES]: pages,
[TAGS]: tags,
}

View File

@@ -0,0 +1,23 @@
import { collection } from '@keystatic/core'
import { base } from '@/keystatic/fields/content/base'
import { content } from '@/keystatic/fields/content/content'
import { pageRoute } from '@/keystatic/fields/content/route'
import { pageSummary } from '@/keystatic/fields/content/summary'
import { PAGES } from '@/lib/constants/collections'
export const pages = collection({
label: 'Pages',
slugField: 'title',
path: `src/content/general/${PAGES}/*`,
format: {
contentField: 'content',
},
columns: ['title', 'status'],
schema: {
...base,
route: pageRoute,
summary: pageSummary,
content: content(PAGES),
},
})

View File

@@ -0,0 +1,21 @@
import { collection, fields } from '@keystatic/core'
import { title } from '@/keystatic/fields/content/base'
import { TAGS } from '@/lib/constants/collections'
export const tags = collection({
label: 'Tags',
slugField: 'title',
path: `src/content/general/${TAGS}/*`,
format: {
data: 'json',
},
columns: ['title'],
schema: {
title,
description: fields.text({
label: 'Description',
multiline: true,
}),
},
})

View File

@@ -0,0 +1,40 @@
import { fields } from '@keystatic/core'
import { STATUS_OPTIONS } from '@/lib/constants/status'
import { SEGMENT, SEGMENT_MESSAGE, slugify } from '@/lib/content/slug'
export const title = fields.slug({
name: {
label: 'Title',
validation: {
isRequired: true,
},
},
slug: {
label: 'File ID',
description: 'Internal and unique across the collection.',
generate: slugify,
validation: {
pattern: {
regex: SEGMENT,
message: SEGMENT_MESSAGE,
},
},
},
})
export const status = fields.select({
label: 'Status',
options: STATUS_OPTIONS,
defaultValue: 'draft',
})
export const noindex = fields.checkbox({
label: 'Hide from search engines',
})
export const base = {
title,
status,
noindex,
}

View File

@@ -0,0 +1,12 @@
import { fields } from '@keystatic/core'
import { assets } from '@/lib/content/assets'
export const content = (collection: string) =>
fields.markdoc({
label: 'Content',
options: {
heading: [2, 3, 4],
image: assets(collection),
},
})

View File

@@ -0,0 +1,25 @@
import { fields } from '@keystatic/core'
import { assets } from '@/lib/content/assets'
export const cover = (collection: string) =>
fields.object(
{
image: fields.image({
label: 'Image',
...assets(collection),
}),
alt: fields.text({
label: 'Alt text',
}),
caption: fields.text({
label: 'Caption',
}),
credit: fields.text({
label: 'Credit',
}),
},
{
label: 'Cover',
},
)

View File

@@ -0,0 +1,18 @@
import { fields } from '@keystatic/core'
export const dates = fields.object(
{
published: fields.datetime({
label: 'Published',
defaultValue: {
kind: 'now',
},
}),
updated: fields.datetime({
label: 'Updated',
}),
},
{
label: 'Dates',
},
)

View File

@@ -0,0 +1,42 @@
import { fields } from '@keystatic/core'
import { ARTICLES } from '@/lib/constants/collections'
import { OPTIONAL_SEGMENT, SEGMENT_MESSAGE } from '@/lib/content/slug'
const segment = fields.text({
label: 'URL segment',
description: 'Optional- Leave empty to use the slugified title',
validation: {
pattern: {
regex: OPTIONAL_SEGMENT,
message: SEGMENT_MESSAGE,
},
},
})
export const pageRoute = fields.object(
{
segment,
},
{
label: 'Route',
},
)
export const articleRoute = fields.object(
{
segment,
parent: fields.relationship({
label: 'Parent',
description: 'Empty makes this a root',
collection: ARTICLES,
}),
index: fields.integer({
label: 'Index',
description: 'Sorts among siblings. Empty sorts by date, newest first',
}),
},
{
label: 'Route',
},
)

View File

@@ -0,0 +1,33 @@
import { fields } from '@keystatic/core'
import { cover } from './cover'
const excerpt = fields.text({
label: 'Excerpt',
multiline: true,
validation: {
length: {
max: 160,
},
},
})
export const pageSummary = fields.object(
{
excerpt,
},
{
label: 'Summary',
},
)
export const articleSummary = (collection: string) =>
fields.object(
{
excerpt,
cover: cover(collection),
},
{
label: 'Summary',
},
)

View File

@@ -0,0 +1,5 @@
/* === CONTENT COLLECTIONS === */
export const ARTICLES = 'articles'
export const PAGES = 'pages'
export const TAGS = 'tags'

View File

@@ -0,0 +1,20 @@
export const STATUS_OPTIONS = [
{
label: 'Draft',
value: 'draft',
},
{
label: 'Published',
value: 'published',
},
{
label: 'Unlisted',
value: 'unlisted',
},
{
label: 'Archived',
value: 'archived',
},
] as const
export type Status = (typeof STATUS_OPTIONS)[number]['value']

View File

@@ -0,0 +1,6 @@
export const ASSET_ROOT = 'src/assets/content'
export const assets = (collection: string) => ({
directory: `${ASSET_ROOT}/${collection}`,
publicPath: `/${ASSET_ROOT}/${collection}/`,
})

34
src/lib/content/slug.ts Normal file
View File

@@ -0,0 +1,34 @@
export const SEGMENT = /^[a-z0-9][a-z0-9_-]*$/
export const OPTIONAL_SEGMENT = /^([a-z0-9][a-z0-9_-]*)?$/
export const SEGMENT_MESSAGE = 'Lowercase letters, digits, - and _'
const UMLAUTS: Record<string, string> = {
ä: 'ae',
ö: 'oe',
ü: 'ue',
ß: 'ss',
Ä: 'ae',
Ö: 'oe',
Ü: 'ue',
å: 'aa',
æ: 'ae',
ø: 'oe',
Å: 'aa',
Æ: 'ae',
Ø: 'oe',
}
export const slugify = (text: string): string => {
return text
.split('')
.map((char) => UMLAUTS[char] || char)
.join('')
.normalize('NFKD')
.replace(/[\u0300-\u036f]/g, '')
.toLowerCase()
.trim()
.replace(/[^a-z0-9 _-]/g, '')
.replace(/\s+/g, '-')
.replace(/-+/g, '-')
.replace(/^[-_]+|[-_]+$/g, '')
}