Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Customizing html template #14195

Closed
pi0 opened this issue Jun 22, 2022 · 54 comments · Fixed by #21615
Closed

Customizing html template #14195

pi0 opened this issue Jun 22, 2022 · 54 comments · Fixed by #21615

Comments

@pi0
Copy link
Member

pi0 commented Jun 22, 2022

History: In Nuxt 2, we were supporting the ability to provide a custom HTML template used for the renderer. While it was simple, several issues:

  • Only could be customized once by end-users (modules cannot hook to extend it and had to use other methods for injection)
  • Can be outdated if we wanted to add a new placeholder for the built-in template
  • Additional spaces or code format for customization can break hydration behavior
  • With Nitro and moving to standalone server builds, we had to convert it into a JS function
  • Different behavior for SSR/SPA is not covered
  • Runtime level customizations are not possible
  • Opaque behavior without ability to inspect by Nuxt

This is the anatomy of the built-in template as a standard HTML structure:

<!DOCTYPE html>
<html {{ HTML_ATTRS }}>
  <head {{ HEAD_ATTRS }}>
    {{ HEAD }}
  </head>
  <body {{ BODY_ATTRS }}>
    {{ APP }}
  </body>
</html>

The main use-case of providing a custom template is extending placeholders to add a global static script or style or attributes. With Nuxt 3, I'm proposing to have a configuration interface in order to extend template:

export default defineNuxtConfig({
  template: {
    htmlAttrs: [],
    headAttrs: [],
    headPrepend: [],
    headPrepend: [],
    bodyAttrs: [],
    bodyPrepend: [],
    bodyAppend: []
  }
})

(note: we shall make it consistent with RenderResult.meta interface)

This interface is forward-compatible, predictable, easily configurable and extandable by modules and always leads to a valid HTML output.

More dynamic customization and runtime behavior can be provided by nuxt:render hooks (#14182).

There was also an initial discussion to provide the ability completely override the template as an escape hatch. I believe unless it is proven there is a valid use case that cannot be supported by the Object interface, it is probably safer to not expose it. There might be cases we want to compile something that is not HTML (like a Native output?) but this probably deserves better refactor on renderer implementation to expose an API and allow full overriding renderer file, taking into account it is not rendering HTML.

@maxim1maslov
Copy link

My dude, I just wanted to thank you so much for this explanation. I know its about the issue I posted and I think it would be an awesome extension of functionality.

@Rigo-m
Copy link
Contributor

Rigo-m commented Jun 23, 2022

Custom html template was fairly easy to use when we needed to append GTM scripts in a really quick way.
Could it be feasible to create custom html to override placeholders?

For example, let's say this is the html template:

<!DOCTYPE html>
<html {{ HTML_ATTRS }}>
  <head {{ HEAD_ATTRS }}>
    {{ HEAD_PREPEND }}
    {{ HEAD }}
    {{ HEAD_APPEND }}
  </head>
  <body {{ BODY_ATTRS }}>
    {{ BODY_PREPEND }}
    {{ APP }}
    {{ BODY_PREPEND }}
  </body>
</html>

And i want to override BODY_PREPEND in a verbose manner, without passing from nuxt.config.ts, I'd like to be able to do this:
/app/template/body_prepend.html

<script src="https://something.com/some-lib.html" />
<div> This will be inserted in body_prepend placeholder </div>

WDYT?

@ennioVisco
Copy link
Contributor

While I can see how intuitive it is for people coming from other frameworks, I'm afraid that an index.html (and similarly a full-featured template object in the config) would be a too easy way to introduce bad design practices. Moreover, it will have ambiguous relations with the useHead/useMeta hooks (what happens if the same script is added to both?).
I'd be for introducing it only in response to a precise use case, and instead, prefer milder solutions to specific issues (e.g. a priority index for scripts that require to be loaded as first/last)

@Rigo-m
Copy link
Contributor

Rigo-m commented Jun 23, 2022

While I can see how intuitive it is for people coming from other frameworks, I'm afraid that an index.html (and similarly a full-featured template object in the config) would be a too easy way to introduce bad design practices. Moreover, it will have ambiguous relations with the useHead/useMeta hooks (what happens if the same script is added to both?). I'd be for introducing it only in response to a precise use case, and instead, prefer milder solutions to specific issues (e.g. a priority index for scripts that require to be loaded as first/last)

That's fair.
The issue to solve is this: as of right now, including a third party script inside nuxt.config.ts looks like this

{
          hid: "gtm",
          children: `(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':
          new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],
          j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src=
          'https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);
          })(window,document,'script','dataLayer','GTM-XXXXXXX');
          `,
          type: "text/javascript",
},

Looks awful and cumbersome. Any ideas on how to make it more elegant?

@pi0
Copy link
Member Author

pi0 commented Jun 23, 2022

I agree about DX points of verbosity introduced to nuxt.config.

It is probably most convenient if there is a module to add 3rd party scripts and we are working on @nuxt/script module, replacing @nuxtjs/gtm and other community script modules for the most convenience.

But I also like the idea of having an app/ file for partial injection 👍🏼 We can introduce app/head.html for headAppend shortcut. What do you think? (it seems the most common place for modification injecting styles and 3rd party scripts. differed scripts can be directly in a nuxt plugin)

@Rigo-m
Copy link
Contributor

Rigo-m commented Jun 23, 2022

IMHO there should be at least a app/head.html for headAppend, app/body-prepend.html and app/body-append.html. I know that you have a lot of granularity with script's async/defer/fetchpriority attributes, but many libraries still invites the user to put the scripts as first element of the body or as one of the last. Having a body-prepend and a body-append would be a better DX for users, since they would'n't have to ask themselves any question and just place the script in the place suggested by their service of reference.
Makes sense?

@pi0
Copy link
Member Author

pi0 commented Jun 23, 2022

I'm not saying it doesn't make sense or usecase. Object API as well as modules (local or external) have the full possibility to inject in all possible positions. And this is probably super temporary since with @nuxt/scripts we can cover most of 3rd party integrations in most elegant way. I don't want to blowup app/ directory with files that might have more rare use in the future but single head.html makes sense...

Do you have some common integration examples can benefit from body-prepend? As for body-append, putting same script in a nuxt plugin would have the same effect.

@Rigo-m
Copy link
Contributor

Rigo-m commented Jun 23, 2022

Google GTM's instructions asks you to place their script as first scripts in the head and firsts in the body:
Screenshot 2022-06-23 at 15 29 51

I don't believe it matters that much, but some users might want to follow guidelines as close as possible I think

@pi0
Copy link
Member Author

pi0 commented Jun 23, 2022

I see. Indeed makes sense for gtm for exact copy-paste BTW I wouldn't personally add no-script to a Nuxt app since without script Nuxt app is probably not working too and doesn't respect Users privacy when scripts are explicitly disabled by the browser. analytics scripts like to take over with the highest priority regardless of performance implications or user preferences 😅

Okay then regardless I think this all means we should accelerate to deliver @nuxt/scripts module to solve current DX issues and provide best practices out of the box. Module can benefit from object api.

@ennioVisco
Copy link
Contributor

ennioVisco commented Jun 23, 2022

Google GTM's instructions asks you to place their script as first scripts in the head and firsts in the body: Screenshot 2022-06-23 at 15 29 51

I don't believe it matters that much, but some users might want to follow guidelines as close as possible I think

It does matter, because GTM could be used to track page events and could even have conditions firing on some <meta> info.

Yet, I believe we should still be able to exploit compile-time pre-processing for optimizations.
I see more coherent to have something like:

<!-- page.vue -->
<script setup>
const gtm = "XXX"
</script>

<template>
  <div>
    <Head>
      <Script type="text/javascript" fetchpriority="high">
      // code using the {{ gtm }} variable
      </Script>
    </Head>
   ...
  </div>
</template>

This would be more in line with Nuxt's approach to head management, and still could be reduced to the cases where bad companies like Google force you to copy/paste 3rd-party scripts.

Edit: Of course, the noscript doesn't make any sense for a JS framework...

Edit #11665: maybe we could exploit Google's new fetchpriority (https://web.dev/priority-hints/) to inform the compiler about the order/priority.

@Rigo-m
Copy link
Contributor

Rigo-m commented Jun 23, 2022

If the website is fully statically generated, it should be able to be navigated even when there's no javascript going, therefore noscript could make sense. Am I wrong?

@ennioVisco
Copy link
Contributor

If the website is fully statically generated, it should be able to be navigated even when there's no javascript going, therefore noscript could make sense. Am I wrong?

That's not true, unfortunately. SSG means that the possible routes result in pre-generated HTML pages, but still, the dynamic data and reactivity will be handled by client-side javascript (the pre-generated pages don't have statically all the information, they are just better for SEO and can avoid server-side logic).

@Rigo-m
Copy link
Contributor

Rigo-m commented Jun 23, 2022

This website is "usable" with javascript disabled. It uses bridge + nitro. Menu doesn't work (because it's not implemented to be used with zero-javascript, although you can make a fallback), but you can still navigate the website (since SSR-wise nuxtLinks are rendered as anchor tags) and you can see contents. Btw, this discussion is not meant to be taken here :)

@jvrsolis
Copy link

jvrsolis commented Jul 2, 2022

Just a comment about my use case, I am using Nuxt 3 and ionic to generate an ios app. While most css classes are standard a-zA-Z the moment I added FormKit (which is meant to be nuxt 3 compatible). It would leave a blank screen due to this error

SyntaxError: Invalid regular expression: range out of order in character class
This is probably due to the $ symbol used in classes for FormKit.

All this to say that if I had a way to update the index.html generated to have a charset metatag of utf-8 the problem would go away. As of now I have to manually add it before deploying to ios every single time I generate the nuxt app or remove a really useful form package.

Using useHead wouldn't work since css is loaded before js so nuxt cannot load itself hence the blank screen.

Just hoping this is resolved soon 👍🏽

@ennioVisco
Copy link
Contributor

ennioVisco commented Jul 2, 2022

@Rigo-m btw did you look at this way of setting GTM?

@jvrsolis this is a bug per-se, did you report it? Meta-charset and few other tags should be the first in the .
For a reference of the high-priority tags, here is a good explanation: https://webmasters.stackexchange.com/a/81409

@justwiebe
Copy link

I think this would also be needed to use a Plugin like this https://github.com/hoiheart/vue-universal-modal#install-plugin

@danielroe
Copy link
Member

@jvrsolis You should be able to set the charset with:

import { defineNuxtConfig } from 'nuxt'

export default defineNuxtConfig({
  app: {
    head: {
      charset: 'utf-8'
    }
  }
})

But utf-8 should actually be the default value and if this is not being generated it's a (separate) bug - do raise a new issue.

@jvrsolis
Copy link

jvrsolis commented Jul 5, 2022

@danielroe

I still only get a file with the following when running nuxi generate. This file is what appears in safari and in the ios app inspect. No utf-8 meta tag so blank screen

<!DOCTYPE html>
<html >

<head >
  <link rel="stylesheet" href="/_nuxt/entry.6467e0f7.css">
</head>

<body >
  <div id="__nuxt"></div><script>window.__NUXT__={serverRendered:false,config:{public:{theme:"facs",themes:"facs,default",apiProtocol:"http",apiDomain:"api.facsware.local",baseUrl:"http:\u002F\u002Flocalhost:3000",appEnv:"local",appName:"FACS",appVersion:"0.0.0",supportEmail:"support@facsware.com"},app:{baseURL:"\u002F",buildAssetsDir:"\u002F_nuxt\u002F",cdnURL:""}}}</script><script type="module" src="/_nuxt/entry-061cc50e.mjs"></script>
</body>

</html>

This is my current nuxt config

export default defineNuxtConfig(() => {
	return {
		target: 'static',
		ssr: false,
		alias: {
			'@': resolve(__dirname)
		},
		vite: {
			build: {
				target: 'esnext'
			}
		},
		app: {
			head: {
				charset: 'utf-8',
				titleTemplate: '%s - FACS',
				meta: [
					{ name: 'viewport', content: 'viewport-fit=cover, width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no' }
				]
			}
		},
		css: [
			'@ionic/core/css/core.css',
			'@ionic/core/css/normalize.css',
			'@ionic/core/css/structure.css',
			'@ionic/core/css/typography.css',
			'@ionic/core/css/ionic.bundle.css',
			'@/assets/css/index.scss'
		],
		modules: [
			'@nuxtjs/dotenv',
			'@nuxtjs/tailwindcss',
			'@formkit/nuxt',
			'@intlify/nuxt3'
		],
		intlify: {
			localeDir: 'locales',
			vueI18n: {
			  locale: 'en'
			}
		},
		pwa: {
			icon: {},
			meta: {},
			manifest: {
				name: 'FACS'
			},
			workbox: {
				enabled: true,
				skipWaiting: true
			}
		},
		formkit: {
			configFile: './formkit.config.ts',
		},
		postcss: {
			plugins: {
				tailwindcss: {},
				autoprefixer: {},
			},
		},
		plugins: [],
		experimental: {
			reactivityTransform: true,
			viteNode: true,
		},
		publicRuntimeConfig: {
			theme: process.env.NUXT_PUBLIC_THEME || 'default',
			themes: process.env.NUXT_PUBLIC_THEMES || 'default',
			apiProtocol: process.env.NUXT_PUBLIC_API_PROTOCOL || 'http',
			apiDomain: process.env.NUXT_PUBLIC_API_DOMAIN || 'localhost',
			baseUrl: process.env.NUXT_PUBLIC_BASE_URL || 'http://localhost:3000',
			appEnv: process.env.NUXT_PUBLIC_APP_ENV || 'production',
			appName: process.env.NUXT_PUBLIC_APP_NAME || 'vite',
			appVersion: process.env.NUXT_PUBLIC_APP_VERSION || '0.0.0',
			supportEmail: process.env.NUXT_PUBLIC_SUPPORT_EMAIL || ''
		},
		privateRuntimeConfig: {
			apiSecret: process.env.API_SECRET
		}
	}
});

@danielroe
Copy link
Member

danielroe commented Jul 5, 2022

You need to set ssr: true to render meta tags - there is an open issue to render some tags on ssr: false.

@Schetnan
Copy link

Schetnan commented Sep 2, 2022

We have a need to insert something like the following at the top of our HTML document ( above everything else ). It is used by a corporate proxy that then uses the information inside of it to render an appropriate template and header/footer. in our Nuxt2 apps, we put this snippet at the top of the app.html file. I am wondering how we might do this in nuxt3 without the app.html or something equivalent?

    <!--
        META_TAGS={"robots": "noindex,nofollow"}
        PAGE_TITLE=The app title
        SITE=<A site known to the proxy>
        TEMPLATE_NAME=<A template name known to the proxy>
        ENABLE_DYNAMIC_HEAD_TAG=true
        ENABLE_DYNAMIC_TITLE=true
    -->

<head {{ HEAD_ATTRS }}>
  {{ HEAD }}
</head>

<div {{ HTML_ATTRS }}>
  <div {{ BODY_ATTRS }}>{{ APP }}</div>
</div>

@vaban-ru
Copy link

vaban-ru commented Sep 6, 2022

Some news about app.html in Nuxt 3?

@danielroe
Copy link
Member

@vaban-ru There are now extensive hooks that allow you to customise the rendering of your app. In your case, you would probably want to use the Nitro hook render:html. (You would do this with a Nitro plugin.)

@pierres
Copy link

pierres commented Sep 7, 2022

@danielroe I have been using the app:templates hook to override the getContents method of views/document.template.mjs. Since nuxt/framework@f58aa81 this is no longer possible and the HTML template itself is hard coded.

Could you elaborate how the nitro hook render:html could be used?

To give some context: We use ESI tags to inject some HTML into each Document right before it is send to the user. This also means these tags will be replaced by some markup on the client. Therefore we cannot use anything that is managed by e.g. Vue on the client without triggering hydration mismatch.

An example:

<!DOCTYPE html>
<html ${joinAttrs(html.htmlAttrs)} data-custom="value">
<head>${joinTags(html.head)}<!--esi <esi:include src="/some/stuff" /> --></head>
<body ${joinAttrs(html.bodyAttrs)}><!--esi <esi:include src="/more/stuff" /> -->${joinTags(html.bodyPreprend)}${joinTags(html.body)}${joinTags(html.bodyAppend)}</body>
</html>

EDIT
I think I got a solution.

  • Put a Nitro plugin into /server/plugins
  • e.g.
import { defineNitroPlugin } from 'nitropack/runtime/plugin'

export default defineNitroPlugin((nitroApp) => {
  nitroApp.hooks.hook('render:html', (html, { event }) => {
    html.head.push('<!--esi <esi:include src="/some/stuff" /> -->')
  })
})

This should work. Similar to html.bodyPreprend and html.bodyAppend it would be nice to pre- and append content to <head> to influence loading of resources (lightly).

@chuckntaylor
Copy link

This solution above by @pierres worked for me. In my case, I wanted a div at the end of the <body> for teleporting a fullscreen modal to.

// server/plugins/nitroPlugin.ts
import { defineNitroPlugin } from 'nitropack/runtime/plugin'

export default defineNitroPlugin((nitroApp) => {
  nitroApp.hooks.hook('render:html', (html, { event }) => {
    html.bodyAppend.push(`<div id="modal-root"></div>`)
  })
})

It barked a little about not finding the module 'nitropack/runtime/plugin' but it still worked just fine. No need to import in the nuxt.config.ts. I'm using Nuxt 3.0.0-rc.11 with Nitro 0.5.4

@kissu
Copy link

kissu commented Dec 1, 2022

Yes, we are aware of it, hence my initial answer to you.
Sorry for the inconvenience during your migration. 😞

@jakubmirejovsky
Copy link

jakubmirejovsky commented Dec 2, 2022

This issue will pop up to anyone who is googling Nuxt3 Google Analytics. GA on their own are very frustrating, so maybe let's introduce a way for developers how to quickly get things done with GA and Nuxt3? :)

@ennioVisco
Copy link
Contributor

This issue will pop up to anyone who is googling Nuxt3 Google Analytics. GA on their own are very frustrating, so maybe let's introduce a way for developers how to quickly get things done with GA and Nuxt3? :)

What about this? It works perfectly since RC1 I think. https://nuxt.com/docs/guide/directory-structure/plugins#vue-plugins

@ThomasBerneSV
Copy link

This issue will pop up to anyone who is googling Nuxt3 Google Analytics. GA on their own are very frustrating, so maybe let's introduce a way for developers how to quickly get things done with GA and Nuxt3? :)

What about this? It works perfectly since RC1 I think. https://nuxt.com/docs/guide/directory-structure/plugins#vue-plugins

It works with Google Tag but what about Google Tag Manager (Here are the differences: https://support.google.com/tagmanager/answer/7582054?hl=en)? We have a script to put at the top of the head and a noscript to put at the top of the body. I don't see a solution yet.

@danielroe
Copy link
Member

@ThomasBerneSV Please open a discussion with the code you are trying and we'll do our best to advise; this should be very possible using useHead within Nuxt at the moment.

@ThomasBerneSV
Copy link

@danielroe here is the discussion link: https://github.com/nuxt/framework/discussions/9590

@ThomasBerneSV
Copy link

@danielroe I didn't received an answer. Did you see the discussion?

@kissu
Copy link

kissu commented Dec 12, 2022

@ThomasBerneSV he probably did, no need to ping him on both sides.

@DAVIDhaker
Copy link

Are issue alive? Need posibility to modify base HTML template.

@manniL
Copy link
Member

manniL commented Jan 2, 2023

@DAVIDhaker I think the important parts to modify the base HTML template are there by now

@menuRivera
Copy link

For anyone looking for a way to include some custom html attributes, the solution that @pierres brings works like a charm!

// at server/plugins/html.render.ts
export default defineNitroPlugin((nitroApp) => {
  nitroApp.hooks.hook('render:html', (html, { event }) => {
    html.htmlAttrs.push("data-theme='light'")
  })
})

Result:
image

@ohroy
Copy link

ohroy commented May 19, 2023

How change <div id="__nuxt"></div> with a loader for SPA with ssr false ?

nitroApp.hooks.hook('render:html'

Need the server. But I only want a SPA page....

Copy link
Member

That hook will also be called when prerendering the /200.html page so you can still use it to customise your SPA loader page.

@ohroy
Copy link

ohroy commented May 19, 2023

That hook will also be called when prerendering the /200.html page so you can still use it to customise your SPA loader page.

Ok! thanks a lot!

@smortexa
Copy link

Any update on this feature?

@manniL
Copy link
Member

manniL commented Jun 14, 2023

The feature already exists (see nuxt/framework#6042) but lacks documentation

@danieldanielecki
Copy link

For those dealing just with GTM - there's a nowadays package https://github.com/zadigetvoltaire/nuxt-gtm, but it doesn't work for AW-* tags (zadigetvoltaire/nuxt-gtm#15).

However, the solution with server/plugins/html.render.ts works completely fine.
It was new to me; it imports the plugin automatically, so I paste the docs for the future folks: https://nuxt.com/docs/guide/directory-structure/server#server-plugins.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging a pull request may close this issue.