Creating a Parent/Child Taxonomy - has 5 likes
Create common taxonomy schemas like Categories and Tags with parent/child relationships
Go to Creating a Parent/Child TaxonomyRemix is a new framework for creating React websites with a server/client approach instead of a static build step. In this guide, we'll hook up Sanity.io for content as well as Live Preview for a seamless content editing experience.
To follow this tutorial you will need:
The complete, finished version of the code produced in this guide is available for download from the sanity-remix-preview repository. Run sanity init
from inside the ./studio
folder to create a new project or to connect it with an existing one.
Remix has a focus on server-side logic of website requests. If you've only been building Jamstack sites might feel like a new concept – if you predate the Jamstack you'll feel right at home!
Sanity.io is a platform for structured content with a fully featured API. You build your content model in schema files, and the Studio compiles into a great editing interface which you can deploy to the hosting of your choice.
When requests to your website are made, we can check for authentication and serve either published content or put the page into preview mode and continually listen to and show updates. No serverless function step is required.
For our Next.js live preview guide we use the next-sanity
toolkit to query, preview and display Sanity content. In this guide, we'll set up those components individually to achieve the same result.
The package groq-store
streams the entire dataset to your browser and overlays draft content into published documents.
If you have an existing Sanity Studio, you can skip this step.
You could use this guide to add live preview and data fetching to an existing Remix project, but we'll focus on creating something completely new.
Let's start a new project, in a folder my-project
Create a new folder, in it create a studio
folder and run sanity init
inside to start a new Sanity project. In this guide we'll use the movies
dataset to get started faster.
If you have an existing Remix project, skip this step.
Back in the my-project
folder, run npx create-remix
and call the project web
.
For this guide, we'll choose "Remix App Server" as the deploy target and "JavaScript" – but neither choice affects this guide.
You should now have a folder structure like this:
my-project/ ├─ studio/ └─ web/
In separate terminal windows you can now run:
sanity start
from the studio
folder, and view the studio at http://localhost:3333
npm run dev
from the web
folder, and view the Remix project at http://localhost:3000
In the web
folder, install picosanity
npm install picosanity
Picosanity is a "tiny Sanity client alternative". Since we're only querying data, we can keep our web project smaller by using it.
We'll put all our Sanity utilities for the Remix site in a dedicated folder, create these two files:
// ./web/app/lib/sanity/config.js
export const config = {
apiVersion: "2021-03-25",
// Find these in your ./studio/sanity.json file
dataset: "production",
projectId: "YOUR_PROJECT_ID",
useCdn: false,
};
// ./web/app/lib/sanity/getClient.js
import PicoSanity from "picosanity";
import { config } from "./config";
// Standard client for fetching data
export const sanityClient = new PicoSanity(config);
// Authenticated client for fetching draft documents
export const previewClient = new PicoSanity({
...config,
useCdn: false,
token: process.env.SANITY_API_TOKEN ?? ``,
});
// Helper function to choose the correct client
export const getClient = (usePreview = false) =>
usePreview ? previewClient : sanityClient;
You can see we're laying the groundwork for previews. But first, we'll focus on just querying published, public data from our project and injecting it into the Remix site.
Let's display a list of all movies on the home page. Open ./web/app/routes/index.jsx
.
Data fetching in Remix is done inside a route's loader()
function. It's here we'll import our getClient()
and run a GROQ query to list all our movies.
// ./web/app/routes/index.jsx
// ...other imports, meta() and links() functions
import { getClient } from "~/lib/sanity/getClient";
// loader() must be async!
export async function loader() {
const movies = await getClient().fetch(
`*[_type == "movie"]{ _id, title, slug }`
);
return { movies };
}
export default function Index() {
let { movies } = useLoaderData();
return (
<div style={{ textAlign: "center", padding: 20 }}>
{movies?.length > 1
? movies.map((movie) => (
<div style={{ padding: 10 }} key={movie._id}>
<Link to={movie.slug.current}>{movie.title}</Link>
</div>
))
: null}
</div>
);
}
If querying successfully, you should now have a list of movies
, with links to each individual page. But those links will 404, we need to make a dynamic route to load those!
Create a new route file using the code below, beginning with $
to indicate a parameter. We'll use that parameter in our query.
// ./web/app/routes/$slug.jsx
import { useLoaderData } from "remix";
import { getClient } from "~/lib/sanity/getClient";
export async function loader({ params }) {
// Query for _all_ documents with this slug
// There could be two: Draft and Published!
const initialData = await getClient().fetch(
`*[_type == "movie" && slug.current == $slug]`,
{ slug: params.slug }
);
return { initialData };
}
export default function Movie() {
let { initialData } = useLoaderData();
return (
<div style={{ textAlign: "center", padding: 20 }}>
<h1>{initialData[0].title}</h1>
</div>
);
}
Phew! You should now be able to load this page or any other movie from the home page.
Now let's add a preview mode and watch changes as they happen!
So that our studio and website can talk to each other, we'll need to adjust some CORS settings.
In sanity.io/manage go to the API tab and add a new CORS origin for http://localhost:3000
with credentials allowed.
Note: You'll need to repeat this step once your website is deployed to add any deployed URLs as well.
In the web
folder, create a .env
file for secrets. These can be read server-side by Remix. This file should not be checked into git.
Back in sanity.io/manage, create a "viewer" token for your project and paste it into your .env
file for the SANITY_API_TOKEN
key. The other SANITY_PREVIEW_SECRET
can be any random string.
# ./web/.env # Create a "Viewer" Token in sanity.io/manage SANITY_API_TOKEN='...' # Any random string SANITY_PREVIEW_SECRET='asdf-1234'
Remix doesn't load .env
files by default, we'll need to enqueue them. In the web
folder install the dotenv
package.
npm install dotenv
Then import and initialize the package in your site's entry.server.jsx
file
import dotenv from "dotenv";
// ...leave the parameters in this function as-is
export default function handleRequest() {
dotenv.config();
// ...and the rest
}
For any request to any page, we'll check the URL for a ?preview=
search parameter that may contain a secret string. If it exists and is correct, the preview client will load with an authentication token server-side to read draft documents.
Back in $slug.jsx
we can update our loader()
function and the default export to:
?preview=asdf-1234
search parameterexport async function loader({ request, params }) {
const requestUrl = new URL(request?.url);
const preview =
requestUrl?.searchParams?.get("preview") ===
process.env.SANITY_PREVIEW_SECRET;
// Query for _all_ documents with this slug
// There could be two: Draft and Published!
const initialData = await getClient(preview).fetch(
`*[_type == "movie" && slug.current == $slug]`,
{ slug: params.slug }
);
return { initialData, preview };
}
export default function Movie() {
let { initialData, preview } = useLoaderData();
// Bonus! A helper function checks the returned documents
// To show Draft if in preview mode, otherwise Published
const movie = filterDataToSingleItem(initialData, preview);
return (
<div style={{ textAlign: "center", padding: 20 }}>
{preview ? <div>Preview Mode Enabled</div> : null}
<h1>{movie.title}</h1>
</div>
);
}
The function filterDataToSingleItem()
can be seen in the example repo here. It ensures we're loading the correct document – either draft or published – based on whether the preview is enabled or not.
That function can be copied from ./web/app/lib/sanity
in the demo repository.
Now if you visit a page with the correct URL, you should see "Preview Mode Enabled" across the top. You should also see unsaved changes from the current draft document when the page loads, and after a page reload.
http://localhost:3000/walle?preview=asdf-1234
However, the page doesn't update as changes are made. Let's make that happen!
It's time to pull in groq-store
, a package that will stream the dataset into the browser and update as changes are made. Install in the web
folder:
npm install @sanity/groq-store
You'll also need to grab two more files from the example repository.
./web/app/lib/sanity/usePreviewSubscription.js
a React hook for taking in a query and importing groq-store
./web/app/components/Preview.jsx
a component that calls the hook, designed only to be mounted if preview mode is enabled.Once you have these two documents, we need to update our $slug.jsx
one more time. The major differences are passing the query and query parameters down into the document – as well as importing the <Preview />
component which will update its state as it receives changes.
import { useState } from "react";
import { useLoaderData } from "remix";
import { getClient } from "~/lib/sanity/getClient";
import { filterDataToSingleItem } from "~/lib/sanity/filterDataToSingleItem";
import Preview from "~/components/Preview";
export async function loader({ request, params }) {
const requestUrl = new URL(request?.url);
const preview =
requestUrl?.searchParams?.get("preview") ===
process.env.SANITY_PREVIEW_SECRET;
// Query for _all_ documents with this slug
// There could be two: Draft and Published!
const query = `*[_type == "movie" && slug.current == $slug]`;
const queryParams = { slug: params.slug };
const initialData = await getClient(preview).fetch(query, queryParams);
return {
initialData,
preview,
// If `preview` mode is active, we'll need these for live updates
query: preview ? query : null,
queryParams: preview ? queryParams : null,
};
}
export default function Movie() {
let { initialData, preview, query, queryParams } = useLoaderData();
// If `preview` mode is active, its component update this state for us
const [data, setData] = useState(initialData);
// Bonus, a helper function checks the returned documents
// To show Draft if in preview mode, otherwise Published
const movie = filterDataToSingleItem(data, preview);
return (
<div style={{ textAlign: "center", padding: 20 }}>
{preview ? (
<Preview
data={data}
setData={setData}
query={query}
queryParams={queryParams}
/>
) : null}
{/* When working with draft content, optional chain _everything_ */}
{movie?.title ? <h1>{movie.title}</h1> : null}
</div>
);
}
Now if you have your studio and Remix site side-by-side, you should see changes as you type!
This works by using a token-authenticated request to make the initial request on the server. Then by using your own logged-in credentials in the browser to keep streaming updates.
So anyone with the link can view the initial draft version of the document. But only users with access to your Sanity project – logged in on the same browser – will see live updates.
Instead of opening two browser windows, you can display your website directly in the studio by leveraging Structure Builder to export your own default document views.
First, install the iframe-pane
plugin for displaying your Remix website. From the studio
folder, run
sanity install iframe-pane
Next, setup Structure Builder and configure your desk structure file with the following config:
import S from "@sanity/desk-tool/structure-builder";
import Iframe from "sanity-plugin-iframe-pane";
import { resolveProductionUrl } from "./resolveProductionUrl";
// Here we declare which view panes show up for which schema types
export const getDefaultDocumentNode = ({ schemaType }) => {
if (schemaType === `movie`) {
return S.document().views([
S.view.form(),
// Including the iframe pane, with a function to create the url
S.view
.component(Iframe)
.options({ url: (doc) => resolveProductionUrl(doc) })
.title("Preview"),
]);
}
return S.document();
};
// Then we export the default list of menu items
export default () =>
S.list()
.title("Content")
.items([
S.documentTypeListItem("movie").title("Movies"),
S.documentTypeListItem("person").title("People"),
S.documentTypeListItem("screening").title("Screenings"),
]);
The resolveProductionUrl()
function takes in the document and outputs a URL which is inserted into the iframe
.
It also appends the preview secret. This needs to be the same random string that the Remix site is aware of.
All going well, you've now got Remix right alongside the writing surface.
You'll notice some additional code in the final version. Including helpers to display images and render Portable Text fields.
Also worth noting, the "preview secret" is not exactly a strict form of security. While it does not leak any credentials or tokens, you may wish to harden this to prevent unwanted access.
Lastly, there's the step of deploying your site live (Remix gives you many options) and ensuring your pages are cached for super-fast loading, and less stress on your API quota.
Sanity.io is a platform to build websites and applications. It comes with great APIs that let you treat content like data. Give your team exactly what they need to edit and publish their content with the customizable Sanity Studio. Get real-time collaboration out of the box. Sanity.io comes with a hosted datastore for JSON documents, query languages like GROQ and GraphQL, CDNs, on-demand asset transformations, presentation agnostic rich text, plugins, and much more.
Don't compromise on developer experience. Join thousands of developers and trusted companies and power your content with Sanity.io. Free to get started, pay-as-you-go on all plans.
Create common taxonomy schemas like Categories and Tags with parent/child relationships
Go to Creating a Parent/Child TaxonomySometimes the content you need to reference lives outside of Sanity
Go to Creating a custom input to display and save third party dataA step-by-step guide to setup Next.js and Sanity Studio with Live Preview
Go to Live Preview with Next.js and Sanity.io: A Complete GuideGet the best of both worlds. Tailwind-styled typography and Portable Text's markup-and-components structure.
Go to ProseableText: Combine Tailwind CSS Typography with Portable Text