Publication transformers¶
Hooks that run in Silex when you publish, before the files go to the forge. They do not run in the CI build.
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