Skip to main content

Wire template preview routes

Add the route that serves your templates, so each one renders at its Template URL for visitors and on the Studio canvas. You need it before any template can open in Studio or go live on your site.

The example on this page is the Blog post template, blog_post_template, at /blog/{{entry.slug}}, previewing AI 101 at /blog/ai-101.

Prerequisites​

What template routes are and what you get​

Studio stores your templates, but it doesn't serve pages. Your app needs a route at each template's URL that fetches the template and renders it.

When you open a template in Studio, the canvas loads the Base URL plus the template's URL for the preview entry. For the Blog post template previewing AI 101, that's http://localhost:3106/blog/ai-101.

The same route serves visitors and the Studio canvas. <StudioComponent /> switches to editing on its own when Studio opens the route, so your route code doesn't branch.

A visitor and the Studio canvas both request /blog/ai-101 from your app. One catch-all route, app/[[...slug]]/page.tsx, fetches the Blog post template, and StudioComponent renders it, as the page for the visitor and for editing on the Studio canvas.

Faster ways to do this​

  • Skill. Ask your agent to wire your template routes, and it uses setup-template-preview-routes. It asks whether you want one catch-all route or a route per template, then wires it, including your header, footer and 404 page. See What are skills?

Serve every template from one catch-all route​

A catch-all route passes the request path to Studio as url. Studio compares it with every template's URL and picks the most specific match. See Which template wins when several match.

With a catch-all, a new template works at its URL with no code change.

In the Next.js App Router, the example uses the catch-all segment app/[[...slug]]/page.tsx. For how catch-all segments match, and how they fit with an app/page.tsx you already have, see the Next.js docs on dynamic routes.

Put a prefix in front of entry.url​

When a content type's URL field has a URL pattern, Studio builds the Template URL from the field's Prefix and {{entry.url}}. Contentstack writes each entry's full address into the field, such as /blog/ai-101 for AI 101, and Studio compares that with the whole address. Set a prefix for the content type's part of your site, such as /blog/, so the Template URL is /blog/{{entry.url}}. Without a prefix, the Template URL is {{entry.url}}, which matches every address on your site. With a second template like it, the most recently updated one wins. With the prefix, /blog/ai-101 can only go to the Blog post template. You set the prefix in the URL field's URL Properties. See Always put a prefix in front of entry.url.

Keep your own routes ahead of the catch-all​

The catch-all gets the addresses no other route claims. Three kinds of path need a route of their own that your framework ranks ahead of it:

  • The Canvas URL. If the catch-all claims it, every section shows a page or nothing on the canvas. In the example, that's app/canvas/page.tsx.
  • Paths that must never reach Studio, such as /api.
  • Pages your code renders without Studio.

The catch-all also takes over your old 404 route, so the 404 has to come from the catch-all. See Send a 404 when no template matches.

A Next.js app folder. canvas/page.tsx is the section preview route, api/ never reaches Studio, and [[...slug]]/page.tsx receives every other address.

When to use a route per template instead​

You can give each URL pattern its own route instead, such as app/blog/[slug]/page.tsx for Blog post. A dedicated route can pass compositionUid, which is faster than matching by URL.

It costs you two things:

  • A template at a URL with no route can't open in Studio. The canvas shows "No Studio Code on This Route".
  • A route that always fetches one template UID can't show any other template at that URL. Studio shows "URL Pinned to Another Template".

Render StudioComponent in the route​

Import StudioComponent from @contentstack/studio-react, and pass it what the fetch returns:

import { StudioComponent } from "@contentstack/studio-react";

<StudioComponent specOptions={specOptions} />

<StudioComponent /> takes two props:

PropRequiredWhat it takes
specOptionsYesWhat useCompositionData returns as specOptions, or what fetchCompositionData resolves to.
dataNoValues that aren't in Contentstack. See Bring your own data.
  • Pass specOptions through as it is. Don't read or change its fetchOptions.
  • Render it only once you have specOptions. It reads specOptions as soon as it renders.
  • Don't use <StudioCanvas /> here. It renders nothing for visitors.

Fetch the template on the client or the server​

The catch-all passes the request path as the query:

{ url: "/blog/ai-101" }

Where you fetch depends on how the route renders:

Both take the same query: a url, a compositionUid, a templateContentTypeUid, or a pair of them.

Fetch by url on a catch-all route. It's the recommended way, because the route doesn't need to know which template an address uses, and a new template works with no code change. Some routes do know, and then a UID fits better:

Fetch by template and entry instead of by URL​

When your routing already knows the template and the entry, you can skip URL matching and ask for them directly.

You need this when your addresses don't hold a value stored in an entry. The common case is a Custom Preview URL with transformation rules. Say the Blog post content type's Custom Preview URL is /blog/{{entry.title}}. Its rules lowercase the title and replace spaces with -, so AI 101 is served at /blog/ai-101. Studio doesn't apply those rules, so a fetch by url looks for a post whose title is ai-101 and finds none. Your route built that address, so it already knows the entry. See Transformation rules aren't applied.

Pass the entry as templateEntryUid in the second argument, the options, not in the query:

const { specOptions, isLoading, error } = useCompositionData(
{ compositionUid: "blog_post_template" },
{ templateEntryUid: "blt5c1d9e0a7f3b2e41" },
);

This renders the Blog post template with AI 101. sdk.fetchCompositionData takes the same two arguments.

You passWhat Studio does
{ compositionUid: "blog_post_template" }Fetches the Blog post template.
{ templateContentTypeUid: "blog_post" }Fetches the template connected to Blog post.
{ url: "/blog/ai-101", templateContentTypeUid: "blog_post" }The content type picks the template. The URL only picks the entry.

Without templateEntryUid, Studio uses the entry for the url if you passed one. Otherwise it uses the content type's first entry.

templateContentTypeUid returns one template, the first connected to that content type, and you don't pick which. The Blog post content type has two connected templates, Blog post and Featured post, so fetch either of them by compositionUid.

Inside Studio, the preview entry an author picks wins over your templateEntryUid.

The query field is compositionUid. Studio doesn't accept composableUid, and reports the missing-identifier error for it.

Send a 404 when no template matches​

When no template matches an address, the fetch fails. It doesn't give you an empty result:

  • fetchCompositionData rejects.
  • useCompositionData returns an error, with specOptions set to null.

The catch-all matched the address, so your framework won't send a 404 by itself. The route has to.

Pass the error to isCompositionNotFound, from @contentstack/studio-react. It reads the error's code, and it's true only when Studio finished looking and nothing matched. Send a 404 when it's true, and a 500 when it's false.

code, for exampleMeaningisCompositionNotFoundRespond with
NO_COMPOSITION_FOR_URLNo template matches the URL.trueA 404
CONNECTED_ENTRY_NOT_FOUNDThe template matched, but its content type has no entry for the URL.trueA 404
TRANSPORT_FAILEDYour site sent a request to Contentstack and nothing came back.falseA 500
STACK_NOT_FOUND_ON_HOSTThe API key doesn't lead to a stack.falseA 500

A false means Studio couldn't get an answer, so the page may still exist. A 500 keeps caches and search engines from dropping it. Two outcomes of the URL search are false too. SEARCH_INCOMPLETE means the search stopped at its limit before it could rule a match out. URL_OWNERSHIP_PROBE_FAILED means several templates match the URL and every check on which one owns it failed. For every code that counts as not found, see Nothing matches what your site asked for.

import { isCompositionNotFound } from "@contentstack/studio-react";

if (isCompositionNotFound(error)) {
// Send your 404.
} else {
// Send a 500.
}

On the client, render your 404 content when isCompositionNotFound(error) is true. On the server, answer with a 404 status. See Render on the server.

The error's message names its code, says what didn't match, and lists what to check, most likely first. For an address no template matches, it starts like this:

[Contentstack Studio] NO_COMPOSITION_FOR_URL: No composition matches "/nothing/here". Considered 1 compositions.

For /blog/not-a-post, the Blog post template's URL pattern matches, but no Blog post entry has that URL. The message starts like this:

[Contentstack Studio] CONNECTED_ENTRY_NOT_FOUND: No entry was found for the content type "blog_post" this composition is connected to.

Don't decide the 404 on hasSpec or hasTemplate. Both are deprecated, and hasTemplate is false for every freeform template, such as About. To tell a connected template from a freeform one, read connectedContentTypeUid on the result of fetchCompositionData. It's the connected content type's UID, such as blog_post, or null for a freeform template.

Check that a template opens at its URL​

  1. In Studio, open the Blog post template. The canvas loads http://localhost:3106/blog/ai-101 and shows AI 101.

    The Blog post template open on the canvas of the Blog project, with AI 101 as the preview entry and its title and summary on the canvas.

  2. Click Deploy, then open http://localhost:3106/blog/ai-101 in your browser. The route renders the Blog post template with AI 101. Until you deploy the template, the route can't find it, because outside Studio your site reads only deployed templates. See Save, preview, deploy.

  3. Open http://localhost:3106/blog/not-a-post. The route shows your 404 content.

Next, write the fetch for your render path: Render on the client or Render on the server.

If it doesn't work​

The first four headings below quote the card titles as Studio shows them on the canvas. The last two are errors in your console or build.

No Studio Code on This Route​

Nothing at the template's URL asked Studio for a template. Add the catch-all route, or a route for that URL that renders <StudioComponent />.

The &quot;No Studio Code on This Route&quot; card while the Blog post template is open, naming the template UID blog_post and the URL /blog/ai-101.

URL Pinned to Another Template​

The route at that URL always fetches a different template by its UID. Fetch by url on that route instead.

The &quot;URL Pinned to Another Template&quot; card while the Blog post template is open, saying the front-end code has About hard-coded on this URL.

Use Studio Component​

The template rendered through <StudioCanvas />. The card's badge says "Editing works — the live page won't", because <StudioCanvas /> renders nothing for visitors. Render templates with <StudioComponent />.

The &quot;Use Studio Component&quot; card on the Blog post template, with its &quot;Editing works — the live page won&#39;t&quot; badge.

Studio Can't Connect to Your Site​

The route's modules don't import the module that calls studioSdk.init. See Install the Studio SDK.

The &quot;Studio Can&#39;t Connect to Your Site&quot; card on the canvas while the Blog post template is open.

StudioComponent crashes with no specOptions​

<StudioComponent /> reads specOptions as soon as it renders, so it throws when specOptions is undefined or null. There are three causes:

  • It renders before the fetch finishes. Render it only once the fetch has returned specOptions.
  • The result is passed under another prop name, such as spec. The prop is specOptions.
  • The server code destructures the result. const { specOptions } = await sdk.fetchCompositionData(...) gives undefined, because fetchCompositionData returns the spec options object itself. Write const specOptions = await sdk.fetchCompositionData(...).

TypeScript reports a missing specOptions prop when you build.

The query has no composition identifier​

The query has no url, compositionUid or templateContentTypeUid, or the one it has is empty or undefined. The message depends on where you fetch.

On the server, fetchCompositionData throws this:

No valid composition identifier provided. Please provide compositionUid, url, or templateContentTypeUid

On the client, useCompositionData doesn't throw. It returns this in error:

Either compositionUid, url, or templateContentTypeUid is required

A common cause is a router value that isn't ready yet, so the query gets an empty url. Another is a misspelled field, such as composableUid for compositionUid.

Report a problem with this page