ElMassimo/iles

Provide an easier way to alter page content in ssg.beforePageRender hook

TechAkayy opened this issue · 5 comments

Currently, when building for production, we can use the ssg.beforePageRender hook to alter the page content before it's generated.

While changing inner content of the body is quite uncommon, prepending & appending to head & body tag is quite common.

Describe the solution you'd like
It would be good to have an easier way to inject tags into body & head tags.

Describe alternatives you've considered
Currently, we have to use some library like node-html-parser to parse the html & modify tags selectively.

Additional context
Some inspirations:

transformIndexHtml = (html, context) => {
  return {
	html: html.replace('{{ title }}', 'Test HTML transforms'),
	tags: [{
            tag: 'meta',
            attrs: { name: 'description', content: 'a vite app' },
            // default injection is head-prepend
          },
          {
            tag: 'meta',
            attrs: { name: 'keywords', content: 'es modules' },
            injectTo: 'head',
  	}]
  }
}
export default defineNitroPlugin((nitroApp) => {
  nitroApp.hooks.hook('render:html', (html, { event }) => {
    html.head.push('<script src="/pgia/lib/index.js "> </script>')
    html.bodyAppend.push('<hr>Appended by custom plugin')
  })
})
ouuan commented

prepending & appending to head & body tag is quite common.

But why would you do this in the hook instead of useHead, <Head>, etc.?

That was just some examples. This feature request is general in nature.

Thanks for the pointer. I just checked the docs and learnt about those composables. And they don't include adding a script to the body tag.

In my case, Im actually working on an Iles module, where I want to add some specific scripts to the pages during build.

In fact, my iles module include a combination of a closely coupled vite plugin & the ssg. beforePageRender hook. So, during development, the transformIndexHtml will do the script injection, and during build, the ssg.beforePageRender will do the same script injection.

As the script injection is the same during dev & build, I see its easy to append to body with the vite plugin, while with iles's ssg beforePageRender, I have to use a parser.

Meanwhile, here is my Iles module, where I'm using node-html-parser to parse the index.html to inject (prepend/append) my scripts (body/head) at the moment. Linking this to related discord chat!

// @ts-nocheck
import type { IlesModule } from 'iles'
import type { UserConfig } from 'vite'
import htmlParser from 'node-html-parser'

// import { fileURLToPath, URL } from 'node:url'
import path from 'path'
import fs from 'fs'

export default async function (): IlesModule {
	return {
		name: '@pinegrow/iles-module',
		config(config) {
			config.vue.reactivityTransform = false
			config.ssg.beforePageRender = (page /*, config*/) => {
					const doc = htmlParser.parse(page.rendered, 'text/html')

					const addons = [
						{
							name: 'pgia',
							resources: [
								{
									// condition: 'interactions',
									parentResource: 'public/pgia', // relative to project root, must exists
									injectTo: 'head-append',
									tag: 'script',
									children: `(function(){ /* code */})()`,
								},
								{
									// condition: 'interactions',
									parentResource: `public/pgia`, // relative to project root, must exists
									injectTo: 'body-append',
									tag: 'script',
									attrs: { src: '/pgia/lib/index.js' },
								},
							],
						},
					]

					addons.forEach(addon => {
						addon.resources.forEach(resource => {
							if (!resource.condition /*|| this.customOptions[resource.condition] */) {
								try {
									let parentResourceExists = true

									if (resource.parentResource) {
										const projectRoot = process.cwd()
										const resourcePath = path.resolve(projectRoot, resource.parentResource)
										parentResourceExists = parentResourceExists && fs.existsSync(resourcePath)
									}

									if (parentResourceExists) {
										const attrContent = Object.entries(resource.attrs || {}).reduce(
											(acc, [key, value]) => {
												return `${acc}${key}${value && value !== true ? `="${value}"` : ''}`
											},
											''
										)

										let parentNode, sourceNode
										switch (resource.injectTo) {
											case 'head-prepend':
												parentNode = doc.querySelector('head')
												sourceNode = htmlParser.parse(`<${resource.tag} ${attrContent}>
                        ${resource.children || ''}
                        </${resource.tag}>`)
												parentNode.childNodes.unshift(sourceNode)
												break
											case 'head-append':
												parentNode = doc.querySelector('head')
												sourceNode = htmlParser.parse(`<${resource.tag} ${attrContent}>
                        ${resource.children || ''}
                        </${resource.tag}>`)
												parentNode.appendChild(sourceNode)
												break

											case 'body-prepend':
												parentNode = doc.querySelector('body')
												sourceNode = htmlParser.parse(`<${resource.tag} ${attrContent}>
                        ${resource.children || ''}
                        </${resource.tag}>`)
												parentNode.childNodes.unshift(sourceNode)
												break

											case 'body-append':
												parentNode = doc.querySelector('body')
												sourceNode = htmlParser.parse(`<${resource.tag} ${attrContent}>
                        ${resource.children || ''}
                        </${resource.tag}>`)
												parentNode.appendChild(sourceNode)
												break
										}
									}
								} catch (err) {
									// console.log(err)
								}
							}
						})
					})
					page.rendered = doc.outerHTML
				},
			},
		},
	}
}

Thanks for the pointer. I just checked the docs and learnt about those composables. And they don't include adding a script to the body tag.

I have learnt that useHead composable can have tagPosition that can take options 'head' | 'bodyClose' | 'bodyOpen', so it is possible to add to body as well via the useHead. Still, would like to know how this can achieved when we want to achieve the same via an iles module. thanks.