139 lines
3.4 KiB
Markdown
139 lines
3.4 KiB
Markdown
---
|
|
title: Import HTML to Portable Text
|
|
description: Use @portabletext/block-tools with JSDOM to convert HTML content
|
|
---
|
|
|
|
## Import HTML to Portable Text
|
|
|
|
Use `@portabletext/block-tools` with `JSDOM` to convert HTML from legacy CMSs to Portable Text.
|
|
|
|
### Setup
|
|
|
|
```bash
|
|
npm install @portabletext/block-tools jsdom
|
|
```
|
|
|
|
### Basic Conversion
|
|
|
|
```typescript
|
|
import { htmlToBlocks } from '@portabletext/block-tools'
|
|
import { JSDOM } from 'jsdom'
|
|
|
|
// Get block content type from your schema
|
|
const blockContentType = schema.get('blockContent')
|
|
|
|
const blocks = htmlToBlocks(htmlString, blockContentType, {
|
|
parseHtml: html => new JSDOM(html).window.document,
|
|
})
|
|
```
|
|
|
|
### Custom Deserializers
|
|
|
|
Handle specific HTML patterns:
|
|
|
|
```javascript
|
|
const blocks = htmlToBlocks(htmlString, blockContentType, {
|
|
parseHtml: html => new JSDOM(html).window.document,
|
|
rules: [
|
|
{
|
|
deserialize(el, next, block) {
|
|
// Custom link handling
|
|
if (el.tagName.toLowerCase() === 'a') {
|
|
return {
|
|
_type: 'link',
|
|
href: el.getAttribute('href'),
|
|
blank: el.getAttribute('target') === '_blank'
|
|
}
|
|
}
|
|
// Custom image handling
|
|
if (el.tagName.toLowerCase() === 'img') {
|
|
return {
|
|
_type: 'image',
|
|
// Upload image separately, store reference
|
|
_sanityAsset: `image@${el.getAttribute('src')}`
|
|
}
|
|
}
|
|
return undefined // Fall through to default handling
|
|
}
|
|
}
|
|
]
|
|
})
|
|
```
|
|
|
|
### Pre-Processing HTML
|
|
|
|
Clean HTML before conversion:
|
|
|
|
```javascript
|
|
function cleanHtml(html) {
|
|
const dom = new JSDOM(html)
|
|
const doc = dom.window.document
|
|
|
|
// Remove layout elements
|
|
doc.querySelectorAll('header, footer, nav, .sidebar').forEach(el => el.remove())
|
|
|
|
// Extract metadata before processing body
|
|
const title = doc.querySelector('title')?.textContent
|
|
const description = doc.querySelector('meta[name="description"]')?.content
|
|
|
|
return {
|
|
body: doc.body.innerHTML,
|
|
metadata: { title, description }
|
|
}
|
|
}
|
|
```
|
|
|
|
### Image Upload
|
|
|
|
Don't just link external images—upload them:
|
|
|
|
```javascript
|
|
async function uploadImage(client, imageUrl) {
|
|
const response = await fetch(imageUrl)
|
|
const buffer = await response.arrayBuffer()
|
|
|
|
const asset = await client.assets.upload('image', Buffer.from(buffer), {
|
|
filename: imageUrl.split('/').pop()
|
|
})
|
|
|
|
return {
|
|
_type: 'image',
|
|
asset: { _type: 'reference', _ref: asset._id }
|
|
}
|
|
}
|
|
```
|
|
|
|
### Using in a Migration
|
|
|
|
Wrap this in `defineMigration` for reproducible imports:
|
|
|
|
```typescript
|
|
// migrations/import-wordpress-posts/index.ts
|
|
import {defineMigration, createOrReplace} from 'sanity/migrate'
|
|
import {htmlToBlocks} from '@portabletext/block-tools'
|
|
|
|
export default defineMigration({
|
|
title: 'Import WordPress posts',
|
|
async *migrate(documents, context) {
|
|
const posts = await fetchWordPressPosts() // Your import source
|
|
|
|
for (const post of posts) {
|
|
const blocks = htmlToBlocks(post.content, blockContentType, {
|
|
parseHtml: html => new JSDOM(html).window.document,
|
|
})
|
|
|
|
yield createOrReplace({
|
|
_id: `post-${post.slug}`,
|
|
_type: 'post',
|
|
title: post.title,
|
|
body: blocks,
|
|
})
|
|
}
|
|
}
|
|
})
|
|
```
|
|
|
|
Run with: `sanity migration run import-wordpress-posts --no-dry-run`
|
|
|
|
Reference: [Schema and Content Migrations](https://www.sanity.io/docs/content-lake/schema-and-content-migrations)
|