Skip to content

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:

  1. renderComponent — Override how GrapesJS renders components to HTML
  2. renderCssRule — Override how GrapesJS renders CSS rules
  3. transformFile — Modify files (HTML, CSS, assets) before publishing
  4. transformPermalink — Rewrite URLs in HTML and CSS
  5. 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
    },
  })
}

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:

  1. Transformer is added in client config: config.addPublicationTransformers(...)
  2. Syntax is correct (all optional methods)
  3. No errors in browser console
  4. Transformer runs on the right files (check type parameter)

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:

  1. Profile with DevTools
  2. Cache expensive operations
  3. Minimize file reads/writes
  4. Avoid blocking operations

See also

Edit this page on GitLab