Thinkmill / keystatic

First class CMS experience, TypeScript API, Markdown & YAML/JSON based, no DB
https://keystatic.com
MIT License
1.03k stars 68 forks source link

Need help with Astro (zod) collection conversion to Keystatic #1115

Open neo-art opened 2 months ago

neo-art commented 2 months ago

Hello all, I am new to Keystatic and Astro and hope to get some help. I am trying to add Keystatic to an existing Astro project but I am stuck with converting the existing ZOD collections to Keystatic. This is the original theme Astro schema (config.ts):

import { defineCollection, z } from 'astro:content';

const seoSchema = z.object({
    title: z.string().min(5).max(120).optional(),
    description: z.string().min(15).max(160).optional(),
    image: z
        .object({
            src: z.string(),
            alt: z.string().optional()
        })
        .optional(),
    pageType: z.enum(['website', 'article']).default('website')
});

const blog = defineCollection({
    schema: z.object({
        title: z.string(),
        excerpt: z.string().optional(),
        publishDate: z.coerce.date(),
        updatedDate: z.coerce.date().optional(),
        isFeatured: z.boolean().default(false),
        tags: z.array(z.string()).default([]),
        seo: seoSchema.optional()
    })
});

const pages = defineCollection({
    schema: z.object({
        title: z.string(),
        seo: seoSchema.optional()
    })
});

const projects = defineCollection({
    schema: z.object({
        title: z.string(),
        description: z.string().optional(),
        publishDate: z.coerce.date(),
        isFeatured: z.boolean().default(false),
        seo: seoSchema.optional()
    })
});

export const collections = { blog, pages, projects };

And this is what I have done so far to the keystatic.config.ts

// keystatic.config.ts
import { config, fields, collection, singleton } from '@keystatic/core';

import { z } from 'zod';

export default config({
    storage: {
        kind: 'local'
    },
    ui: {
        brand: {
            name: '2 Aquarius' // NAME OF YOUR SITE
        }
    },
    collections: {
        blog: collection({
            label: 'Blog',
            slugField: 'title',
            path: 'src/content/blog/*',
            entryLayout: 'content',
            columns: ['title', 'publishDate'],
            format: { contentField: 'content' },
            schema: {
                title: fields.slug({ name: { label: 'Title' } }),
                excerpt: fields.text({
                    label: 'Excerpt',
                    multiline: true
                }),
                description: fields.text({
                    label: 'Description',
                    multiline: true
                }),
                publishDate: fields.date({
                    defaultValue: { kind: 'today' },
                    label: 'Date of the publication'
                }),
                updatedDate: fields.date({
                    label: 'Updated date',
                    description: 'Date when the article was updated',
                    validation: {
                        isRequired: false
                    }
                }),
                isFeatured: fields.checkbox({
                    label: 'Is featured?',
                    defaultValue: false
                }),
                tags: fields.array(
                    fields.text({ label: 'Tags' }),
                    // Labelling options
                    {
                        label: 'Tags',
                        itemLabel: (props) => props.value
                    }
                ),
                // seoSchema: fields.text({
                //     label: 'seoSchema',
                //     multiline: true,
                //     description: 'seoSchema',
                //     validation: {
                //         isRequired: true,
                //         length: {
                //             min: 5,
                //             max: 120
                //         }
                //     }
                // }),
                seo: fields.relationship({
                    label: 'Seo',
                    collection: 'seoSchema',
                    validation: {
                        isRequired: false
                    }
                }),
                content: fields.markdoc({
                    label: 'Content',
                    extension: 'md'
                    // formatting: true,
                    // dividers: true,
                    // links: true,
                    // images: true,
                })
            }
        }),

        seoSchema: collection({
            label: 'seoSchema',
            slugField: 'title',
            path: 'src/content/seoSchema/*',
            schema: {
                title: fields.slug({ name: { label: 'Title' } }),
                description: fields.text({
                    label: 'seoSchema Description',
                    multiline: true,
                    validation: {
                        isRequired: false,
                        length: {
                            min: 5,
                            max: 120
                        }
                    }
                }),
                image: fields.image({
                    label: 'Image',
                    directory: 'src/assets/images/pages',
                    publicPath: '../../assets/images/pages/'
                }),
                imageAlt: fields.text({
                    label: 'ImageAlt',
                    validation: {
                        isRequired: false
                    }
                }),
                pageType: fields.select({
                    label: 'Page Type',
                    description: 'Type of this page',
                    options: [
                        { label: 'Website', value: 'website' },
                        { label: 'Article', value: 'article' }
                    ],
                    defaultValue: 'website'
                })
            }
        }),

        projects: collection({
            label: 'Projects',
            slugField: 'title',
            path: 'src/content/projects/*',
            format: { contentField: 'content' },
            schema: {
                title: fields.text({ label: 'Projects headline' }),
                description: fields.text({
                    label: 'Description',
                    multiline: true
                }),
                publishDate: fields.date({
                    defaultValue: { kind: 'today' },
                    label: 'Date of the publication'
                }),
                isFeatured: fields.checkbox({
                    label: 'Is featured?',
                    defaultValue: false
                }),
                content: fields.markdoc({
                    label: 'Content',
                    extension: 'md'
                    // formatting: true,
                    // dividers: true,
                    // links: true,
                    // images: true,
                }),
                seo: fields.relationship({
                    label: 'SEO',
                    collection: 'seoSchema'
                })
            }
        }),

        pages: collection({
            label: 'Pages',
            slugField: 'title',
            path: 'src/content/pages/*',
            entryLayout: 'content',
            columns: ['title', 'description'],
            format: { contentField: 'content' },
            schema: {
                title: fields.slug({ name: { label: 'SEO Title' } }),
                description: fields.text({
                    label: 'SEO Description',
                    multiline: true
                }),
                seo: fields.relationship({
                    label: 'SEO',
                    collection: 'seoSchema',
                    validation: {
                        isRequired: false
                    }
                }),
                ogImage: fields.image({
                    label: 'Image',
                    directory: 'src/assets/images/pages',
                    publicPath: '../../assets/images/pages/'
                }),
                // noIndex: fields.checkbox({
                //   label: "Don't index the page",
                //   defaultValue: false,
                // }),
                content: fields.markdoc({
                    label: 'Content',
                    extension: 'md',
                    options: {
                        image: {
                            directory: 'src/assets/images/pages',
                            publicPath: '../../assets/images/pages/'
                        }
                    }
                })
            }
        })
    }
});
neo-art commented 2 months ago

Unfortunately, I am not able to edit the old posts or pages. I got the following error message when I tried to access the relevant pages/posts in the Admin UI:

Field validation failed: seo: Must be a string

Any help is appreciated.

Link to the repo: https://github.com/neo-art/dante-astro-theme-2AQ.git

neo-art commented 2 months ago

Any help guys? Or some workaround?

Matthijz98 commented 1 month ago

I think the problem is with the front matter of the page.

If i look at your repo some pages have this front matter

---
title: About
seo:
  title: About Me
  description: Learn more about the person behind the website and embark on a journey of inspiration and shared experiences.
  image:
    src: '/about.jpeg'
    alt: A person sitting at a desk in front of a computer
---

And other:

---
title: Page test 1
description: SEO DESC
ogImage: ../../assets/images/pages/page-test-1/ogImage.jpg
---

Your config says that the seo if a relation so it should be something like this:

---
title: About
seo: test-set
---

Then a test-set.yaml should exist in your seoSchema content collection

neo-art commented 1 month ago

@Matthijz98 thank you for the comment and I appreciate your taking the time to review it. Unfortunately, I couldn't get how to fix it :( The file "Page test 1" was left from previous tests and I removed it.

Anyway a few days ago I made some changes to the config (changed it to . TSX) and edited the Keystatic configuration, you can see it here: keystatic. config.tsx

Still the same error message, no matter what I try and now can't even add a new post in Keystatic admin (blank page).

I guess the problem is that I can't match the ZOD schema in Keystatic config file but not sure how to fix it. More specifically this part of Astro config.ts:

const seoSchema = z.object({
    title: z.string().min(5).max(120).optional(),
    description: z.string().min(15).max(160).optional(),
    image: z
        .object({
            src: z.string(),
            alt: z.string().optional()
        })
        .optional(),
    pageType: z.enum(['website', 'article']).default('website')
});