Custom Types

prismic.io natively supports Custom types. Custom types allow you to define and configure fields for your content. Define custom types for pages, posts, events, authors, products, places, …

A productive way to build pages with prismic.io, is to follow an iterative cycle of defining fields for your custom type, querying its content, and then integrating it into templates.

There are two categories of custom types: Repeatable Types and Single Types. Repeatable types are for content that you will have more than one of, such as articles, products, places, authors, etc. Single Types are for content that you only need one instance of, such as a homepage or a privacy policy page.

Custom types are created by selecting the category as either repeatable or single, then using a JSON syntax to define the content. Each custom type will contains a list of the content fields and their configuration. A variety of fields are available including those for rich texts, images, dates, etc.

Fields can be grouped under tabs for better content organization.

Examples

A basic page with a single "Main" tab, containing UID (friendly unique identifier for URLs), image, title and description fields. Enter something like “Basic Page” for the Custom Type name. prismic.io will automatically assign this type the API ID of "basic-page". The API ID will be the document type used to query these documents:

Copy
      {
  "Main" : {
    "uid" : {
      "type" : "UID",
      "config" : {
        "placeholder" : "UID"
      }
    },
    "image" : {
      "type" : "Image"
    },
    "title" : {
      "type" : "StructuredText",
      "config" : {
        "single" : "heading1",
        "placeholder" : "Title..."
      }
    },
    "description" : {
      "type" : "StructuredText",
      "config" : {
        "multi" : "paragraph,em,strong,hyperlink",
        "placeholder" : "Description..."
      }
    }
  }
}
    

A more advanced custom type for a blog post using Slices:

What are Slices?

Slices are used in custom types to define a dynamic zone for richer page layouts. For example in a blog post, defining a Slices zone allows the writer to choose between a text, an image or a quote. This gives freedom to compose the blog post by alternating and ordering these choices.

Copy
      {
  "Blog Post" : {
    "uid" : {
      "type" : "UID",
      "config" : {
        "placeholder" : "unique-identifier-for-blog-post-url"
      }
    },
    "body" : {
      "fieldset" : "Post content",
      "type" : "Slices",
      "config" : {
        "choices" : {
          "text" : {
            "type" : "StructuredText",
            "fieldset" : "Text",
            "config" : {
              "multi" : "paragraph, heading1, heading2, heading3, strong, em, hyperlink",
              "placeholder" : "Post text..."
            }
          },
          "quote" : {
            "type" : "StructuredText",
            "fieldset" : "Quote",
            "config" : {
              "placeholder" : "Post quote...",
              "single" : "paragraph"
            }
          },
          "image-with-caption" : {
            "type" : "Group",
            "fieldset" : "Post image and caption",
            "config" : {
              "fields" : {
                "illustration" : {
                  "type" : "Image"
                },
                "caption" : {
                  "type" : "StructuredText",
                  "config" : {
                    "single" : "heading3",
                    "placeholder" : "Image caption..."
                  }
                }
              }
            }
          }
        }
      }
    }
  },
  "Metadata" : {
    "author" : {
      "fieldset" : "Author",
      "type" : "Text"
    },
    "date" : {
      "fieldset" : "Post properties",
      "type" : "Date",
      "config" : {
        "label" : "Post date"
      }
    }
  }
}
    

You're now ready to compose your own custom types by referring to the docs where you'll find out examples and full configuration reference for all the fields (StructuredText, Image, Slices, etc.).

Queries allow you to retrieve content to be displayed in your pages.

Queries in prismic.io are based on predicates. Some predicates apply to any custom type, and some are field-specific. You can also combine multiple predicates to express your query.

Here is an example query for retrieving a basic page through its UID:

Copy
      // Using NodeJS
app.route('/:uid').get(function(req, res) {
  var uid = req.params.uid;
  api(req, res).then(function(api) {
    return api.query(prismic.Predicates.at('my.basic-page.uid', uid));
  }).then(function(pageContent) {
    res.render('index', {
      pageContent: pageContent.results[0]
    });
  });
});
    
Copy
      $uid = $_GET('uid');
$doc = $api->getByUID("basic-page", $uid);
    
Copy
      // String uid comes from the querystring
Document doc = api.getByUID("basic-page", uid);
    
Copy
      // String uid comes from the querystring
Document doc = api.GetByUID("basic-page", uid);
    
Copy
      // String uid comes from the querystring
doc = api.getByUID("basic-page", uid);
    

The following example retrieves all blog posts:

Copy
      // Using NodeJS
app.route('/blog').get(function(req, res) {
  api(req, res).then(function(api) {
    return api.query(prismic.Predicates.at('document.type', 'blog-post'), 
      { pageSize : 10,
        orderings :'[my.blog-post.date desc]' });
  }).then(function(postsContent) {
    res.render('index', {
      postsContent: postsContent.results
    });
  });
});
    
Copy
      $response = $api->query(
    array(Predicates::at("document.type", "blog-post")),
    array("pageSize" => 10, "orderings" => "[my.blog-post.date desc]")
);
$posts = $response->getResults(); // Array
    
Copy
      Response response = api.query(Predicates.at("document.type", "blog-post"))
   .pageSize(10)
   .orderings("my.blog-post.date desc")
   .submit();
List<Document> results = response.getResults();
    
Copy
      Response response = api.query(Predicates.at("document.type", "blog-post"))
   .PageSize(10)
   .Orderings("my.blog-post.date desc")
   .Submit();
    
Copy
      response = api.query([Predicates.at("document.type", "blog-post")], {
  "page_size" => 10,
  "orderings" => '["my.blog-post.date desc"]'
})
    

A full-text search on all blog posts is done combining two predicates:

Copy
      // Using NodeJS
// search blog posts /blog/search?terms=term1 term2
app.route('/blog/search').get(function(req, res) {
  var terms = req.query.terms;
  api(req, res).then(function(api) {
    return api.query([
      prismic.Predicates.at("document.type", "blog-post"),
      prismic.Predicates.fulltext("document", terms)
    ],
    { pageSize : 10 });
  }).then(function(postsContent) {
    res.render('index', {
      postsContent: postsContent.results
    });
  });
});
    
Copy
      $terms = $_GET("terms");
$response = $api
    ->query(array(
        Predicates::at("document.type", "blog-post"),
        Predicates::fulltext("document", terms)
    ));
$posts = $response->getResults(); // Array
    
Copy
      Response response = api.query(
    Predicates.at("document.type", "blog-post"),
    Predicates.fulltext("document", terms) // terms is a String
).submit();
List<Document> documents = response.getResults();
    
Copy
      Response response = api.Query(
    Predicates.at("document.type", "blog-post"),
    Predicates.fulltext("document", terms) // terms is a String
).Submit();
IList<Document> documents = response.Results;
    
Copy
      response = api.query([
    Predicates.at("document.type", "blog-post"),
    Predicates.fulltext("document", terms) // terms is a string
])
documents = response.results
    

Predicates Reference

at(path, value)

The at operator is the equality operator, checking that the fragment matches the described value exactly.
It takes a value for a field or an array (only for tags)

path

document.type, document.tags or my.{type}.{field}

value

single value

example

at("document.type", "product")
at("document.tags", ["Macaron", "Cupcake"])
at("my.articles.gender", "male")

not(path, value)

The not operator is the different operator, checking that the fragment doesn't match the described value exactly.
It takes a value for a field.

path

document.type or my.{type}.{field}

value

single value

example

not("document.type", "product")

any(path, values)

The any operator takes an array of strings as a value. It works exactly the same way as the at operator, but checks whether the fragment matches either one of the values in the array. You can use it with all fragment types.

path

document.type or my.{type}.{field}

values

array of values

example

any("document.type", ["product", "blog-post"])

in(path, values)

The in operator is used to retrieve documents using an Array of IDs or UIDs. This operator is much more performant than the any operator. This operator is more performant than any to query document by UID or ID. This operator return the documents in the same order as the passed Array.

path

document.type or my.{type}.{field}

values

array of values

example

in("document.uid, ["myuid1", "myuid2"])

fulltext(path, value)

The fulltext operator provides two capabilities: either you want to check if a certain string is anywhere inside a document (this is what you should use to make your project's search engine feature), or if the string is contained inside a specific document's structured text fragment. In the example you want to search the term banana in the document

If you want to check in a given structured text fragment you will use something like the second example.

path

global namespace 'document' or your custom types 'my.type.field'

value

search terms

example

fulltext("document", "banana")
fulltext("my.product.title", "banana")

has(path)

The has operator check whether a fragment has a value.

path

your custom types 'my.type.field'

example

has('my.product.price')

missing(path)

The missing operator check if a fragment doesn't have a value. Note that the missing operator will restrict the results to the type implied in the fragment path.

path

your custom types 'my.type.field'

example

missing('my.product.price')

similar(id,value)

The similar operator is especially smart, since it takes an ID of a document, and returns a list of documents whose contents are similar. This allows to build an automated content discovery feature (for instance, a "Related posts" block) at almost no cost.

Also, remember that you can combine it with other predicates, and search for the "similar blog posts" for instance, of even the "similar blog posts that mention chocolate".

id

document ID

value

number

the maximum count of documents that a term may appear in to be still considered relevant

example

similar("VkRmhykAAFA6PoBj", 10)


Predicate options

pageSize(value)

description

The pageSize options define the maximum of document that the API will return for your query. Default is 20, max is 100.

value

page size

page(value)

description

The page options define the pagination for the result of your query. Defaults to "1", corresponding to the first page.

value

page index

example

page(2)

orderings(values)

description

order result by fields. You can specify as many fields as you want, in order to address all of the documents you are querying or if you have the same value on a given field. Use "desc" next to the field name, to order it from greatest to lowest.

values

fields

example

orderings("[my.product.price desc, my.product.price.title]")

after(value)

description

query documents after a specific document. By reversing the ordering, you can query for previous documents. Useful when creating navigation for a blog.

value

document id

example

after("VkRmhykAAFA6PoBj")

fetchLinks(values)

description

additional fields to retrieve in LinkDocument fragments. The LinkDocument can then be queried like a Document. Note that this only works with basic fields such as Text, Number or Date. It is not possible to retrieve StructuredText fragments from linked document using this field.

value

fields

example

fetchLinks([author.full_name, author.first_name])

fetch(values)

description

fetch only specific fields.

value

fields to retrieve

example

fetch(["author.full_name", "author.first_name"])

After defining custom types, querying your content and passing the results to the template, you need to integrate the content of the defined fields into the template's markup. This is done with simple calls to the appropriate methods and field key parameters.

For example, here is how you integrate the basic page:

Copy
      // using jade templates

block body
    div.welcome
        img(src=pageContent.getImage('basic-page.image').url, class='star')
        != pageContent.getStructuredText('basic-page.title').asHtml(ctx.linkResolver)
        != pageContent.getStructuredText('basic-page.description').asHtml(ctx.linkResolver)
    
Copy
      <body>
    <div class="welcome">
        <img src="<?= $pageContent->getImage('basic-page.image')->getUrl() ?>">
        <?= $pageContent->getStructuredText('basic-page.title')->asHtml($linkResolver) ?>
        <?= $pageContent->getStructuredText('basic-page.description')->asHtml($linkResolver) ?>
    </div>
</body>
    
Copy
      // Using JSP

<body>
    <div class="welcome">
        <img src="${pageContent.getImage("basic-page.image").getUrl()}">
        ${pageContent.getStructuredText("basic-page.title").asHtml(linkResolver)}
        ${pageContent.getStructuredText("basic-page.description").asHtml(linkResolver)}
    </div>
</body>
    
Copy
      <body>
    <div class="welcome">
        <img src="@Model.PageContent.GetImage("basic-page.image").Url">
        @Model.PageContent.GetStructuredText("basic-page.title").AsHtml(Model.Context.resolver)
        @Model.PageContent.getStructuredText("basic-page.description").AsHtml(Model.Context.resolver)
    </div>
</body>
    
Copy
      <body>
    <div class="welcome">
        <img src="<%= @doc["basic-page.image"].url %>">
        <%= @doc["basic-page.title"].as_html(linkResolver) %>
        <%= @doc["basic-page.description"].as_html(linkResolver) %>
    </div>
</body>
    

Here is an advanced integration example, integrating slices into a rich blog post:

Copy
      // using jade templates

div.blog-main.single.container
    for slice in postContent.getSliceZone('blog-post.body').slices
        //- Render the right markup for a given slice type.
        case slice.sliceType
            when 'text'
                div !{slice.value.asHtml()}
            when 'quote'
                span.block-quotation !{slice.value.asText()}
            when 'image-with-caption'
                 - var imageWithCaption = slice.value.toArray()[0]
                    p.block-img
                        img(src=imageWithCaption.getImage('illustration').url)
                    p
                        span.image-label !{imageWithCaption.get('caption').asText()}

    
Copy
      <div class="blog-main single container">
<? foreach($postContent->getSliceZone('blog-post.body')->getSlices() as $slice) { ?>
    <? if ($slice->getSliceType() == "text") { ?>
        <div><?= $slice->getValue()->asHtml() ?></div>
    <? } else if ($slice->getSliceType() == "quote") { ?>
        <span class="block-quotation"><?= $slice->getValue()->asText() ?></div>
    <? } else if ($slice->getSliceType() == "image-with-caption") { ?>
        <? $imageWithCaption = $slice->getValue()->getArray()[0] ?>
        <p class="block=img"><img src="<?= $imageWithCaption->getImage('illustration')->getUrl() ?>"></p>
        <span class="block-quotation"><?= $imageWithCaption->get('caption')->asText() ?></div>
    <? } ?>
<? } ?>
</div>
    
Copy
      // Using JSP

<div class="blog-main single container">
<c:forEach items="${postContent.getSliceZone("blog-post.body").getSlices()}" var="slice">
    <% if (slice.getSliceType() == "text") { %>
        <div>${slice.getValue().asHtml()}</div>
    <% } else if (slice.getSliceType() == "quote") { %>
        <span class="block-quotation">${slice.getValue().asText()}</div>
    <% } else if (slice.getSliceType() == "image-with-caption") { %>
        <c:set var="imageWithCaption" value="slice.getValue()" />
        <p class="block=img"><img src="${imageWithCaption.getImage("illustration").getUrl()}"></p>
        <span class="block-quotation">${imageWithCaption.get("caption").asText()}</div>
    <% } %>
</c:forEach>
</div>
    
Copy
      <div class="blog-main single container">
@foreach (var slice in Model.PostContent.GetSliceZone("blog-post.body").Slices) {
    @switch (slice.SliceType)
    {
    case "text": @: <div>@slice.Value.AsHtml()</div>
    break;
    case "quote": @: <span class="block-quotation">@slice.Value.AsText()</div>
    break;
    case "image-with-caption": @:
        <p class="block=img"><img src="@slice.Value.getImage("illustration").Url"></p>
        <span class="block-quotation">@slice.Value.Get("caption").AsText()</div>
    break;
    }
}
</div>
    
Copy
      <div class="blog-main single container">
<% @postContent['blog-post.body'].slices.each do |slice| %>
  <% case slice.slice_type
     when "text" %><div><%= slice.value.as_html() %></div>
  <% when "quote" %><span class="block-quotation"><%= slice.value.as_text() %></div>
  <% when "image-with-caption" %>
      <%= imageWithCaption = slice.value[0] ?>
      <p class="block=img"><img src="<?= slice.value[0]['illustration'].url ?>"></p>
      <span class="block-quotation"><?= slice.value[0]['caption'].as_text() ?></div>
  <% end %>
<% end %>
</div>