Getting Started with Directus and SolidStart
Published January 24th, 2024
SolidStart a meta-framework built around SolidJS - SolidStart is a framework aiming to allow people and teams to built high-end applications without being too opinionated. In this tutorial, you will learn how to build a website using Directus as a CMS. You will store, retrieve, and use global metadata such as the site title, create new pages dynamically based on Directus items, and build a blog.
Before You Start
You will need:
- Node.js and a code editor.
- A Directus project - follow our quickstart guide if you don't already have one.
- (Bonus) SolidJS knowledge will definitely help, but is not required.
Initialize SolidStart Project
Open your terminal and run the following commands to create a new Solid project and add the Directus JavaScript SDK:
npm create solid@latest solid-directus
✔ Which template do you want to use?
› bare
✔ Server Side Rendering?
› yes
✔ Use TypeScript?
› no
npm create solid@latest solid-directus
✔ Which template do you want to use?
› bare
✔ Server Side Rendering?
› yes
✔ Use TypeScript?
› no
Now change directory to what you created and install all dependencies while adding the Directus SDK.
cd solid-directus && npm install @directus/sdk
cd solid-directus && npm install @directus/sdk
With this, we are ready to start coding!
npm run dev --open
npm run dev --open
Your browser will open on localhost:3000 or the next available port.
Create a Directus Helper
We now need to setup the Directus SDK. We will add our data fetchers in this same file so we can handle caching and requests all at the same place. Create a new file directus.js
inside of the src/libs
directory:
import { createDirectus, readItem, readItems, rest } from "@directus/sdk";
import { cache } from "@solidjs/router";
export const directus = createDirectus("YOUR_DIRECTUS_PROJECT_URL").with(rest());
import { createDirectus, readItem, readItems, rest } from "@directus/sdk";
import { cache } from "@solidjs/router";
export const directus = createDirectus("YOUR_DIRECTUS_PROJECT_URL").with(rest());
We have some unused import
statements at the moment since now we are only instantiating the Directus client.
Provide Your Directus Project URL
Remember to replace YOUR_DIRECTUS_PROJECT_URL
with your real project URL.
Using Global Metadata and Settings
In your Directus project, navigate to Settings -> Data Model and create a new collection called global. Under the Singleton option, select 'Treat as a single object', as this collection will have just a single entry containing global website metadata.
By default, new collections are not accessible to the public. Navigate to Settings -> Access Control -> Public and give Read access to the Global collection.
Fetch Global Data
Now that the table is created and we have data added, let's create our first fetcher. In /lib/directus.js
we will add a new method:
export const getGlobals = cache(async () => {
"use server";
try {
return await directus.request.readItems("global");
} catch (e) {
console.error(e);
return null;
}
}, "globals");
export const getGlobals = cache(async () => {
"use server";
try {
return await directus.request.readItems("global");
} catch (e) {
console.error(e);
return null;
}
}, "globals");
Notice the "use server"
directive. This indicates the bundler this method will never run (and therefore will not exist) in the client-side build. The cache()
method wrapper defines a cache-key ("globals") where this response will be stored. We will try the request to Directus, if the request is not successful we can throw a 404
.
With our fetcher, we can head over to our src/routes/index.jsx
and create our first loader request.
export const route = {
load: () => getGlobals(),
};
export default function Home() {
const pageInfo = createAsync(getGlobals);
createEffect(() => {
if (pageInfo() === null) {
throw new Error("404");
}
});
return (
<main>
<Show when={pageInfo()}>{(info) => <h1>{info().title}</h1>}</Show>
<Show when={pageInfo()}>
{(info) => <strong>{info().description}</strong>}
</Show>
</main>
);
}
export const route = {
load: () => getGlobals(),
};
export default function Home() {
const pageInfo = createAsync(getGlobals);
createEffect(() => {
if (pageInfo() === null) {
throw new Error("404");
}
});
return (
<main>
<Show when={pageInfo()}>{(info) => <h1>{info().title}</h1>}</Show>
<Show when={pageInfo()}>
{(info) => <strong>{info().description}</strong>}
</Show>
</main>
);
}
The route
object defines a load
method that will be called before our route is accessed. Then createAsync
will receive the deferred data as a signal and allow our UI to update. The <Show>
component is important to make sure we only render our elements once the data has finally arrived at the browser.
Dynamic Routes With Directus
Create a new collection called pages - make the Primary ID Field a "Manually Entered String" called slug, which will correlate with the URL for the page. For example, about
will later correlate to the page localhost:3000/about.
Create a text input field called title and a WYSIWYG input field called content. In the Access Control settings, give the Public role read access to the new collection. Create 3 items in the new collection - here's some sample data.
Open src/lib/directus.js
and create our new fetcher:
export const getPage = (slug) =>
cache(async () => {
"use server";
try {
return await directus.request(readItem("pages", slug));
} catch (e) {
return null;
}
}, slug);
export const getPage = (slug) =>
cache(async () => {
"use server";
try {
return await directus.request(readItem("pages", slug));
} catch (e) {
return null;
}
}, slug);
This method attempts to fetch information from a given slug
. If there is no information, it will return null
.
Inside of the src/routes
directory, create a new a file called slug.jsx
. This is a dynamic route, so a single file can be used for all of the top-level pages.
import { createAsync, useParams } from "@solidjs/router";
import { Show, createEffect } from "solid-js";
import { getPage } from "~/libs/directus";
export const route = {
load: (loc) => {
if (loc.pathname) {
return getPage(loc.pathname.substring(1));
}
},
};
export default function UserPage() {
const param = useParams();
const page = createAsync(getPage(param?.slug));
createEffect(() => {
if (page() === null) {
throw new Error("404");
}
});
return (
<Show when={page()}>
{(pageInfo) => (
<>
<h1>{pageInfo().title}</h1>
<p innerHTML={pageInfo().content} />
</>
)}
</Show>
);
}
import { createAsync, useParams } from "@solidjs/router";
import { Show, createEffect } from "solid-js";
import { getPage } from "~/libs/directus";
export const route = {
load: (loc) => {
if (loc.pathname) {
return getPage(loc.pathname.substring(1));
}
},
};
export default function UserPage() {
const param = useParams();
const page = createAsync(getPage(param?.slug));
createEffect(() => {
if (page() === null) {
throw new Error("404");
}
});
return (
<Show when={page()}>
{(pageInfo) => (
<>
<h1>{pageInfo().title}</h1>
<p innerHTML={pageInfo().content} />
</>
)}
</Show>
);
}
Dynamic Route Precedence
It's important to note that exact matches take precedence over dynamic routes. If it exists, a about.jsx
file will render instead of [slug].jsx
for the /about
URL.
Creating Blog Posts
Create a new collection called authors
with a single text input field called name
. Create one or more authors.
Then, create a new collection called posts
- make the Primary ID Field a "Manually Entered String" called slug
, which will correlate with the URL for the page. For example, hello-world
will later correlate to the page localhost:3000/blog/hello-world
.
Create the following fields in your posts data model:
- a text input field called
title
. - a WYSIWYG input field called
content
. - an image relational field called
image
. - a datetime selection field called
publish_date
- set the type to 'date'. - a many-to-one relational field called
author
with the related collection set toauthors
.
In the Access Control settings, give the Public role read access to the authors
, posts
, and directus_files
collections.
Create 3 items in the posts collection - here's some sample data.
List Blog Posts
Open src/lib/directus.js
, add a helper to request all items from the posts
collection:
export const getPosts = cache(async () => {
"use server";
try {
const post = await directus.request(
readItems("posts", {
fields: ["slug", "title", "publish_date", { author: ["name"] }],
sort: ["-publish_date"],
})
);
return post;
} catch (error) {
return null;
}
}, "post-list");
export const getPosts = cache(async () => {
"use server";
try {
const post = await directus.request(
readItems("posts", {
fields: ["slug", "title", "publish_date", { author: ["name"] }],
sort: ["-publish_date"],
})
);
return post;
} catch (error) {
return null;
}
}, "post-list");
This query will retrieve the first 100 items (default), sorted by publishing date (descending order, which is latest first). It will only return the specific fields we request - slug
, title
, publish_date
, and the name
from the related author
item.
This fetcher should not throw an error, and instead show an empty list if the request is not successful. Inside of the src/routes
directory, now create a blog
directory. And within this newly created directory, an index.jsx
file:
import { A, createAsync } from "@solidjs/router";
import { For, Show, createEffect } from "solid-js";
import { getPosts } from "~/libs/directus";
export const route = {
load: () => getPosts,
};
export default function PostsList() {
const list = createAsync(getPosts);
return (
<Show when={list()}>
{(posts) => (
<ul>
<For each={posts()}>
{(post) => (
<li>
<A href={`/blog/${post.slug}`}>
<h2>{post.title}</h2>
</A>
<span>
{post.publish_date} • {post.author.name}
</span>
</li>
)}
</For>
</ul>
)}
</Show>
);
}
import { A, createAsync } from "@solidjs/router";
import { For, Show, createEffect } from "solid-js";
import { getPosts } from "~/libs/directus";
export const route = {
load: () => getPosts,
};
export default function PostsList() {
const list = createAsync(getPosts);
return (
<Show when={list()}>
{(posts) => (
<ul>
<For each={posts()}>
{(post) => (
<li>
<A href={`/blog/${post.slug}`}>
<h2>{post.title}</h2>
</A>
<span>
{post.publish_date} • {post.author.name}
</span>
</li>
)}
</For>
</ul>
)}
</Show>
);
}
Add Navigation
While not strictly Directus-related, there are now several pages that aren't linked to each other. Create a src/routes/app.jsx
file to add a navigation above the main content. Don't forget to use your specific page slugs.
Within the root
attribute, we can wrap our app within an <ErrorBoundary>
. That way every time we throw within a component will be caught by our boundary:
// @refresh reload
import { A, Router } from "@solidjs/router";
import { FileRoutes } from "@solidjs/start";
import { ErrorBoundary, Suspense } from "solid-js";
import NotFound from "./components/NotFound";
export default function App() {
return (
<Router
root={(props) => (
<ErrorBoundary fallback={(err) => <NotFound code={err} />}>
<nav>
<A href="/">Home</A>
<A href="/about">About</A>
<A href="/blog">Blog</A>
</nav>
<Suspense>{props.children}</Suspense>
</ErrorBoundary>
)}
>
<FileRoutes />
</Router>
);
}
// @refresh reload
import { A, Router } from "@solidjs/router";
import { FileRoutes } from "@solidjs/start";
import { ErrorBoundary, Suspense } from "solid-js";
import NotFound from "./components/NotFound";
export default function App() {
return (
<Router
root={(props) => (
<ErrorBoundary fallback={(err) => <NotFound code={err} />}>
<nav>
<A href="/">Home</A>
<A href="/about">About</A>
<A href="/blog">Blog</A>
</nav>
<Suspense>{props.children}</Suspense>
</ErrorBoundary>
)}
>
<FileRoutes />
</Router>
);
}
Next Steps
Through this guide, you have set up a SolidStart project, created a Directus helper, and used it to query data. You have used a singleton collection for global metadata, dynamically created pages, as well as blog listing and post pages.
If you want to change what is user-accessible, consider setting up more restrictive roles and accessing only valid data at build-time. Additionally, it may be useful to create an .env
file and store the URL there in case you use different projects for production, development, etc. Otherwise it is not a private value, access is managed through the Directus dashboard.