From 526d7bd69bbcf63ff680452e3bfcebd3eebc17b5 Mon Sep 17 00:00:00 2001 From: zamkovskayaai Date: Mon, 23 Mar 2026 14:35:50 +0300 Subject: [PATCH 1/2] feat: add support for tag in rendered HTML Add a new `base` option to `RenderParams` that allows setting `` in the document ``. - Add `Base` interface with optional `href` and `target` fields - Add `base?` to `HeadContent` and `RenderParams` interfaces - Wire `base` through `generateRenderContent` (including plugin access) - Render `` tag after `` and before `` - Export `Base` type from package index - Add tests for base tag rendering - Document in README.md and README-ru.md --- README-ru.md | 29 +++++++++++++++++++++++++++++ README.md | 29 +++++++++++++++++++++++++++++ src/index.ts | 1 + src/render.test.ts | 18 ++++++++++++++++++ src/types.ts | 7 +++++++ src/utils/generateRenderContent.ts | 4 +++- src/utils/renderHeadContent.ts | 14 ++++++++++++-- 7 files changed, 99 insertions(+), 3 deletions(-) diff --git a/README-ru.md b/README-ru.md index b4d059e..f4ec211 100644 --- a/README-ru.md +++ b/README-ru.md @@ -43,6 +43,8 @@ interface RenderParams<Data, Plugins> { icon?: Icon; // nonce to be set on the appropriate tags nonce?: string; + // base tag attributes + base?: Base; // common options // Page title @@ -85,6 +87,32 @@ interface RenderParams<Data, Plugins> { } ``` +### `Base` + +Описывает тег `base`: + +```typescript +interface Base { + href?: string; + target?: HTMLBaseElement['target']; +} +``` + +Например: + +```js +renderLayout({ + title: 'Home page', + base: {target: '_top'}, +}); +``` + +Будет выглядеть следующим образом: + +```html +<base target="_top" /> +``` + ### `Meta` Описывает тег `meta`: @@ -252,6 +280,7 @@ interface CommonOptions { } export interface HeadContent { + base?: Base; scripts: Script[]; helpers: RenderHelpers; links: Link[]; diff --git a/README.md b/README.md index 2f38f1c..6fb5dac 100644 --- a/README.md +++ b/README.md @@ -43,6 +43,8 @@ interface RenderParams<Data, Plugins> { icon?: Icon; // nonce to be set on the appropriate tags nonce?: string; + // base tag attributes + base?: Base; // common options // Page title @@ -85,6 +87,32 @@ interface RenderParams<Data, Plugins> { } ``` +### Base + +Describes `base` tag: + +```typescript +interface Base { + href?: string; + target?: HTMLBaseElement['target']; +} +``` + +Example: + +```js +renderLayout({ + title: 'Home page', + base: {target: '_top'}, +}); +``` + +Will be rendered as: + +```html +<base target="_top" /> +``` + ### Meta Describes `meta` tag: @@ -252,6 +280,7 @@ interface CommonOptions { } export interface HeadContent { + base?: Base; scripts: Script[]; helpers: RenderHelpers; links: Link[]; diff --git a/src/index.ts b/src/index.ts index b391db4..1017b0e 100644 --- a/src/index.ts +++ b/src/index.ts @@ -12,6 +12,7 @@ export { } from './plugins/index.js'; export type { + Base, Plugin, Icon, Link, diff --git a/src/render.test.ts b/src/render.test.ts index 9e993aa..a8207af 100644 --- a/src/render.test.ts +++ b/src/render.test.ts @@ -16,6 +16,24 @@ test('should allow `<html>` attributes override', () => { expect(createRenderFunction([dirPlugin()])({title: 'Foobar'})).toMatch('<html dir="ltr">'); }); +test('should render `<base>` tag with target', () => { + const html = createRenderFunction()({title: 'Foobar', base: {target: '_top'}}); + expect(html).toMatch('<base target="_top">'); +}); + +test('should render `<base>` tag with href and target', () => { + const html = createRenderFunction()({ + title: 'Foobar', + base: {href: 'https://example.com/', target: '_top'}, + }); + expect(html).toMatch('<base href="https://example.com/" target="_top">'); +}); + +test('should not render `<base>` tag when base is not provided', () => { + const html = createRenderFunction()({title: 'Foobar'}); + expect(html).not.toMatch('<base'); +}); + test('should render root content', () => { expect(createRenderFunction()({title: 'Foobar'})).toMatch(/<div id="root">\s*<\/div>/); expect(createRenderFunction()({title: 'Foobar', bodyContent: {root: 'content'}})).toMatch( diff --git a/src/types.ts b/src/types.ts index 191f01f..e60198c 100644 --- a/src/types.ts +++ b/src/types.ts @@ -32,6 +32,11 @@ export interface Icon { export type Meta = {name: string; content: string}; +export interface Base { + href?: string; + target?: HTMLBaseElement['target']; +} + export interface CommonOptions { title: string; lang?: string; @@ -39,6 +44,7 @@ export interface CommonOptions { } export interface HeadContent { + base?: Base; scripts: Script[]; helpers: RenderHelpers; links: Link[]; @@ -84,6 +90,7 @@ export interface RenderParams<Data, Plugins extends Plugin[] = []> extends Commo data?: Data; icon?: Icon; nonce?: string; + base?: Base; // content htmlAttributes?: Attributes; meta?: Meta[]; diff --git a/src/utils/generateRenderContent.ts b/src/utils/generateRenderContent.ts index e7f4c17..9bbf7ef 100644 --- a/src/utils/generateRenderContent.ts +++ b/src/utils/generateRenderContent.ts @@ -71,13 +71,14 @@ export function generateRenderContent<Plugins extends Plugin[], Data>( links.unshift({rel: 'icon', type: icon.type, sizes: icon.sizes, href: icon.href}); } - const {lang, isMobile, title, pluginsOptions = {}} = params; + const {lang, isMobile, title, base, pluginsOptions = {}} = params; for (const plugin of plugins ?? []) { plugin.apply({ options: hasProperty(pluginsOptions, plugin.name) ? pluginsOptions[plugin.name] : undefined, renderContent: { + base, htmlAttributes, meta, links, @@ -99,6 +100,7 @@ export function generateRenderContent<Plugins extends Plugin[], Data>( } return { + base, htmlAttributes, meta, links, diff --git a/src/utils/renderHeadContent.ts b/src/utils/renderHeadContent.ts index 926760d..1eb5c82 100644 --- a/src/utils/renderHeadContent.ts +++ b/src/utils/renderHeadContent.ts @@ -1,11 +1,21 @@ import type {HeadContent} from '../types.js'; export function renderHeadContent(content: HeadContent): string { - const {scripts, helpers, links, meta, styleSheets, inlineStyleSheets, inlineScripts, title} = - content; + const { + base, + scripts, + helpers, + links, + meta, + styleSheets, + inlineStyleSheets, + inlineScripts, + title, + } = content; return ` <meta charset="utf-8"> + ${base ? `<base ${helpers.attrs({...base})}>` : ''} <title>${title} ${[ ...scripts.map(({src, crossOrigin}) => From dd06eda7f2357bf88a0e64d2d474d88210968c9d Mon Sep 17 00:00:00 2001 From: zamkovskayaai Date: Fri, 3 Apr 2026 12:12:28 +0300 Subject: [PATCH 2/2] fix: after review --- src/types.ts | 2 +- src/utils/generateRenderContent.ts | 2 +- src/utils/renderHeadContent.ts | 4 +++- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/types.ts b/src/types.ts index e60198c..813bf31 100644 --- a/src/types.ts +++ b/src/types.ts @@ -62,7 +62,7 @@ export interface BodyContent { afterRoot: string[]; } -export interface RenderContent extends HeadContent { +export interface RenderContent extends Required { htmlAttributes: Attributes; bodyContent: BodyContent; } diff --git a/src/utils/generateRenderContent.ts b/src/utils/generateRenderContent.ts index 9bbf7ef..69e8a71 100644 --- a/src/utils/generateRenderContent.ts +++ b/src/utils/generateRenderContent.ts @@ -71,7 +71,7 @@ export function generateRenderContent( links.unshift({rel: 'icon', type: icon.type, sizes: icon.sizes, href: icon.href}); } - const {lang, isMobile, title, base, pluginsOptions = {}} = params; + const {lang, isMobile, title, base = {}, pluginsOptions = {}} = params; for (const plugin of plugins ?? []) { plugin.apply({ options: hasProperty(pluginsOptions, plugin.name) diff --git a/src/utils/renderHeadContent.ts b/src/utils/renderHeadContent.ts index 1eb5c82..1a7e342 100644 --- a/src/utils/renderHeadContent.ts +++ b/src/utils/renderHeadContent.ts @@ -13,9 +13,11 @@ export function renderHeadContent(content: HeadContent): string { title, } = content; + const baseAttrs = base ? helpers.attrs({...base}) : ''; + return ` - ${base ? `` : ''} + ${baseAttrs ? `` : ''} ${title} ${[ ...scripts.map(({src, crossOrigin}) =>