diff --git a/packages/solid-router/package.json b/packages/solid-router/package.json
index 84ea201c06..8017971d2f 100644
--- a/packages/solid-router/package.json
+++ b/packages/solid-router/package.json
@@ -100,7 +100,6 @@
"dependencies": {
"@solid-devtools/logger": "^0.9.4",
"@solid-primitives/refs": "^1.0.8",
- "@solidjs/meta": "^0.29.4",
"@tanstack/history": "workspace:*",
"@tanstack/router-core": "workspace:*",
"@tanstack/solid-store": "^0.8.0",
diff --git a/packages/solid-router/src/Asset.tsx b/packages/solid-router/src/Asset.tsx
index 8e1e79bff2..17fe33eae9 100644
--- a/packages/solid-router/src/Asset.tsx
+++ b/packages/solid-router/src/Asset.tsx
@@ -1,4 +1,3 @@
-import { Link, Meta, Style, Title } from '@solidjs/meta'
import { onCleanup, onMount } from 'solid-js'
import { useRouter } from './useRouter'
import type { RouterManagedTag } from '@tanstack/router-core'
@@ -9,15 +8,43 @@ export function Asset({
attrs,
children,
}: RouterManagedTag): JSX.Element | null {
+ const router = useRouter()
+
+ if (router.isServer) {
+ switch (tag) {
+ case 'title':
+ return
{children}
+ case 'meta':
+ return
+ case 'link':
+ return
+ case 'style':
+ if (typeof children === 'string') {
+ return
+ }
+ return
+ case 'script':
+ if (attrs?.src && typeof attrs.src === 'string') {
+ return
+ }
+ if (typeof children === 'string') {
+ return
+ }
+ return
+ default:
+ return null
+ }
+ }
+
switch (tag) {
case 'title':
- return {children}
+ return
case 'meta':
- return
+ return
case 'link':
- return
+ return
case 'style':
- return
+ return
case 'script':
return
default:
@@ -138,3 +165,120 @@ function Script({
return null
}
+
+function HeadTag({
+ tag,
+ attrs,
+ children,
+}: {
+ tag: 'title' | 'meta' | 'link' | 'style'
+ attrs?: Record
+ children?: string
+}): null {
+ onMount(() => {
+ if (typeof document === 'undefined') {
+ return
+ }
+
+ const element = findOrCreateHeadElement(tag, attrs, children)
+
+ if (!element) {
+ return
+ }
+
+ onCleanup(() => {
+ if (element.parentNode) {
+ element.parentNode.removeChild(element)
+ }
+ })
+ })
+
+ return null
+}
+
+function findOrCreateHeadElement(
+ tag: 'title' | 'meta' | 'link' | 'style',
+ attrs?: Record,
+ children?: string,
+) {
+ const existing = findExistingHeadElement(tag, attrs, children)
+ if (existing) {
+ return existing
+ }
+
+ const element = document.createElement(tag)
+ setAttributes(element, attrs)
+
+ if (typeof children === 'string' && (tag === 'title' || tag === 'style')) {
+ element.textContent = children
+ }
+
+ document.head.appendChild(element)
+
+ return element
+}
+
+function findExistingHeadElement(
+ tag: 'title' | 'meta' | 'link' | 'style',
+ attrs?: Record,
+ children?: string,
+) {
+ const candidates = document.head.querySelectorAll(tag)
+ for (const candidate of candidates) {
+ if (!matchesAttributes(candidate, attrs)) {
+ continue
+ }
+
+ if (typeof children === 'string' && (tag === 'title' || tag === 'style')) {
+ if (candidate.textContent !== children) {
+ continue
+ }
+ }
+
+ return candidate as HTMLElement
+ }
+
+ return undefined
+}
+
+function matchesAttributes(
+ element: Element,
+ attrs?: Record,
+): boolean {
+ if (!attrs) {
+ return true
+ }
+
+ for (const [key, value] of Object.entries(attrs)) {
+ if (value === undefined || value === false) {
+ continue
+ }
+
+ if (value === true) {
+ if (!element.hasAttribute(key)) {
+ return false
+ }
+ continue
+ }
+
+ if (element.getAttribute(key) !== String(value)) {
+ return false
+ }
+ }
+
+ return true
+}
+
+function setAttributes(element: Element, attrs?: Record) {
+ if (!attrs) {
+ return
+ }
+
+ for (const [key, value] of Object.entries(attrs)) {
+ if (value === undefined || value === false) {
+ continue
+ }
+
+ element.setAttribute(key, value === true ? '' : String(value))
+ }
+}
diff --git a/packages/solid-router/src/HeadContent.tsx b/packages/solid-router/src/HeadContent.tsx
index 103be65c46..89018ac96c 100644
--- a/packages/solid-router/src/HeadContent.tsx
+++ b/packages/solid-router/src/HeadContent.tsx
@@ -1,5 +1,4 @@
import * as Solid from 'solid-js'
-import { MetaProvider } from '@solidjs/meta'
import { For } from 'solid-js'
import { Asset } from './Asset'
import { useRouter } from './useRouter'
@@ -191,9 +190,7 @@ export function HeadContent() {
const tags = useTags()
return (
-
- {(tag) => }
-
+ {(tag) => }
)
}
diff --git a/packages/solid-router/src/ssr/RouterServer.tsx b/packages/solid-router/src/ssr/RouterServer.tsx
index 99b4e684bb..39c278eed8 100644
--- a/packages/solid-router/src/ssr/RouterServer.tsx
+++ b/packages/solid-router/src/ssr/RouterServer.tsx
@@ -5,7 +5,6 @@ import {
ssr,
useAssets,
} from 'solid-js/web'
-import { MetaProvider } from '@solidjs/meta'
import { Asset } from '../Asset'
import { useTags } from '../HeadContent'
import { RouterProvider } from '../RouterProvider'
@@ -16,11 +15,11 @@ export function ServerHeadContent() {
const tags = useTags()
useAssets(() => {
return (
-
+ <>
{tags().map((tag) => (
))}
-
+ >
)
})
return null
@@ -44,11 +43,9 @@ export function RouterServer(props: {
router={props.router}
InnerWrap={(props) => (
-
-
- {props.children}
-
-
+
+ {props.children}
+
)}
/>
diff --git a/packages/solid-router/tests/Scripts.test.tsx b/packages/solid-router/tests/Scripts.test.tsx
index 6ffa0bd645..cbaf06a4a4 100644
--- a/packages/solid-router/tests/Scripts.test.tsx
+++ b/packages/solid-router/tests/Scripts.test.tsx
@@ -1,5 +1,5 @@
-import { describe, expect, test } from 'vitest'
-import { render } from '@solidjs/testing-library'
+import { afterEach, describe, expect, test } from 'vitest'
+import { cleanup, render } from '@solidjs/testing-library'
import {
HeadContent,
@@ -11,6 +11,10 @@ import {
} from '../src'
import { Scripts } from '../src/Scripts'
+afterEach(() => {
+ cleanup()
+})
+
describe('ssr scripts', () => {
test('it works', async () => {
const rootRoute = createRootRoute({
@@ -195,3 +199,80 @@ describe('ssr HeadContent', () => {
])
})
})
+
+describe('client HeadContent', () => {
+ test('renders and cleans up head tags', async () => {
+ const rootRoute = createRootRoute({
+ head: () => ({
+ meta: [
+ { title: 'Root' },
+ { name: 'description', content: 'Root description' },
+ ],
+ links: [{ rel: 'stylesheet', href: '/root.css' }],
+ styles: [{ children: '.root{color:red}' }],
+ }),
+ component: () => {
+ return
+ },
+ })
+
+ const indexRoute = createRoute({
+ path: '/',
+ getParentRoute: () => rootRoute,
+ head: () => ({
+ meta: [
+ { title: 'Index' },
+ { name: 'description', content: 'Index description' },
+ ],
+ links: [{ rel: 'stylesheet', href: '/index.css' }],
+ styles: [{ children: '.index{color:red}' }],
+ }),
+ })
+
+ const router = createRouter({
+ history: createMemoryHistory({
+ initialEntries: ['/'],
+ }),
+ routeTree: rootRoute.addChildren([indexRoute]),
+ })
+
+ await router.load()
+
+ const { unmount } = render(() => )
+
+ const meta = document.head.querySelector('meta[name="description"]')
+ const link = document.head.querySelector(
+ 'link[rel="stylesheet"][href="/index.css"]',
+ )
+ const style = Array.from(document.head.querySelectorAll('style')).find(
+ (el) => el.textContent === '.index{color:red}',
+ )
+ const title = Array.from(document.head.querySelectorAll('title')).find(
+ (el) => el.textContent === 'Index',
+ )
+
+ expect(meta?.getAttribute('content')).toBe('Index description')
+ expect(link).not.toBeNull()
+ expect(style).not.toBeUndefined()
+ expect(title).not.toBeUndefined()
+
+ unmount()
+
+ expect(
+ document.head.querySelector('meta[name="description"]'),
+ ).toBeNull()
+ expect(
+ document.head.querySelector('link[rel="stylesheet"][href="/index.css"]'),
+ ).toBeNull()
+ expect(
+ Array.from(document.head.querySelectorAll('style')).find(
+ (el) => el.textContent === '.index{color:red}',
+ ),
+ ).toBeUndefined()
+ expect(
+ Array.from(document.head.querySelectorAll('title')).find(
+ (el) => el.textContent === 'Index',
+ ),
+ ).toBeUndefined()
+ })
+})
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 0f8bf0ee50..f30bc797de 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -11703,9 +11703,6 @@ importers:
'@solid-primitives/refs':
specifier: ^1.0.8
version: 1.1.0(solid-js@1.9.10)
- '@solidjs/meta':
- specifier: ^0.29.4
- version: 0.29.4(solid-js@1.9.10)
'@tanstack/history':
specifier: workspace:*
version: link:../history