Thinkmill / keystatic

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

Tailwind styles aren't loaded for Keystatic UI custom components in Astro #1189

Open orlovol opened 2 weeks ago

orlovol commented 2 weeks ago

Hi!

I've been trying to setup a Footnote custom mark component, as written here. There's a sample repo using Next, but I had an issue getting the same result for Astro.

Basic idea is adding a custom component with tailwind classes:

// keystatic.config.tsx

// config.collections.<name>.schema
content: fields.markdoc({
  label: "Text",
  components: {
    Footnote: mark({
      label: "Footnote",
      icon: <SuperscriptIcon />,
      className: "align-super text-xs",
      schema: {},
    }),
  },
}),

But when the text is marked in UI, styles aren't applied, because Keystatic Astro component has no idea about the styles file.

How I think this can be fixed

(some steps can be automated by keystatic integration, others might need user input)

  1. Add "keystatic.config.tsx" to tailwind content config, so the custom component styles are picked up into global css
  2. Add a custom Astro component, like so:
    
    ---
    // src/components/MyKeystatic.astro

import Keystatic from '@keystatic/astro/internal/keystatic-astro-page.astro'; import "styles/globals.css";

export const prerender = false;

Custom Keystatic component with Tailwind
3. Expand integration with option to provide custom Astro component, like so:
```js
// /node_modules/@keystatic/astro/dist/keystatic-astro.js

function keystatic(options) {
  return {
    name: 'keystatic',
    hooks: {
      'astro:config:setup': ({
        ...
        const astroPage = options.astroPage ?? '@keystatic/astro/internal/keystatic-astro-page.astro'
        logger.info(`Using ${astroPage} as Keystatic component`)
...
  1. Configure Astro integration:
    
    // astro.config.mjs

integrations: [ markdoc(), keystatic({ astroPage: "src/components/MyKeystatic.astro" }), tailwind(), ]



As a result, tailwind CSS styles are loaded and applied to custom components in the editor UI.

If this approach makes sense — I'll gladly open a PR. Thanks!
orlovol commented 1 week ago

Thought about this a bit more, found a workaround that could work without changes to Keystatic: By adding Astro middleware for keystatics routes, we can prepend the styles to the Astro island with Keystatic UI, by importing tailwind css content and injecting it into Astro styles. There might be a better way, but this works!

It's especially helpful for custom content components:

// keystatic.config.tsx
...
content: fields.markdoc({
  label: "Body",
  components: {
    Badge: inline({
      label: "Footnote",
      schema: {
        foo: fields.number({ label: "My Foo" }),
      },
      NodeView: ({ value }) => (
        <Badge variant="secondary"> Foo: #{value.foo} </Badge>
      ),
  })

Attaching the middleware code:

?url in style path is needed for vite to build for production, otherwise it produces src/middleware.ts (2:7): "default" is not exported by "src/styles/globals.css", imported by "src/middleware.ts".

// /src/middleware.ts
import { defineMiddleware } from "astro:middleware";
import css from "./styles/globals.css?url";

export const onRequest = defineMiddleware(async (context, next) => {
  const response = await next();

  if (!context.url.pathname.startsWith("/keystatic")) {
    return response;
  }

  const html = await response.text();
  const redactedHtml = html.replace("<!DOCTYPE html>", `<!DOCTYPE html><meta charset="utf-8"/><link rel="stylesheet" href="${css}" />`);

  return new Response(redactedHtml, {
    status: 200,
    headers: response.headers,
  });
});