keystatic icon indicating copy to clipboard operation
keystatic copied to clipboard

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

Open orlovol opened this issue 1 year ago • 2 comments

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;
---

<div class="bg-blue-500 text-black">Custom Keystatic component with Tailwind</div>
<Keystatic />
  1. Expand integration with option to provide custom Astro component, like so:
// /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 avatar Jun 20 '24 14:06 orlovol

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,
  });
});


orlovol avatar Jun 22 '24 19:06 orlovol

i think setting up the keystatic page and api route manually should also work, like here

stefanprobst avatar Aug 30 '24 09:08 stefanprobst