Files
hugo/content/en/functions/transform/PortableText.md
T

5.7 KiB

title, description, categories, keywords, params
title description categories keywords params
transform.PortableText Converts Portable Text to Markdown.
functions_and_methods
returnType signatures
string
transform.PortableText MAP

{{< new-in "0.145.0" />}}

Portable Text is a JSON structure that represent rich text content in the Sanity CMS. In Hugo, this function is typically used in a Content Adapter that creates pages from Sanity data.

Types supported

  • block and span
  • image. Note that the image handling is currently very simple; we link to the asset.url using asset.altText as the image alt text and asset.title as the title. For more fine grained control you may want to process the images in a image render hook.
  • code (see the code-input plugin). Code will be rendered as fenced code blocks with any file name provided passed on as a markdown attribute.

{{< note >}} Since the Portable Text gets converted to Markdown before it gets passed to Hugo, rendering of links, headings, images and code blocks can be controlled with Render Hooks. {{< /note >}}

Example

Content Adapter

{{ $projectID := "mysanityprojectid" }}
{{ $useCached := true }}
{{ $api := "api" }}
{{ if $useCached }}
  {{/* See https://www.sanity.io/docs/api-cdn */}}
  {{ $api = "apicdn" }}
{{ end }}
{{ $url := printf "https://%s.%s.sanity.io/v2021-06-07/data/query/production"  $projectID $api }}

{{/* prettier-ignore-start */ -}}
{{ $q :=  `*[_type == 'post']{
  title, publishedAt, summary, slug, body[]{
    ...,
    _type == "image" => {
      ...,
      asset->{
        _id,
        path,
        url,
        altText,
        title,
        description,
        metadata {
          dimensions {
            aspectRatio,
            width,
            height
          }
        }
      }
    }
  },
  }`
}}
{{/* prettier-ignore-end */ -}}
{{ $body := dict "query" $q | jsonify }}
{{ $opts := dict "method" "post" "body" $body }}
{{ $r := resources.GetRemote $url $opts }}
{{ $m := $r | transform.Unmarshal }}
{{ $result := $m.result }}
{{ range $result }}
  {{ if not .slug }}
    {{ continue }}
  {{ end }}
  {{ $markdown := transform.PortableText .body }}
  {{ $content := dict
    "mediaType" "text/markdown"
    "value" $markdown
  }}
  {{ $params := dict
    "portabletext" (.body | jsonify (dict "indent" " "))
  }}
  {{ $page := dict
    "content" $content
    "kind" "page"
    "path" .slug.current
    "title" .title
    "date" (.publishedAt | time )
    "summary" .summary
    "params" $params
  }}
  {{ $.AddPage $page }}
{{ end }}

Sanity setup

Below outlines a suitable Sanity studio setup for the above example.

import {defineConfig} from 'sanity'
import {structureTool} from 'sanity/structure'
import {visionTool} from '@sanity/vision'
import {schemaTypes} from './schemaTypes'
import {media} from 'sanity-plugin-media'
import {codeInput} from '@sanity/code-input'

export default defineConfig({
  name: 'default',
  title: 'my-sanity-project',

  projectId: 'mysanityprojectid',
  dataset: 'production',

  plugins: [structureTool(), visionTool(), media(),codeInput()],

  schema: {
    types: schemaTypes,
  },
})

Type/schema definition:

import {defineField, defineType} from 'sanity'

export const postType = defineType({
  name: 'post',
  title: 'Post',
  type: 'document',
  fields: [
    defineField({
      name: 'title',
      type: 'string',
      validation: (rule) => rule.required(),
    }),
    defineField({
      name: 'summary',
      type: 'string',
      validation: (rule) => rule.required(),
    }),
    defineField({
      name: 'slug',
      type: 'slug',
      options: {source: 'title'},
      validation: (rule) => rule.required(),
    }),
    defineField({
      name: 'publishedAt',
      type: 'datetime',
      initialValue: () => new Date().toISOString(),
      validation: (rule) => rule.required(),
    }),
    defineField({
      name: 'body',
      type: 'array',
      of: [
        {
          type: 'block',
        },
        {
          type: 'image'
        },
        {
          type: 'code',
          options: {
            language: 'css',
            languageAlternatives: [
              {title: 'HTML', value: 'html'},
              {title: 'CSS', value: 'css'},
            ],
            withFilename: true,
          },
        },
      ],
    }),
  ],
})

Note that the above requires some additional plugins to be installed:

npm i sanity-plugin-media @sanity/code-input
import {postType} from './postType'

export const schemaTypes = [postType]

Server setup

Unfortunately, Sanity's API does not support RFC 7234 and their output changes even if the data has not. A recommended setup is therefore to use their cached apicdn endpoint (see above) and then set up a reasonable polling and file cache strategy in your Hugo configuration, e.g:

{{< code-toggle file=hugo >}} [HTTPCache] HTTPCache.polls disable = false low = '30s' high = '3m' [HTTPCache.polls.for] includes = ['https://..sanity.io/**']

[caches.getresource] dir = ':cacheDir/:project' maxAge = "5m" {{< /code-toggle >}}

The polling above will be used when running the server/watch mode and rebuild when you push new content in Sanity.

See Caching in resources.GetRemote for more fine grained control.