Zum Hauptinhalt wechseln
Blog / Veröffentlicht am

Next.js + Prismic CMS

Porträt von Roman
Autorin

Roman Ernst, Geschäftsführung, Softwareentwicklung

Wir haben kürzlich Prismic mit Next.js integriert. Und weil wir damit so gute Erfahrungen gemacht haben, möchten wir unseren Ansatz teilen. Tatsächlich nutzt dieser Blog selbst Prismic und Next.js für SSR.

Warum wir uns für Prismic entschieden haben

Es gibt viele Headless‑CMS‑Konkurrenten mit ganz unterschiedlichen Ansätzen. Bevor wir uns für Prismic entschieden haben, haben wir uns andere beliebte CMS‑as‑a‑Service Anbieter angesehen: Contentful, ButterCMS, Scrivito und Strapi, um nur einige zu nennen. Am Ende hat uns Prismic überzeugt wegen:

  • hervorragender Mehrsprachenunterstützung

  • einfacher Bedienung

  • guter Vorschau‑ und Veröffentlichungsfunktionen

  • attraktivem Preis für kleine Teams

Insbesondere die Mehrsprachenunterstützung ist bei vielen anderen Systemen oft unbefriedigend.

Next.js beherrscht Server‑Side Rendering für React‑Apps wie kaum ein anderes Framework. Da Prismic gute React‑Unterstützung bietet und universelle Anwendungen unterstützt, passen beide hervorragend zusammen.

Wo fangen wir an?

Ein typisches Beispiel für Prismic ist ein Blog, in dem Autoren dynamisch neue Inhalte hinzufügen können. Wir schauen uns an, wie man eine einfache Blog‑Engine implementiert. Wir starten damit, in Prismic einen neuen Inhaltstyp Blog mit zwei Feldern anzulegen: Title und Body.

Routing

// lib/prismic.js
export const linkResolver = (doc) => {
  if (doc.type === 'blog_article') {
    return `/article?uid=${doc.uid}`
  }
  return '/'
}

Die Grundlagen

Nun fügen wir unserer Next‑App zwei Seiten hinzu: blog und article. Sehen wir uns zuerst die Blog‑Indexseite an:

// pages/blog.js

import React from 'react'
import Link from 'next/link'
import { RichText } from 'prismic-reactjs'

import { fetchBlogPosts, linkResolver } from '../lib/prismic.js'
import withPrismic from '../lib/with-prismic.js'

class BlogIndex extends React.Component {
  static async getInitialProps (ctx) {
    const posts = await fetchBlogPosts(ctx.prismic)
    return { posts }
  }

  render () {
    return (
      <ul>
        {posts.map(post => (
          <li key={post.id}>
            <h2>{RichText.asText(post.data.title)}</h2>
            <Link href={linkResolver(post.uid)}>
              <a>View Article</a>
            </Link>
          </li>
        ))}
      </ul>
    )
  }
}

export default withPrismic(BlogIndex)

In Next’s statischer Methode getInitialProps fragen wir die Blog‑Posts über unseren noch zu implementierenden Prismic‑API‑Adapter ab. getInitialProps ist ein hervorragender Ort, um Inhalte abzurufen, da sie entweder auf dem Server (beim ersten Laden) oder im Client (bei weiteren Anfragen) ausgeführt wird. So sorgt Next für gute Erreichbarkeit und Indexierbarkeit durch Suchmaschinen.

In render durchlaufen wir einfach die Posts, zeigen den Titel im Klartext und nutzen die Link-Komponente, um auf eine echte Artikelseite zu verweisen, wobei wir die uid des Artikels übergeben.

Für den Moment ignorieren wir die Details von withPrismicHOC und nehmen einfach an, dass sie den Next‑Kontext in getInitialProps mit unserem Prismic API‑Client (in ctx.prismic) erweitert, den wir in unseren Prismic‑Adapter injizieren.

Schauen wir uns nun die Artikel‑Seite an, die ziemlich ähnlich aussieht:

// pages/article.js

import React from 'react'
import Error from 'next/error'
import { RichText } from 'prismic-reactjs'

import { fetchBlogPost, linkResolver } from '../lib/prismic.js'
import withPrismic from '../lib/with-prismic.js'

class BlogArticle extends React.Component {
  static async getInitialProps (ctx) {
    const { uid } = ctx.query
    const post = await fetchBlogPost(ctx.prismic, uid)
    if (!post && ctx.res) ctx.res.statusCode = 404 // Return 404 if on server no article was found
    return { post }
  }

  render () {
    const { post } = this.props
    if (!post) {
      return <Error statusCode={404} /> // Present adequate 404
    }

    return (
      <article>
        <h1>{RichText.render(post.data.title)}</h1>
        {RichText.render(post.data.body, linkResolver)}
      </article>
    )
  }
}

export default withPrismic(BlogArticle)

Das war einfach. Wir holen den Artikel anhand der uid über unseren noch zu implementierenden Prismic‑Adapter, und rendern ihn mit dem von Prismic bereitgestellten RichText-Renderer. Wieder injecten wir ctx.prismic von unserem withPrismicHOC in unseren Adapter.

Prismic adapter

// lib/prismic.js

import { Predicates } from 'prismic-javascript'

const fetchBlogPosts = async ({ api, ref }) => {
  const { results } = await api.query(
    Predicates.at('document.type', 'blog_article'), { ref }
  )
  return results
}

const fetchBlogPost = ({ api, ref }, uid) => {
  return api.getByUID('blog_article', uid, { ref })
}

export { fetchBlogPosts, fetchBlogPost }

Hier fassen wir lediglich die Logik zusammen, wie man Beiträge vom Prismic-Client abruft. Für weitere Möglichkeiten lohnt sich ein Blick in die Prismic-Dokumentation. Der Adapter erwartet dabei selbstverständlich den Prismic-Client als erstes Argument. Diesen erhalten wir über das HOC im getInitialProps-Kontext und reichen ihn einfach an den Adapter weiter.

Schauen wir uns nun an, wie dieses HOC eigentlich funktioniert.

withPrismic HOC

// lib/with-prismic.js

import React from 'react'
import Prismic from 'prismic-javascript'
import cookie from 'cookie'

// Universal cookie getter
function parseCookie (req) {
  return cookie.parse(
    req ? req.headers.cookie || '' : document.cookie
  )
}

const prismicApi = new Prismic.Api('YOUR_API_ENDPOINT')

export default Page => {
  return class withPrismic extends React.Component {
    static displayName = `WithPrismic(${Page.displayName})`

    static async getInitialProps (ctx) {
      const { req } = ctx
      const api = await prismicApi.get()
      const previewRef = parseCookies(req)[Prismic.previewCookie]
      const ref = previewRef || api.master()

      // Enhance Next context
      ctx.prismic = { api, ref }

      let pageProps = {}
      if (Page.getInitialProps) {
        pageProps = await Page.getInitialProps(ctx)
      }

      return { ...pageProps }
    }

    render () {
      return <Page {...this.props} />
    }
  }
}

Lass uns etwas tiefer einsteigen.
Hier überschreiben wir die getInitialProps-Methode der Seite, um den ursprünglichen Kontext mit unserem Prismic-Client zu erweitern. Diesen initialisieren wir nur einmal, um ihn zwischen verschiedenen Requests wiederverwenden zu können.

Vielleicht fragst du dich jetzt, was ref und api.master() eigentlich genau sind.
Ein ref-Token wird von Prismic verwendet, um den Benutzerzugriff zu bestimmen. Es kommt bei mehreren Funktionen zum Einsatz – darunter Caching, Vorschauen, In-Website-Bearbeitung und A/B-Tests.
In unserem Beispiel nutzen wir es ausschließlich für Vorschauen. In Kürze zeigen wir, wie man dieses Token in einem Cookie speichert.

Hier holen wir es einfach aus dem Cookie (identifiziert durch den Namen, den wir über Prismic.previewCookie erhalten) – oder wir verwenden api.master(), das wiederum den Standard-Ref darstellt.

Preview Cookie

Prismic unterstützt Vorschauen, indem es eine definierte URL aufruft, die einen Query-Parameter namens token enthält.
Prismic bietet dazu eine Methode namens previewSession, die aus diesem Token die URL des jeweiligen Artikels generiert.

Indem wir dieses Token in einem Cookie speichern, können wir die Vorschau-Sitzung für eine bestimmte Zeit aktiv halten.

// pages/preview.js

import React from 'react'
import Error from 'next/error'
import Router from 'next/router'
import Prismic from 'prismic-javascript'
import cookie from 'cookie'

import { fetchBlogPost, linkResolver } from '../lib/prismic.js'
import withPrismic from '../lib/withPrismic.js'

// Universal cookie setter
function setCookie (res, key, value) {
  const options = { maxAge: 60 * 300, path: '/', httpOnly: false }
  if (res) {
    res.cookie(key, value, options)
  } else {
    document.cookie = cookie.serialize(key, value, options)
  }
}

// Universal redirect
function redirect (ctx, target) {
  if (ctx.res) {
    // On server
    // 303: "See other"
    ctx.res.writeHead(303, { Location: target })
    ctx.res.end()
  } else {
    // In the browser, we just pretend this never even happened
    Router.replace(target)
  }
}

class Preview extends React.Component {
  static async getInitialProps (ctx) {
    const { token } = ctx.query
    const redirectUrl = await ctx.prismic.api.previewSession(token, linkResolver, '/')

    setCookie(ctx.res, Prismic.previewCookie, token)
    redirect(ctx, redirectUrl)
  }

  render () {
    // This component is supposed to redirect to an actual preview location
    // We don't want this to render anything
    return <Error statusCode={400} />
  }
}

export default withPrismic(Preview)

Wie du siehst, leitet diese Seite den Nutzer auf die eigentliche Vorschau-URL weiter und sorgt dafür, dass das Session-Token (das später als ref verwendet wird) in einem Cookie gespeichert wird.

Wenn dir dieser Artikel geholfen hat oder du andere Ansätze diskutieren möchtest, vernetze dich gerne mit uns auf GitHub!

farbenmeer Logo

Weiterführende Links

Umfrage

Welches Thema interessiert dich am meisten?