Skip to content

Commit c5eed67

Browse files
feat: allow tags insertion before </body> (#67)
Co-authored-by: Daniel Roe <[email protected]>
1 parent 1f6bec8 commit c5eed67

File tree

6 files changed

+96
-25
lines changed

6 files changed

+96
-25
lines changed

README.md

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ import { renderHeadToString } from '@vueuse/head'
7070
const appHTML = await renderToString(yourVueApp)
7171

7272
// `head` is created from `createHead()`
73-
const { headTags, htmlAttrs, bodyAttrs } = renderHeadToString(head)
73+
const { headTags, htmlAttrs, bodyAttrs, bodyTags } = renderHeadToString(head)
7474

7575
const finalHTML = `
7676
<html${htmlAttrs}>
@@ -81,6 +81,7 @@ const finalHTML = `
8181
8282
<body${bodyAttrs}>
8383
<div id="app">${appHTML}</div>
84+
${bodyTags}
8485
</body>
8586
8687
</html>
@@ -132,6 +133,19 @@ useHead({
132133
})
133134
```
134135

136+
To render tags at the end of the `<body>`, set `body: true` in a HeadAttrs Object.
137+
138+
```ts
139+
useHead({
140+
script: [
141+
{
142+
children: `console.log('Hello world!')`,
143+
body: true
144+
},
145+
],
146+
})
147+
```
148+
135149
To set the `textContent` of an element, use the `children` attribute:
136150

137151
```ts
@@ -186,13 +200,15 @@ Note that you need to use `<html>` and `<body>` to set `htmlAttrs` and `bodyAttr
186200
- Returns: `HTMLResult`
187201

188202
```ts
189-
interface HTMLResult {
203+
export interface HTMLResult {
190204
// Tags in `<head>`
191205
readonly headTags: string
192206
// Attributes for `<html>`
193207
readonly htmlAttrs: string
194208
// Attributes for `<body>`
195209
readonly bodyAttrs: string
210+
// Tags in `<body>`
211+
readonly bodyTags: string
196212
}
197213
```
198214

example/app.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,12 @@ export const createApp = () => {
6262
key: 'zh',
6363
},
6464
],
65+
script: [
66+
{
67+
children: `console.log('hi')`,
68+
body: true
69+
},
70+
],
6571
})
6672
return () => (
6773
<div>

src/constants.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,5 @@ export const HEAD_COUNT_KEY = `head:count`
44
export const HEAD_ATTRS_KEY = `data-head-attrs`
55

66
export const SELF_CLOSING_TAGS = ['meta', 'link', 'base']
7+
8+
export const BODY_TAG_ATTR_NAME = `data-meta-body`

src/create-element.ts

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { BODY_TAG_ATTR_NAME } from "./constants"
2+
13
export const createElement = (
24
tag: string,
35
attrs: { [k: string]: any },
@@ -6,16 +8,21 @@ export const createElement = (
68
const el = document.createElement(tag)
79

810
for (const key of Object.keys(attrs)) {
9-
let value = attrs[key]
11+
if (key === 'body' && attrs.body === true) {
12+
// set meta-body attribute to add the tag before </body>
13+
el.setAttribute(BODY_TAG_ATTR_NAME, 'true')
14+
} else {
15+
let value = attrs[key]
1016

11-
if (key === 'key' || value === false) {
12-
continue
13-
}
17+
if (key === 'key' || value === false) {
18+
continue
19+
}
1420

15-
if (key === 'children') {
16-
el.textContent = value
17-
} else {
18-
el.setAttribute(key, value)
21+
if (key === 'children') {
22+
el.textContent = value
23+
} else {
24+
el.setAttribute(key, value)
25+
}
1926
}
2027
}
2128

src/index.ts

Lines changed: 46 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {
1414
HEAD_COUNT_KEY,
1515
HEAD_ATTRS_KEY,
1616
SELF_CLOSING_TAGS,
17+
BODY_TAG_ATTR_NAME,
1718
} from './constants'
1819
import { createElement } from './create-element'
1920
import { stringifyAttrs } from './stringify-attrs'
@@ -40,6 +41,7 @@ export type HeadObjectPlain = UnwrapRef<HeadObject>
4041
export type HeadTag = {
4142
tag: string
4243
props: {
44+
body?: boolean
4345
[k: string]: any
4446
}
4547
}
@@ -63,6 +65,8 @@ export interface HTMLResult {
6365
readonly htmlAttrs: string
6466
// Attributes for `<body>`
6567
readonly bodyAttrs: string
68+
// Tags in `<body>`
69+
readonly bodyTags: string
6670
}
6771

6872
const getTagKey = (
@@ -172,20 +176,30 @@ const updateElements = (
172176
tags: HeadTag[],
173177
) => {
174178
const head = document.head
179+
const body = document.body
175180
let headCountEl = head.querySelector(`meta[name="${HEAD_COUNT_KEY}"]`)
181+
let bodyMetaElements = body.querySelectorAll(`[${BODY_TAG_ATTR_NAME}]`);
176182
const headCount = headCountEl
177183
? Number(headCountEl.getAttribute('content'))
178184
: 0
179-
const oldElements: Element[] = []
185+
const oldHeadElements: Element[] = []
186+
const oldBodyElements: Element[] = []
180187

188+
if (bodyMetaElements) {
189+
for (let i = 0; i < bodyMetaElements.length; i++) {
190+
if (bodyMetaElements[i] && bodyMetaElements[i].tagName?.toLowerCase() === type) {
191+
oldBodyElements.push(bodyMetaElements[i])
192+
}
193+
}
194+
}
181195
if (headCountEl) {
182196
for (
183197
let i = 0, j = headCountEl.previousElementSibling;
184198
i < headCount;
185199
i++, j = j?.previousElementSibling || null
186200
) {
187201
if (j?.tagName?.toLowerCase() === type) {
188-
oldElements.push(j)
202+
oldHeadElements.push(j)
189203
}
190204
}
191205
} else {
@@ -194,28 +208,34 @@ const updateElements = (
194208
headCountEl.setAttribute('content', '0')
195209
head.append(headCountEl)
196210
}
197-
let newElements = tags.map((tag) =>
198-
createElement(tag.tag, tag.props, document),
199-
)
211+
let newElements = tags.map((tag) => ({
212+
element: createElement(tag.tag, tag.props, document),
213+
body: tag.props.body ?? false
214+
}))
200215

201216
newElements = newElements.filter((newEl) => {
202-
for (let i = 0; i < oldElements.length; i++) {
203-
const oldEl = oldElements[i]
204-
if (isEqualNode(oldEl, newEl)) {
205-
oldElements.splice(i, 1)
217+
for (let i = 0; i < oldHeadElements.length; i++) {
218+
const oldEl = oldHeadElements[i]
219+
if (isEqualNode(oldEl, newEl.element)) {
220+
oldHeadElements.splice(i, 1)
206221
return false
207222
}
208223
}
209224
return true
210225
})
211226

212-
oldElements.forEach((t) => t.parentNode?.removeChild(t))
227+
oldBodyElements.forEach((t) => t.parentNode?.removeChild(t))
228+
oldHeadElements.forEach((t) => t.parentNode?.removeChild(t))
213229
newElements.forEach((t) => {
214-
head.insertBefore(t, headCountEl)
230+
if (t.body === true) {
231+
body.insertAdjacentElement('beforeend', t.element)
232+
} else {
233+
head.insertBefore(t.element, headCountEl)
234+
}
215235
})
216236
headCountEl.setAttribute(
217237
'content',
218-
'' + (headCount - oldElements.length + newElements.length),
238+
'' + (headCount - oldHeadElements.length + newElements.filter(t => !t.body).length),
219239
)
220240
}
221241

@@ -341,27 +361,35 @@ export const useHead = (obj: MaybeRef<HeadObject>) => {
341361
}
342362

343363
const tagToString = (tag: HeadTag) => {
364+
let isBodyTag = false
365+
if (tag.props.body) {
366+
isBodyTag = true
367+
// avoid rendering body attr
368+
delete tag.props.body
369+
}
344370
let attrs = stringifyAttrs(tag.props)
345-
346371
if (SELF_CLOSING_TAGS.includes(tag.tag)) {
347-
return `<${tag.tag}${attrs}>`
372+
return `<${tag.tag}${attrs}${isBodyTag ? ' ' + BODY_TAG_ATTR_NAME : ''}>`
348373
}
349374

350-
return `<${tag.tag}${attrs}>${tag.props.children || ''}</${tag.tag}>`
375+
return `<${tag.tag}${attrs}${isBodyTag ? ' ' + BODY_TAG_ATTR_NAME : ''}>${tag.props.children || ''}</${tag.tag}>`
351376
}
352377

353378
export const renderHeadToString = (head: HeadClient): HTMLResult => {
354379
const tags: string[] = []
355380
let titleTag = ''
356381
let htmlAttrs: HeadAttrs = {}
357382
let bodyAttrs: HeadAttrs = {}
383+
let bodyTags: string[] = []
358384
for (const tag of head.headTags) {
359385
if (tag.tag === 'title') {
360386
titleTag = tagToString(tag)
361387
} else if (tag.tag === 'htmlAttrs') {
362388
Object.assign(htmlAttrs, tag.props)
363389
} else if (tag.tag === 'bodyAttrs') {
364390
Object.assign(bodyAttrs, tag.props)
391+
} else if (tag.props.body) {
392+
bodyTags.push(tagToString(tag))
365393
} else {
366394
tags.push(tagToString(tag))
367395
}
@@ -384,6 +412,9 @@ export const renderHeadToString = (head: HeadClient): HTMLResult => {
384412
[HEAD_ATTRS_KEY]: Object.keys(bodyAttrs).join(','),
385413
})
386414
},
415+
get bodyTags() {
416+
return bodyTags.join('')
417+
}
387418
}
388419
}
389420

tests/test.tsx

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -211,3 +211,12 @@ test('script key', async (t) => {
211211
`<script>console.log('B')</script><meta name="head:count" content="1">`,
212212
)
213213
})
214+
215+
test('body script', async (t) => {
216+
const page = await t.context.browser.newPage()
217+
await page.goto(`http://localhost:3000`)
218+
219+
const script = await page.$('[data-meta-body]')
220+
const scriptHtml = await script.innerHTML()
221+
t.is(scriptHtml, `console.log('hi')`)
222+
})

0 commit comments

Comments
 (0)