HUGO
News Docs Themes Community GitHub

transform.PortableText

Converts Portable Text to Markdown.

Syntax

transform.PortableText MAP

Returns

string
New in v0.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 filename provided passed on as a markdown attribute.

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.

Example

Content Adapter

content/_content.gotmpl
{{ $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.

sanity.config.ts
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:

schemaTypes/postType.ts
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
schemaTypes/index.ts
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:

HTTPCache:
  polls:
  - disable: false
    for:
      includes:
      - https://*.*.sanity.io/**
    high: 3m
    low: 30s
caches:
  getresource:
    dir: :cacheDir/:project
    maxAge: 5m
[HTTPCache]
  [[HTTPCache.polls]]
    disable = false
    high = '3m'
    low = '30s'
    [HTTPCache.polls.for]
      includes = ['https://*.*.sanity.io/**']
[caches]
  [caches.getresource]
    dir = ':cacheDir/:project'
    maxAge = '5m'
{
   "HTTPCache": {
      "polls": [
         {
            "disable": false,
            "for": {
               "includes": [
                  "https://*.*.sanity.io/**"
               ]
            },
            "high": "3m",
            "low": "30s"
         }
      ]
   },
   "caches": {
      "getresource": {
         "dir": ":cacheDir/:project",
         "maxAge": "5m"
      }
   }
}

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.


Last updated: February 26, 2025 : One more copy button (3c555d5f8)
Improve this page