diff --git a/packages/router/.size-limit.json b/packages/router/.size-limit.json index e9ad0285..2e8fe992 100644 --- a/packages/router/.size-limit.json +++ b/packages/router/.size-limit.json @@ -4,7 +4,7 @@ "gzip": true, "path": "dist/index.production.js", "import": "*", - "limit": "4.15 kB" + "limit": "4.2 kB" }, { "name": "All publics (Brotli)", diff --git a/packages/router/src/head.spec.ts b/packages/router/src/head.spec.ts index a0d3028f..6ffbda11 100644 --- a/packages/router/src/head.spec.ts +++ b/packages/router/src/head.spec.ts @@ -16,11 +16,23 @@ import { dir, link, meta, - script + script, + toHtmlAttribute } from './head.js' describe('router', () => { describe('head', () => { + describe('toHtmlAttribute', () => { + it('should map httpEquiv to http-equiv', () => { + expect(toHtmlAttribute('httpEquiv')).toBe('http-equiv') + }) + + it('should lowercase other prop names', () => { + expect(toHtmlAttribute('crossOrigin')).toBe('crossorigin') + expect(toHtmlAttribute('content')).toBe('content') + }) + }) + describe('syncHead', () => { describe('title', () => { beforeEach(() => { @@ -589,6 +601,36 @@ describe('router', () => { stop() }) + it('should render httpEquiv as the http-equiv attribute', () => { + const stop = syncHead(() => ({ + default: null, + Head$: () => [meta({ + httpEquiv: 'refresh', + content: '0; url=/next' + })] + })) + + expect(document.head.innerHTML).toBe('') + + stop() + }) + + it('should hydrate existing http-equiv meta tag', () => { + document.head.innerHTML = '' + + const stop = syncHead(() => ({ + default: null, + Head$: () => [meta({ + httpEquiv: 'refresh', + content: '0; url=/next' + })] + })) + + expect(document.head.innerHTML).toBe('') + + stop() + }) + it('should hydrate existing meta tag', () => { document.head.innerHTML = '' diff --git a/packages/router/src/head.ts b/packages/router/src/head.ts index c956a5fc..4b49199c 100644 --- a/packages/router/src/head.ts +++ b/packages/router/src/head.ts @@ -113,6 +113,16 @@ export function dir($value: DirValue): DirPropertyDescriptor { } } +/** + * Converts a head descriptor prop name to its HTML attribute name. + * @param prop - Prop name, such as `httpEquiv` or `crossOrigin`. + * @returns The attribute name, such as `http-equiv` or `crossorigin`. + */ +/* @__NO_SIDE_EFFECTS__ */ +export function toHtmlAttribute(prop: string) { + return prop === 'httpEquiv' ? 'http-equiv' : prop.toLowerCase() +} + function buildPredicate(tag: string, attributes: [string, AnySignalish][]) { let selector = tag let count = 0 @@ -126,7 +136,7 @@ function buildPredicate(tag: string, attributes: [string, AnySignalish][]) { code = String(resolvedValue) } else { count++ - selector += `[${key}="${String(resolvedValue).replace(/"/g, '\\"')}"]` + selector += `[${toHtmlAttribute(key)}="${String(resolvedValue).replace(/"/g, '\\"')}"]` } } } @@ -188,7 +198,7 @@ function startElement( if (key === 'code') { element.textContent = stringValue } else { - element.setAttribute(key, stringValue) + element.setAttribute(toHtmlAttribute(key), stringValue) } } } diff --git a/packages/ssr/src/renderer/utils.spec.ts b/packages/ssr/src/renderer/utils.spec.ts index 028eb06e..5de56ca0 100644 --- a/packages/ssr/src/renderer/utils.spec.ts +++ b/packages/ssr/src/renderer/utils.spec.ts @@ -48,6 +48,13 @@ describe('ssr', () => { }))).toBe('') }) + it('should render httpEquiv as the http-equiv attribute', () => { + expect(headDescriptorToHtml(meta({ + httpEquiv: 'refresh', + content: '0; url=/next' + }))).toBe('') + }) + it('should insert script code as is', () => { expect(headDescriptorToHtml(script({ type: 'application/ld+json', diff --git a/packages/ssr/src/renderer/utils.ts b/packages/ssr/src/renderer/utils.ts index 81125b73..a9932b0d 100644 --- a/packages/ssr/src/renderer/utils.ts +++ b/packages/ssr/src/renderer/utils.ts @@ -2,7 +2,8 @@ import { type HeadDescriptor, type Location, type PageRef, - PermanentReplaceHistoryAction + PermanentReplaceHistoryAction, + toHtmlAttribute } from '@nano_kit/router' import { type EmptyValue, @@ -84,7 +85,7 @@ export function headDescriptorToHtml(descriptor: HeadDescriptor): string { if (key === 'code') { code = String(resolvedValue) } else { - html += ` ${key.toLowerCase()}="${escapeHtml(String(resolvedValue))}"` + html += ` ${toHtmlAttribute(key)}="${escapeHtml(String(resolvedValue))}"` } } })