# Converting WordPress blocks to Portable Text https://www.sanity.io/learn/course/migrating-content-from-wordpress-to-sanity/converting-wordpress-blocks-to-portable-text.md Convert raw WordPress content into Portable Text, create custom schema types in Sanity Studio, and make authenticated requests to WordPress. There might be situations where you want to preserve some of the presentational data from WordPress in your content. Sometimes, for some types of content, typically marketing landing pages with unique content that doesn't need to be resued, it's more pragmatic to migrate one-to-one. The wrapper function from the [Converting HTML to Portable Text](https://www.sanity.io/learn/course/migrating-content-from-wordpress-to-sanity/converting-html-to-portable-text) lesson will not be wasted. What you need is the raw, _unprocessed_ HTML, which contains markup from the WordPress block editor to create objects in Portable Text. Consider a "columns" block, for example. This is a core block in WordPress. The previous lesson would have extracted text and images from its HTML and transformed them into block content without any column positioning detail (it's better to have this logic in your front end code). Trying to preserve that using class names alone from the post-processed HTML would be too difficult. ## Adding a custom column block type Our Portable Text configuration in the Sanity Studio has no native concept of columns. You'll need to fix this first. Register two new schema types to the Sanity Studio: 1. **Create** a schema type for an array of columns ```typescript:./schemaTypes/columnsType.ts import {defineField, defineType} from 'sanity' export const columnsType = defineType({ name: 'columns', type: 'object', fields: [ defineField({ name: 'columns', type: 'array', of: [{type: 'column'}], }), ], }) ``` 1. **Create** a schema type for an individual column: ```typescript:./schemaTypes/columnType.ts import {defineField, defineType} from 'sanity' export const columnType = defineType({ name: 'column', type: 'object', fields: [ defineField({ name: 'content', type: 'portableText', }), ], }) ``` 1. **Update** your Portable Text schema type to include columns: ```typescript import {defineField} from 'sanity' export const portableTextType = defineField({ name: 'portableText', type: 'array', of: [{type: 'block'}, {type: 'image'}, {type: 'externalImage'}, {type: 'columns'}], }) ``` 1. **Update** your workspace schema types to include columns: ```typescript:./schemaTypes/index.ts import {authorType} from './authorType' import {categoryType} from './categoryType' import {columnsType} from './columnsType' import {columnType} from './columnType' import {externalImageType} from './externalImageType' import {pageType} from './pageType' import {portableTextType} from './portableTextType' import {postType} from './postType' import {tagType} from './tagType' export const schemaTypes = [ authorType, categoryType, columnsType, columnType, externalImageType, pageType, portableTextType, postType, tagType, ] ``` Your posts and pages' Portable Text fields should now have the option to add "columns." ![Sanity Studio with Portable Text editor showing a "columns" option](https://cdn.sanity.io/images/3do82whm/next/3dd76d0cc4571660e095645f4701195a9bf5e712-2144x1388.png) ## Making authenticated requests to WordPress If you examine the output of your WordPress REST API you won't find a `content.raw` in the response. This is because it is only available when a request contains the "context" of `edit`. You can try adding this parameter to your request – by adding `?context=edit` to the URL, you'll receive a 401 in response as that context is not publicly available. ![WordPress REST API response showing a 401 error](https://cdn.sanity.io/images/3do82whm/next/6cd192c7250fc4fd343618b4a6a1aebe32fe0a0c-2144x1388.png) To resolve this, you'll need to add "basic authentication" to the request, which can be done with an "application password." 1. Learn more about [WordPress application passwords](https://make.wordpress.org/core/2020/11/05/application-passwords-integration-guide/) Login to your WordPress dashboard and go to wp-admin -> Users -> Edit User. Find your user account and scroll to the bottom of the page. ![WordPress dashboard showing application passwords](https://cdn.sanity.io/images/3do82whm/next/6903cdfe89fce9ff70554342d01667a36c9aa0df-2144x1388.png) 1. **Create** a new application password in WordPress with any name, but be sure to copy the password. ### Update your WordPress fetch function The `wpDataTypeFetch` function created in the [Find your WordPress API](https://www.sanity.io/learn/course/migrating-content-from-wordpress-to-sanity/first-steps) lesson can now be updated to make an authenticated request. 1. **Update** your WordPress fetch function to add authentication and a context search parameter – with your WordPress username and application password: ```typescript:./migrations/import-wp-lib/wpDataTypeFetch.ts import {BASE_URL, PER_PAGE} from '../constants' import type {WordPressDataType, WordPressDataTypeResponses} from '../types' // Basic auth setup in wp-admin -> Users -> Edit User // This is the WordPress USER name, not the password name const username = 'replace-with-your-username' const password = 'replace-with-your-password' export async function wpDataTypeFetch( type: T, page: number, edit: boolean = false, ): Promise { const wpApiUrl = new URL(`${BASE_URL}/${type}`) wpApiUrl.searchParams.set('page', page.toString()) wpApiUrl.searchParams.set('per_page', PER_PAGE.toString()) const headers = new Headers() if (edit) { // 'edit' context returns pre-processed content and other non-public fields wpApiUrl.searchParams.set('context', 'edit') headers.set( 'Authorization', 'Basic ' + Buffer.from(username + ':' + password).toString('base64'), ) } return fetch(wpApiUrl, {headers}).then((res) => (res.ok ? res.json() : null)) } ``` **Important**: the script above stores a password in plain text. If you plan to commit this script to version control – or host it somewhere – consider storing and retrieving it as environment variables from a `.env` file. ## Serializing raw content from WordPress Now, your migration script can retrieve raw content. It's time to see what that looks like. Earlier in this course, you installed [@wordpress/block-serialization-default-parser](https://www.npmjs.com/package/@wordpress/block-serialization-default-parser). A library to take raw content – with all the block editor's comments and unprocessed "shortcodes" – and convert it into an array of objects. This serialized data is much simpler to work with and convert into Portable Text. Now, you can target each individual block by its name and create whatever block content shape you like. Deep inside "inner blocks," the content is still stored as HTML, so you will still need to use the same `htmlToBlockContent` function from the last lesson to convert that HTML into block content – but now targeting and processing content layouts like columns is much simpler. 1. **Create** a helper function to process raw content from WordPress, converting paragraphs and columns into Portable Text. ```typescript:./migrations/import-wp/lib/serializedHtmlToBlockContent.ts import type {htmlToBlocks} from '@portabletext/block-tools' import {parse} from '@wordpress/block-serialization-default-parser' import type {SanityClient, TypedObject} from 'sanity' import {htmlToBlockContent} from './htmlToBlockContent' export async function serializedHtmlToBlockContent( html: string, client: SanityClient, imageCache: Record, ) { // Parse content.raw HTML into WordPress blocks const parsed = parse(html) let blocks: ReturnType = [] for (const wpBlock of parsed) { // Convert inner HTML to Portable Text blocks if (wpBlock.blockName === 'core/paragraph') { const block = await htmlToBlockContent(wpBlock.innerHTML, client, imageCache) blocks.push(...block) } else if (wpBlock.blockName === 'core/columns') { const columnBlock = {_type: 'columns', columns: [] as TypedObject[]} for (const column of wpBlock.innerBlocks) { const columnContent = [] for (const columnBlock of column.innerBlocks) { const content = await htmlToBlockContent(columnBlock.innerHTML, client, imageCache) columnContent.push(...content) } columnBlock.columns.push({ _type: 'column', content: columnContent, }) } blocks.push(columnBlock) } else if (!wpBlock.blockName) { // Do nothing } else { console.log(`Unhandled block type: ${wpBlock.blockName}`) } } return blocks } ``` ## Update the migration script 1. **Update** your request to WordPress to use authentication: ```typescript:./migrations/import-wp/index.ts let wpData = await wpDataTypeFetch(wpType, page, true) ``` 1. **Update** your `doc.content` field to use the serialized raw content for your documents: ```typescript:./migrations/import-wp/lib/transformPost.ts doc.content = wpDoc.content.raw ? await serializedHtmlToBlockContent(wpDoc.content.raw, client, existingImages) : undefined ``` Run your posts and pages migrations again. ```sh npx sanity@latest migration run import-wp --no-dry-run --type=posts ``` If your existing content used the core columns block like this: ![WordPress sample page showing a three column layout](https://cdn.sanity.io/images/3do82whm/next/eb4913c7f1963ff9930a95f0879d8685dc971ea4-2144x1388.png) You should see Sanity documents that use the newly created Portable Text columns object like this. ![Sanity Studio showing the Portable Text editor with a columns block](https://cdn.sanity.io/images/3do82whm/next/aa5c2481505c8b6e2614bcf59cdcde8c4ae7934a-2144x1388.png) ## Enhance the editorial experience As you notice, the block preview gives you the JSONesque data. This isn't super helpful for most content teams (unless they're into raw data). The last step is to update the block preview to show the columns a little bit nicer: ```typescript:schemaTypes/columnsType.ts import {defineField, defineType} from 'sanity' export const columnsType = defineType({ name: 'columns', type: 'object', fields: [ defineField({ name: 'columns', type: 'array', of: [{type: 'column'}], }), ], preview: { select: { columns: 'columns', }, prepare({columns}) { const columnsCount = columns.length return { title: `${columnsCount} column${columnsCount == 1 ? '' : 's'}`, } }, }, }) ``` 1. **Update** the columnsType field with the new preview configuration. Your studio should now have a preview like this: ![The Portable Text editor showing a preview for the column block type saying "2 columns"](https://cdn.sanity.io/images/3do82whm/next/f2b7174fa85eab4bc5ba6abd98ba6f789e3dd569-645x364.png) ```typescript:schemaTypes/columnType.ts import {defineField, defineType} from 'sanity' export const columnType = defineType({ name: 'column', type: 'object', fields: [ defineField({ name: 'content', type: 'portableText', }), ], preview: { select: { title: 'content', }, }, }) ``` 1. **Update** the `columnType` field with the new preview configuration. If you click into the columns block type, you should see the first bit of content in the individual columns: ![Missing alt text](https://cdn.sanity.io/images/3do82whm/next/823d3773841377257950b3b55a5d98c643d23630-673x325.png) You can further enhance this preview with custom preview components, using React components to show an even richer preview within the Portable Text editor. This lesson has only scratched the surface of converting raw WordPress content into Portable Text, but you now have a plan to convert all other WordPress blocks: 1. Create a custom schema type in Sanity Studio's Portable Text editor for each new block 2. Intercept that block during serialization and convert to Portable Text 3. Make sure that the block previews are helpful for your content team