SolidStart

A blog with server queries.

View as Markdown

A blog whose posts are read in "use server" queries, so a page gets only what it renders. Open the example in StackBlitz.

Setup

Add the plugin before SolidStart's:

vite.config.ts
import { solidStart } from "@solidjs/start/config";
import { nitro } from "nitro/vite";
import { tomekit } from "tomekit/vite";
import { defineConfig } from "vite";

export default defineConfig({
  plugins: [tomekit(), solidStart(), nitro()],
});
tsconfig.json
{
  "compilerOptions": {
    "paths": {
      "~/*": ["./src/*"],
      "tomekit/content*": ["./.tomekit/content*"]
    }
  }
}

The config

tomekit.config.ts
import { marked } from "marked";
import { defineConfig, directory } from "tomekit";
import { z } from "zod";

export default defineConfig({
  collections: {
    posts: {
      loader: directory("content/posts"),
      schema: z.strictObject({
        description: z.string(),
        publishedAt: z.coerce.date(),
        title: z.string(),
      }),
      transform: ({ body }) => ({ body: marked.parse(body, { async: false }) }),
    },
  },
});

The queries

"use server" keeps the collection on the server; the index gets titles, not every post:

src/lib/posts.ts
import { query } from "@solidjs/router";
import { posts } from "tomekit/content";

const getPosts = query(async () => {
  "use server";

  return posts
    .documents()
    .toSorted(
      (a, b) =>
        b.metadata.publishedAt.getTime() - a.metadata.publishedAt.getTime()
    )
    .map(({ metadata, slug }) => ({ slug, title: metadata.title }));
}, "posts");

const getPost = query(async (slug: string) => {
  "use server";

  return posts.get(slug);
}, "post");

export { getPost, getPosts };

The routes

get takes the slug from the URL, a plain string, so it may return undefined; render a 404 for it:

src/routes/posts/[slug].tsx
import { createAsync, useParams } from "@solidjs/router";
import { HttpStatusCode } from "@solidjs/start";
import { Show } from "solid-js";

import { getPost } from "~/lib/posts";

export default function Post() {
  const params = useParams<{ slug: string }>();
  const post = createAsync(() => getPost(params.slug));

  return (
    <Show
      fallback={
        <>
          <HttpStatusCode code={404} />
          <h1>Post not found</h1>
        </>
      }
      when={post()}
    >
      {(post) => (
        <article>
          <h1>{post().metadata.title}</h1>
          <div innerHTML={post().body} />
        </article>
      )}
    </Show>
  );
}

publishedAt arrives in the page as a Date: server functions serialize with seroval, which keeps it one.