Publication transformers¶
Hook into the publication pipeline to customize how websites are rendered and deployed.
Overview¶
Publication transformers are five hooks that run during the publication process:
- renderComponent — Override how GrapesJS renders components to HTML
- renderCssRule — Override how GrapesJS renders CSS rules
- transformFile — Modify files (HTML, CSS, assets) before publishing
- transformPermalink — Rewrite URLs in HTML and CSS
- transformPath — Change where files are published on the server
Transformers are chained: each one receives the output of the previous, allowing multiple transformers to compose.
Prerequisites¶
- Understanding of client config
- Familiarity with publication flow
- Basic JavaScript knowledge
PublicationTransformer interface¶
export interface PublicationTransformer {
renderComponent?(
component: Component,
toHtml: () => string
): string | undefined
renderCssRule?(
rule: CssRule,
initialRule: () => StyleProps
): StyleProps | undefined
transformFile?(file: ClientSideFile): ClientSideFile
transformPermalink?(
link: string,
type: ClientSideFileType,
initiator: Initiator
): string
transformPath?(
path: string,
type: ClientSideFileType
): string
}
Use cases¶
Minify HTML and CSS¶
const minifyHtml = require('html-minifier').minify
const postcss = require('postcss')
const cssnano = require('cssnano')
export default async function (config) {
config.addPublicationTransformers({
transformFile(file) {
// Minify HTML
if (file.type === 'html') {
file.content = minifyHtml(file.content, {
removeComments: true,
collapseWhitespace: true,
minifyCSS: true,
})
}
// Minify CSS
if (file.type === 'css') {
const result = postcss([cssnano()]).process(file.content)
file.content = result.css
}
return file
},
})
}
Add cache busting to assets¶
export default async function (config) {
const assetHashes = new Map()
config.addPublicationTransformers({
transformFile(file) {
// Generate hash for assets
if (file.type === 'asset') {
const crypto = require('crypto')
const hash = crypto
.createHash('md5')
.update(file.content)
.digest('hex')
.slice(0, 8)
const newName = file.name.replace(
/(\.[^.]+)$/,
`.${hash}$1`
)
assetHashes.set(file.name, newName)
file.name = newName
}
return file
},
transformPermalink(link, type, initiator) {
// Rewrite asset references in HTML/CSS
if (type === 'asset') {
const hashed = assetHashes.get(link)
return hashed ? hashed : link
}
return link
},
})
}
Rewrite URLs for a CDN¶
export default async function (config) {
config.addPublicationTransformers({
transformPermalink(link, type, initiator) {
// Skip external URLs
if (link.startsWith('http')) {
return link
}
// Point assets to CDN
if (type === 'asset') {
return `https://cdn.example.com${link}`
}
// Keep HTML links local
return link
},
})
}
Change file structure¶
export default async function (config) {
config.addPublicationTransformers({
transformPath(path, type) {
// Move CSS to a different directory
if (type === 'css') {
return path.replace(/^\/css/, '/styles')
}
// Flatten file structure
if (type === 'asset') {
return '/' + path.split('/').pop()
}
return path
},
transformPermalink(link, type, initiator) {
// Update references in HTML/CSS
if (type === 'css') {
link = link.replace(/^\/css/, '/styles')
}
if (type === 'asset' && initiator === 'css') {
// Assets referenced in CSS: adjust relative path
const newPath = '/' + link.split('/').pop()
// Go up one level from CSS directory
return '../' + newPath
}
return link
},
})
}
Strip HTML comments for production¶
export default async function (config) {
config.addPublicationTransformers({
transformFile(file) {
if (file.type === 'html' && process.env.NODE_ENV === 'production') {
// Remove HTML comments
file.content = file.content.replace(/<!--[\s\S]*?-->/g, '')
}
return file
},
})
}
Add analytics scripts¶
export default async function (config) {
config.addPublicationTransformers({
transformFile(file) {
if (file.type === 'html') {
const analyticsScript = `
<script async src="https://analytics.example.com/track.js"></script>
<script>
window.trackingId = '${process.env.TRACKING_ID}'
</script>
`
// Inject before closing </head>
file.content = file.content.replace(
/<\/head>/,
analyticsScript + '</head>'
)
}
return file
},
})
}
Transformer APIs¶
renderComponent¶
Intercept component rendering before GrapesJS converts to HTML:
export default async function (config) {
config.addPublicationTransformers({
renderComponent(component, toHtml) {
// Check component type
if (component.get('type') === 'image') {
// Custom logic for images
const src = component.get('attributes').src
console.log(`Rendering image: ${src}`)
}
// Call the default renderer
return toHtml()
},
})
}
Returning undefined uses the default renderer.
renderCssRule¶
Intercept CSS rule rendering:
export default async function (config) {
config.addPublicationTransformers({
renderCssRule(rule, initialRule) {
// Get the CSS rule
const selector = rule.getSelector()
const style = initialRule()
// Transform the style
if (selector.includes('button')) {
style['cursor'] = 'pointer'
}
return style
},
})
}
Returning undefined uses the default styles.
transformFile¶
Modify the file object (content, name, type, path):
export interface ClientSideFile {
name: string // Filename
type: ClientSideFileType // 'html', 'css', 'asset'
content: string | Buffer // File content
path?: string // File path on server
}
Example:
export default async function (config) {
config.addPublicationTransformers({
transformFile(file) {
// Rename file
if (file.type === 'css') {
file.name = file.name + '.min'
}
// Transform content
if (file.type === 'html') {
file.content = file.content.toUpperCase()
}
return file
},
})
}
transformPermalink¶
Rewrite URLs referenced in HTML and CSS:
export default async function (config) {
config.addPublicationTransformers({
transformPermalink(link, type, initiator) {
// type: 'html', 'css', 'asset'
// initiator: 'html' (in HTML file), 'css' (in CSS file)
// Example: Make all links absolute
if (!link.startsWith('http')) {
return 'https://example.com' + (link.startsWith('/') ? link : '/' + link)
}
return link
},
})
}
transformPath¶
Change where files are published:
export default async function (config) {
config.addPublicationTransformers({
transformPath(path, type) {
// type: 'html', 'css', 'asset'
// Example: Prefix all files with '/v1/'
return '/v1' + (path.startsWith('/') ? path : '/' + path)
},
})
}
Chaining transformers¶
Multiple transformers compose:
export default async function (config) {
// Transformer 1: Add hashes
config.addPublicationTransformers({
transformFile(file) {
if (file.type === 'css') {
file.name = file.name + '.hash'
}
return file
},
})
// Transformer 2: Minify
config.addPublicationTransformers({
transformFile(file) {
if (file.type === 'css') {
file.content = minifyCSS(file.content)
}
return file
},
})
}
Transformers run in order. The output of transformer 1 becomes input for transformer 2.
Common patterns¶
Environment-based transforms¶
export default async function (config) {
if (process.env.NODE_ENV === 'production') {
config.addPublicationTransformers({
transformFile(file) {
// Minify in production only
if (file.type === 'html') {
file.content = minifyHtml(file.content)
}
return file
},
})
}
}
Conditional transforms based on file¶
export default async function (config) {
config.addPublicationTransformers({
transformFile(file) {
// Only transform specific files
if (file.name === 'index.html') {
// Special handling for homepage
file.content = file.content.replace(/OLD_TEXT/g, 'NEW_TEXT')
}
return file
},
})
}
Logging and debugging¶
export default async function (config) {
config.addPublicationTransformers({
transformFile(file) {
console.log(`Processing ${file.name} (${file.type})`)
return file
},
transformPath(path, type) {
console.log(`Path: ${path} → Type: ${type}`)
return path
},
})
}
Performance considerations¶
Transformers run for every publication. Optimize:
// Cache expensive operations
const styleCache = new Map()
export default async function (config) {
config.addPublicationTransformers({
transformFile(file) {
if (file.type === 'css') {
// Check cache first
if (styleCache.has(file.name)) {
return { ...file, content: styleCache.get(file.name) }
}
// Expensive operation
const processed = processCSS(file.content)
styleCache.set(file.name, processed)
file.content = processed
}
return file
},
})
}
Troubleshooting¶
Transformer not applied¶
Check:
- Transformer is added in client config:
config.addPublicationTransformers(...) - Syntax is correct (all optional methods)
- No errors in browser console
- Transformer runs on the right files (check
typeparameter)
Add logging to debug:
config.addPublicationTransformers({
transformFile(file) {
console.log('Transforming:', file.name, file.type)
return file
},
})
URLs are broken after transform¶
Ensure transformPermalink matches transformPath:
config.addPublicationTransformers({
transformPath(path, type) {
if (type === 'css') {
return '/styles/' + path.split('/').pop()
}
return path
},
transformPermalink(link, type, initiator) {
// If CSS is in /styles/, update references
if (type === 'css' && initiator === 'html') {
return '/styles/' + link.split('/').pop()
}
return link
},
})
Performance issues¶
If publication is slow:
- Profile with DevTools
- Cache expensive operations
- Minimize file reads/writes
- Avoid blocking operations
See also¶
- Client configuration and plugins — Client config API
- build awesome build
- How publishing works — Publishing flow