From f82d631ba6cb918779e05e7a435ac5b5208ff2f6 Mon Sep 17 00:00:00 2001 From: Dave Date: Mon, 21 Sep 2026 16:59:08 +0200 Subject: [PATCH] Foundation + Schemas --- astro.config.mjs | 13 +++- keystatic.config.ts | 6 +- package.json | 2 +- pnpm-lock.yaml | 76 +------------------ src/content.config.ts | 1 - .../general/articles/prima-materia.mdoc | 12 +++ src/content/general/articles/stages.mdoc | 12 +++ .../general/articles/the-crucible.mdoc | 15 ++++ src/content/general/pages/imprint.mdoc | 7 ++ src/content/general/tags/the-crucible.json | 4 + src/keystatic/collections/general/articles.ts | 30 ++++++++ src/keystatic/collections/general/index.ts | 11 +++ src/keystatic/collections/general/pages.ts | 23 ++++++ src/keystatic/collections/general/tags.ts | 21 +++++ src/keystatic/fields/content/base.ts | 40 ++++++++++ src/keystatic/fields/content/content.ts | 12 +++ src/keystatic/fields/content/cover.ts | 25 ++++++ src/keystatic/fields/content/dates.ts | 18 +++++ src/keystatic/fields/content/route.ts | 42 ++++++++++ src/keystatic/fields/content/summary.ts | 33 ++++++++ src/lib/constants/collections.ts | 5 ++ src/lib/constants/status.ts | 20 +++++ src/lib/content/assets.ts | 6 ++ src/lib/content/slug.ts | 34 +++++++++ tsconfig.json | 1 + 25 files changed, 391 insertions(+), 78 deletions(-) delete mode 100644 src/content.config.ts create mode 100644 src/content/general/articles/prima-materia.mdoc create mode 100644 src/content/general/articles/stages.mdoc create mode 100644 src/content/general/articles/the-crucible.mdoc create mode 100644 src/content/general/pages/imprint.mdoc create mode 100644 src/content/general/tags/the-crucible.json create mode 100644 src/keystatic/collections/general/articles.ts create mode 100644 src/keystatic/collections/general/index.ts create mode 100644 src/keystatic/collections/general/pages.ts create mode 100644 src/keystatic/collections/general/tags.ts create mode 100644 src/keystatic/fields/content/base.ts create mode 100644 src/keystatic/fields/content/content.ts create mode 100644 src/keystatic/fields/content/cover.ts create mode 100644 src/keystatic/fields/content/dates.ts create mode 100644 src/keystatic/fields/content/route.ts create mode 100644 src/keystatic/fields/content/summary.ts create mode 100644 src/lib/constants/collections.ts create mode 100644 src/lib/constants/status.ts create mode 100644 src/lib/content/assets.ts create mode 100644 src/lib/content/slug.ts diff --git a/astro.config.mjs b/astro.config.mjs index 2e0871b..8cdf238 100644 --- a/astro.config.mjs +++ b/astro.config.mjs @@ -2,13 +2,22 @@ import { defineConfig, fontProviders } from 'astro/config' import keystatic from '@keystatic/astro' -import markdoc from '@astrojs/markdoc' import node from '@astrojs/node' import react from '@astrojs/react' +const devOnly = (integration) => ({ + name: `${integration.name}:dev-only`, + hooks: { + 'astro:config:setup': async (params) => { + if (params.command !== 'dev') return + await integration.hooks['astro:config:setup']?.(params) + }, + }, +}) + export default defineConfig({ site: 'https://dave-dmg.de', - integrations: [react(), markdoc(), keystatic()], + integrations: [react(), devOnly(keystatic())], adapter: node({ mode: 'standalone', diff --git a/keystatic.config.ts b/keystatic.config.ts index ad94f79..6e916f5 100644 --- a/keystatic.config.ts +++ b/keystatic.config.ts @@ -1,8 +1,12 @@ import { config } from '@keystatic/core' +import { contentCollections } from '@/keystatic/collections/general' + export default config({ storage: { kind: 'local', }, - collections: {}, + collections: { + ...contentCollections, + }, }) diff --git a/package.json b/package.json index ab195b5..7b3bd12 100644 --- a/package.json +++ b/package.json @@ -12,11 +12,11 @@ "preview": "astro preview" }, "dependencies": { - "@astrojs/markdoc": "^2.0.1", "@astrojs/node": "^11.0.0", "@astrojs/react": "^6.0.0", "@keystatic/astro": "^6.0.0", "@keystatic/core": "^0.6.7", + "@markdoc/markdoc": "^0.5.10", "astro": "^7.0.4", "react": "^19.2.8", "react-dom": "^19.2.8" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d7135ba..8c63a45 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -11,9 +11,6 @@ importers: .: dependencies: - '@astrojs/markdoc': - specifier: ^2.0.1 - version: 2.0.9(@types/react@19.3.0)(astro@7.3.3(@emnapi/core@1.11.3)(@emnapi/runtime@1.11.3)(idb-keyval@6.3.0)(sugarss@5.0.1(postcss@8.5.28))(yaml@2.9.1))(react@19.3.0) '@astrojs/node': specifier: ^11.0.0 version: 11.1.6(astro@7.3.3(@emnapi/core@1.11.3)(@emnapi/runtime@1.11.3)(idb-keyval@6.3.0)(sugarss@5.0.1(postcss@8.5.28))(yaml@2.9.1)) @@ -26,6 +23,9 @@ importers: '@keystatic/core': specifier: ^0.6.7 version: 0.6.9(@keystar/ui@0.10.0(react-aria@3.50.0(react-dom@19.3.0(react@19.3.0))(react@19.3.0))(react-dom@19.3.0(react@19.3.0))(react-stately@3.48.0(react@19.3.0))(react@19.3.0))(react-aria@3.50.0(react-dom@19.3.0(react@19.3.0))(react@19.3.0))(react-dom@19.3.0(react@19.3.0))(react-stately@3.48.0(react@19.3.0))(react@19.3.0) + '@markdoc/markdoc': + specifier: ^0.5.10 + version: 0.5.10(@types/react@19.3.0)(react@19.3.0) astro: specifier: ^7.0.4 version: 7.3.3(@emnapi/core@1.11.3)(@emnapi/runtime@1.11.3)(idb-keyval@6.3.0)(sugarss@5.0.1(postcss@8.5.28))(yaml@2.9.1) @@ -208,12 +208,6 @@ packages: prettier-plugin-astro: optional: true - '@astrojs/markdoc@2.0.9': - resolution: {integrity: sha512-1w9Zg9czdfZXjBx9xv/eUy+65cOEr5AG6nsD36ZF3Eytir5Uip/0WpALLdOg6uN35wNbws+ZaibH5aAdC/6Jyg==} - engines: {node: '>=22.12.0'} - peerDependencies: - astro: ^7.0.0 - '@astrojs/markdown-satteri@0.4.1': resolution: {integrity: sha512-EniHbFNa6SQHDih7JnpPos3NWXO6hdt4raBcOosfGmlfc3gP9CI/sESA3VOf1PgJZ7SFfewlZW9Livn0JugEZg==} @@ -1694,32 +1688,16 @@ packages: dom-serializer@2.0.0: resolution: {integrity: sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==} - dom-serializer@3.1.1: - resolution: {integrity: sha512-4MEa38/QexBob6gFNwu+EGdWvhJ1OKuNwdYY3Y3NyeWDQfnGeDYQUDfIRzWu5B5gsv03so2Uxd28YC6zrsx3Lw==} - engines: {node: '>=20.19.0'} - domelementtype@2.3.0: resolution: {integrity: sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==} - domelementtype@3.0.0: - resolution: {integrity: sha512-umCQid3jKbDmVjx8jGaW7uUykm4DEUeyV21hPxNMo2nV955DhUThwqyOIDtreepP31hl84X7G5U9ZfsWvIB3Pg==} - engines: {node: '>=20.19.0'} - domhandler@5.0.3: resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==} engines: {node: '>= 4'} - domhandler@6.0.1: - resolution: {integrity: sha512-gYzvtM72ZtxQO0T048kd6HWSbbGCNOUwcnfQ01cqIJ4X2IYKFFHZ5mKvrQETcFXxsRObZulDaKmy//R7TPtsBg==} - engines: {node: '>=20.19.0'} - domutils@3.2.2: resolution: {integrity: sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==} - domutils@4.0.2: - resolution: {integrity: sha512-qI4JLRKnSzqFqr7hAlS5xQDusBCjKSEG4t4+7aNrIQMHBcsC2TGEhuyABJdYkgSewL57PNLYEiibY2iPKhKpaA==} - engines: {node: '>=20.19.0'} - dset@3.1.4: resolution: {integrity: sha512-2QF/g9/zTaPDc3BjNcVTGoBbXBgYfMTTceLaYcFJ/W9kggFUkhxD/hMEeuLKbugyef9SqAx8cpgwlIP/jinUTA==} engines: {node: '>=4'} @@ -1750,10 +1728,6 @@ packages: resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} engines: {node: '>=0.12'} - entities@8.1.0: - resolution: {integrity: sha512-kxL7msIffSuh9aaFAMD7rxAIuTRMAHMeBtgHW2yUdWw732ZNh4MehkF2gdjvtdmikkaIP9bFDDJOPlsvm7avrA==} - engines: {node: '>=20.19.0'} - env-paths@2.2.1: resolution: {integrity: sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==} engines: {node: '>=6'} @@ -2073,10 +2047,6 @@ packages: html-void-elements@3.0.0: resolution: {integrity: sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==} - htmlparser2@12.0.0: - resolution: {integrity: sha512-Tz7u1i95/g2x2jz81+x0FBVhBhY5aRTvD3tXXdFaljuNdzDLJ8UGNRrTcj2cgQvAg3iW/h77Fz15nLW0L0CrZw==} - engines: {node: '>=20.19.0'} - htmlparser2@9.1.0: resolution: {integrity: sha512-5zfg6mHUoaer/97TxnGpxmbR7zJtPwIYFMZ/H5ucTlPZhKvtum05yiPK3Mgai3a0DyVxv7qYqoweaEd2nrYQzQ==} @@ -3658,19 +3628,6 @@ snapshots: - '@emnapi/runtime' - typescript - '@astrojs/markdoc@2.0.9(@types/react@19.3.0)(astro@7.3.3(@emnapi/core@1.11.3)(@emnapi/runtime@1.11.3)(idb-keyval@6.3.0)(sugarss@5.0.1(postcss@8.5.28))(yaml@2.9.1))(react@19.3.0)': - dependencies: - '@astrojs/internal-helpers': 0.11.0 - '@astrojs/prism': 4.0.2 - '@markdoc/markdoc': 0.5.10(@types/react@19.3.0)(react@19.3.0) - astro: 7.3.3(@emnapi/core@1.11.3)(@emnapi/runtime@1.11.3)(idb-keyval@6.3.0)(sugarss@5.0.1(postcss@8.5.28))(yaml@2.9.1) - esbuild: 0.28.2 - github-slugger: 2.0.0 - htmlparser2: 12.0.0 - transitivePeerDependencies: - - '@types/react' - - react - '@astrojs/markdown-satteri@0.4.1': dependencies: '@astrojs/internal-helpers': 0.11.0 @@ -5212,36 +5169,18 @@ snapshots: domhandler: 5.0.3 entities: 4.5.0 - dom-serializer@3.1.1: - dependencies: - domelementtype: 3.0.0 - domhandler: 6.0.1 - entities: 8.1.0 - domelementtype@2.3.0: {} - domelementtype@3.0.0: {} - domhandler@5.0.3: dependencies: domelementtype: 2.3.0 - domhandler@6.0.1: - dependencies: - domelementtype: 3.0.0 - domutils@3.2.2: dependencies: dom-serializer: 2.0.0 domelementtype: 2.3.0 domhandler: 5.0.3 - domutils@4.0.2: - dependencies: - dom-serializer: 3.1.1 - domelementtype: 3.0.0 - domhandler: 6.0.1 - dset@3.1.4: {} ee-first@1.1.1: {} @@ -5263,8 +5202,6 @@ snapshots: entities@4.5.0: {} - entities@8.1.0: {} - env-paths@2.2.1: {} error-ex@1.3.4: @@ -5613,13 +5550,6 @@ snapshots: html-void-elements@3.0.0: {} - htmlparser2@12.0.0: - dependencies: - domelementtype: 3.0.0 - domhandler: 6.0.1 - domutils: 4.0.2 - entities: 8.1.0 - htmlparser2@9.1.0: dependencies: domelementtype: 2.3.0 diff --git a/src/content.config.ts b/src/content.config.ts deleted file mode 100644 index e0f9af2..0000000 --- a/src/content.config.ts +++ /dev/null @@ -1 +0,0 @@ -export const collections = {} diff --git a/src/content/general/articles/prima-materia.mdoc b/src/content/general/articles/prima-materia.mdoc new file mode 100644 index 0000000..e625dda --- /dev/null +++ b/src/content/general/articles/prima-materia.mdoc @@ -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: [] +--- diff --git a/src/content/general/articles/stages.mdoc b/src/content/general/articles/stages.mdoc new file mode 100644 index 0000000..532e8a9 --- /dev/null +++ b/src/content/general/articles/stages.mdoc @@ -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: [] +--- diff --git a/src/content/general/articles/the-crucible.mdoc b/src/content/general/articles/the-crucible.mdoc new file mode 100644 index 0000000..bf32d70 --- /dev/null +++ b/src/content/general/articles/the-crucible.mdoc @@ -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 +--- diff --git a/src/content/general/pages/imprint.mdoc b/src/content/general/pages/imprint.mdoc new file mode 100644 index 0000000..38f8a2d --- /dev/null +++ b/src/content/general/pages/imprint.mdoc @@ -0,0 +1,7 @@ +--- +title: Imprint +status: draft +noindex: false +route: {} +summary: {} +--- diff --git a/src/content/general/tags/the-crucible.json b/src/content/general/tags/the-crucible.json new file mode 100644 index 0000000..1644c52 --- /dev/null +++ b/src/content/general/tags/the-crucible.json @@ -0,0 +1,4 @@ +{ + "title": "The Crucible", + "description": "I AM JUST A ITSY-BITSY TEENY-WEENIE TEST" +} diff --git a/src/keystatic/collections/general/articles.ts b/src/keystatic/collections/general/articles.ts new file mode 100644 index 0000000..83ccc62 --- /dev/null +++ b/src/keystatic/collections/general/articles.ts @@ -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), + }, +}) diff --git a/src/keystatic/collections/general/index.ts b/src/keystatic/collections/general/index.ts new file mode 100644 index 0000000..0a11522 --- /dev/null +++ b/src/keystatic/collections/general/index.ts @@ -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, +} diff --git a/src/keystatic/collections/general/pages.ts b/src/keystatic/collections/general/pages.ts new file mode 100644 index 0000000..9c7810d --- /dev/null +++ b/src/keystatic/collections/general/pages.ts @@ -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), + }, +}) diff --git a/src/keystatic/collections/general/tags.ts b/src/keystatic/collections/general/tags.ts new file mode 100644 index 0000000..a4eb451 --- /dev/null +++ b/src/keystatic/collections/general/tags.ts @@ -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, + }), + }, +}) diff --git a/src/keystatic/fields/content/base.ts b/src/keystatic/fields/content/base.ts new file mode 100644 index 0000000..dbc932f --- /dev/null +++ b/src/keystatic/fields/content/base.ts @@ -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, +} diff --git a/src/keystatic/fields/content/content.ts b/src/keystatic/fields/content/content.ts new file mode 100644 index 0000000..7b8ef5a --- /dev/null +++ b/src/keystatic/fields/content/content.ts @@ -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), + }, + }) diff --git a/src/keystatic/fields/content/cover.ts b/src/keystatic/fields/content/cover.ts new file mode 100644 index 0000000..7e2bb61 --- /dev/null +++ b/src/keystatic/fields/content/cover.ts @@ -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', + }, + ) diff --git a/src/keystatic/fields/content/dates.ts b/src/keystatic/fields/content/dates.ts new file mode 100644 index 0000000..9f08760 --- /dev/null +++ b/src/keystatic/fields/content/dates.ts @@ -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', + }, +) diff --git a/src/keystatic/fields/content/route.ts b/src/keystatic/fields/content/route.ts new file mode 100644 index 0000000..241c413 --- /dev/null +++ b/src/keystatic/fields/content/route.ts @@ -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', + }, +) diff --git a/src/keystatic/fields/content/summary.ts b/src/keystatic/fields/content/summary.ts new file mode 100644 index 0000000..8042686 --- /dev/null +++ b/src/keystatic/fields/content/summary.ts @@ -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', + }, + ) diff --git a/src/lib/constants/collections.ts b/src/lib/constants/collections.ts new file mode 100644 index 0000000..6fc19cd --- /dev/null +++ b/src/lib/constants/collections.ts @@ -0,0 +1,5 @@ +/* === CONTENT COLLECTIONS === */ + +export const ARTICLES = 'articles' +export const PAGES = 'pages' +export const TAGS = 'tags' diff --git a/src/lib/constants/status.ts b/src/lib/constants/status.ts new file mode 100644 index 0000000..9646ebd --- /dev/null +++ b/src/lib/constants/status.ts @@ -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'] diff --git a/src/lib/content/assets.ts b/src/lib/content/assets.ts new file mode 100644 index 0000000..c847422 --- /dev/null +++ b/src/lib/content/assets.ts @@ -0,0 +1,6 @@ +export const ASSET_ROOT = 'src/assets/content' + +export const assets = (collection: string) => ({ + directory: `${ASSET_ROOT}/${collection}`, + publicPath: `/${ASSET_ROOT}/${collection}/`, +}) diff --git a/src/lib/content/slug.ts b/src/lib/content/slug.ts new file mode 100644 index 0000000..4bf4c91 --- /dev/null +++ b/src/lib/content/slug.ts @@ -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 = { + ä: '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, '') +} diff --git a/tsconfig.json b/tsconfig.json index c1167f5..895a067 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -7,6 +7,7 @@ "jsxImportSource": "react", "paths": { "@/*": ["./src/*"], + "~/*": ["./*"], }, }, }