diff --git a/README.md b/README.md
index ed5cbe7..515fbd5 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,3 @@
-gt pul
diff --git a/docs/src/routes/guide/(2)config.mdx b/docs/src/routes/guide/(2)config.mdx
index db697e2..43b0a6c 100644
--- a/docs/src/routes/guide/(2)config.mdx
+++ b/docs/src/routes/guide/(2)config.mdx
@@ -76,7 +76,7 @@ There are several options for setting site-wide metadata and behavior. These opt
// ..
{
title: "My Documentation Site",
- titleTemplate: "%s - MySite",
+ titleTemplate: ":title - MySite",
description: "A comprehensive guide to MySite.",
siteUrl: "https://docs.example.com",
logo: "/logo.png",
@@ -86,6 +86,10 @@ There are several options for setting site-wide metadata and behavior. These opt
Set `siteUrl` to the canonical public URL for your site. SolidBase uses it as the shared base for generated sitemap URLs, `robots.txt`, and default Open Graph URL metadata.
+SolidBase resolves `title`, `titleTemplate`, and `description` from the active route config. Page frontmatter overrides those values. Client-side navigation updates the document title, description, Open Graph metadata, and Twitter metadata for the new route.
+
+When `siteUrl` is set, SolidBase also emits a canonical link and `og:url`. The URL uses the route pathname without its query string or hash. Without `siteUrl`, these URL tags are omitted.
+
:::note
For multilingual support, use the `routes.locale` option. More details can be found in the [Internationalization guide](/guide/features/i18n).
:::
diff --git a/docs/src/routes/reference/frontmatter.mdx b/docs/src/routes/reference/frontmatter.mdx
index 7dc9dcd..d529991 100644
--- a/docs/src/routes/reference/frontmatter.mdx
+++ b/docs/src/routes/reference/frontmatter.mdx
@@ -38,6 +38,20 @@ The title is formatted from the config:
}
```
+Set `titleTemplate` in page frontmatter to override the active route config for one page. Use `:title` where the page title must appear.
+
+## Description
+
+Type: `string`
+
+Overrides the active route description for the document description, Open Graph description, and Twitter description.
+
+```md
+---
+description: Install and configure SolidBase.
+---
+```
+
## Layout
Type: `"home" | undefined`
diff --git a/docs/src/solidbase-theme/Layout.tsx b/docs/src/solidbase-theme/Layout.tsx
index 081b840..7bc791f 100644
--- a/docs/src/solidbase-theme/Layout.tsx
+++ b/docs/src/solidbase-theme/Layout.tsx
@@ -1,9 +1,7 @@
import { useLocale, useSolidBaseContext } from "@kobalte/solidbase/client";
import { DefaultThemeComponentsProvider } from "@kobalte/solidbase/default-theme/context.jsx";
-import { useDefaultThemeFrontmatter } from "@kobalte/solidbase/default-theme/frontmatter.js";
import Layout from "@kobalte/solidbase/default-theme/Layout.jsx";
import { Meta } from "@solidjs/meta";
-import { useLocation } from "@solidjs/router";
import type { ComponentProps } from "solid-js";
// import { OGImage } from "./og-image"; // re enable after start 2 vite 8 release
@@ -20,34 +18,13 @@ export default function (props: ComponentProps) {
}
function OpenGraph() {
- const location = useLocation();
-
const solidBaseCtx = useSolidBaseContext();
const locale = useLocale();
- const frontmatter = useDefaultThemeFrontmatter();
return (
<>
-
-
-
-
-
{/**/}
>
);
diff --git a/src/client/Root.tsx b/src/client/Root.tsx
index 831ceab..c4b03fc 100644
--- a/src/client/Root.tsx
+++ b/src/client/Root.tsx
@@ -5,10 +5,21 @@ import { Layout, mdxComponents } from "virtual:solidbase/components";
// as different modules, resulting in this file getting its own MDXContext (id `file://.../mdx.js),
// and the MDX files sharing another (id `@kobalte/solidbase/mdx`).
import { MDXProvider } from "virtual:solidbase/mdx";
-import { Meta, MetaProvider, Title } from "@solidjs/meta";
-import { createMemo, onMount, type ParentProps, Suspense } from "solid-js";
+import { Link, Meta, MetaProvider, Title } from "@solidjs/meta";
+import { useLocation } from "@solidjs/router";
+import {
+ createMemo,
+ onMount,
+ type ParentProps,
+ Show,
+ Suspense,
+} from "solid-js";
import { useRouteSolidBaseConfig } from "./config.js";
import { SolidBaseContext } from "./context.jsx";
+import {
+ resolveDocumentMetadata,
+ resolveHeadMetadata,
+} from "./document-metadata.js";
export function SolidBaseRoot(
props: ParentProps & {
@@ -56,26 +67,45 @@ import { SolidBaseRoutesContextProvider } from "./routes.js";
export function Inner(props: ParentProps) {
const config = useRouteSolidBaseConfig();
+ const location = useLocation();
const pageData = useCurrentPageData();
-
- const metaTitle = createMemo(() => {
- const titleTemplate =
- pageData()?.frontmatter.titleTemplate ?? config().titleTemplate;
-
- const title = pageData()?.frontmatter?.title ?? config().title;
-
- if (titleTemplate?.includes(":title"))
- return titleTemplate.replace(":title", title);
- return `${title} - ${titleTemplate ?? config().title}`;
- });
-
- const description = () =>
- pageData()?.frontmatter?.description ?? config().description;
+ // @solidjs/meta cannot retract tags from an incomplete SSR pass.
+ const metadata = createMemo(() =>
+ resolveHeadMetadata(config(), pageData(), location),
+ );
+ const metaTitle = () =>
+ metadata()?.title ?? resolveDocumentMetadata(config()).title;
return (
- {metaTitle()}
- {description() && }
+
+ {(head) => (
+ <>
+ {head.title}
+
+ {(description) => (
+ <>
+
+
+
+ >
+ )}
+
+
+
+
+ {(url) => (
+ <>
+
+
+ >
+ )}
+
+
+
+ >
+ )}
+
{props.children}
);
diff --git a/src/client/document-metadata.ts b/src/client/document-metadata.ts
new file mode 100644
index 0000000..635e3f0
--- /dev/null
+++ b/src/client/document-metadata.ts
@@ -0,0 +1,49 @@
+import type { Location } from "@solidjs/router";
+
+import type { BaseFrontmatter } from "./page-data.js";
+
+type MetadataConfig = {
+ description?: string;
+ siteUrl?: string;
+ title: string;
+ titleTemplate?: string;
+};
+
+type MetadataPageData = {
+ frontmatter: BaseFrontmatter;
+};
+
+export function resolveDocumentMetadata(
+ config: MetadataConfig,
+ frontmatter?: BaseFrontmatter,
+) {
+ const titleTemplate = frontmatter?.titleTemplate ?? config.titleTemplate;
+ const title = frontmatter?.title ?? config.title;
+
+ return {
+ description: frontmatter?.description ?? config.description,
+ title: titleTemplate?.includes(":title")
+ ? titleTemplate.replace(":title", title)
+ : `${title} - ${titleTemplate ?? config.title}`,
+ };
+}
+
+export function resolveCanonicalUrl(
+ siteUrl: string | undefined,
+ location: Pick,
+) {
+ return siteUrl ? new URL(location.pathname, siteUrl).toString() : undefined;
+}
+
+export function resolveHeadMetadata(
+ config: MetadataConfig,
+ pageData: MetadataPageData | undefined,
+ location: Pick,
+) {
+ if (!pageData) return;
+
+ return {
+ ...resolveDocumentMetadata(config, pageData.frontmatter),
+ canonicalUrl: resolveCanonicalUrl(config.siteUrl, location),
+ };
+}
diff --git a/tests/client/config.test.ts b/tests/client/config.test.ts
index db71014..55b6217 100644
--- a/tests/client/config.test.ts
+++ b/tests/client/config.test.ts
@@ -94,6 +94,7 @@ describe("route config helper", () => {
pathname.mockReturnValue("/v1/fr");
setSolidBaseConfig({
title: "Docs",
+ description: "Documentation",
routes: {
path: "/{version}/{locale}",
version: {
@@ -119,6 +120,7 @@ describe("route config helper", () => {
{
version: "v1",
title: "Docs v1",
+ description: "Legacy documentation",
themeConfig: {
sidebar: { "/v1": [] },
},
@@ -132,6 +134,9 @@ describe("route config helper", () => {
const { SolidBaseRoutesContextProvider } = await import(
"../../src/client/routes.ts"
);
+ const { resolveDocumentMetadata } = await import(
+ "../../src/client/document-metadata.ts"
+ );
createRoot((dispose) => {
let config: ReturnType> | undefined;
@@ -145,11 +150,16 @@ describe("route config helper", () => {
expect(config?.()).toMatchObject({
title: "Docs v1",
+ description: "Legacy documentation",
themeConfig: {
nav: { title: "Localized" },
sidebar: { "/v1": [] },
},
});
+ expect(resolveDocumentMetadata(config!(), { title: "Install" })).toEqual({
+ description: "Legacy documentation",
+ title: "Install - Docs v1",
+ });
dispose();
});
});
diff --git a/tests/client/document-metadata.test.ts b/tests/client/document-metadata.test.ts
new file mode 100644
index 0000000..ced261b
--- /dev/null
+++ b/tests/client/document-metadata.test.ts
@@ -0,0 +1,97 @@
+import { describe, expect, it } from "vitest";
+
+import {
+ resolveCanonicalUrl,
+ resolveDocumentMetadata,
+ resolveHeadMetadata,
+} from "../../src/client/document-metadata.ts";
+
+describe("SolidBase document metadata", () => {
+ it("resolves a normal route from page and site metadata", () => {
+ expect(
+ resolveDocumentMetadata(
+ {
+ description: "SolidBase documentation",
+ siteUrl: "https://solidbase.dev",
+ title: "SolidBase",
+ },
+ { title: "Getting Started" },
+ ),
+ ).toEqual({
+ description: "SolidBase documentation",
+ title: "Getting Started - SolidBase",
+ });
+ expect(
+ resolveCanonicalUrl("https://solidbase.dev", {
+ pathname: "/guide/getting-started",
+ }),
+ ).toBe("https://solidbase.dev/guide/getting-started");
+ });
+
+ it("uses only pathname and normalizes URL slashes", () => {
+ const location = {
+ hash: "#installation",
+ pathname: "/fr/guide/getting-started/",
+ search: "?tab=api",
+ };
+
+ expect(resolveCanonicalUrl("https://solidbase.dev///", location)).toBe(
+ "https://solidbase.dev/fr/guide/getting-started/",
+ );
+ });
+
+ it("uses frontmatter title, template, and description overrides", () => {
+ expect(
+ resolveDocumentMetadata(
+ {
+ description: "Config description",
+ title: "SolidBase",
+ titleTemplate: ":title | Docs",
+ },
+ {
+ description: "Page description",
+ title: "Installation",
+ titleTemplate: ":title | Custom",
+ },
+ ),
+ ).toEqual({
+ description: "Page description",
+ title: "Installation | Custom",
+ });
+ });
+
+ it("returns no description when none resolves", () => {
+ expect(resolveDocumentMetadata({ title: "SolidBase" })).toEqual({
+ description: undefined,
+ title: "SolidBase - SolidBase",
+ });
+ });
+
+ it("returns no canonical URL without siteUrl", () => {
+ expect(
+ resolveCanonicalUrl(undefined, { pathname: "/reference" }),
+ ).toBeUndefined();
+ });
+
+ it("waits for page data before resolving the head metadata set", () => {
+ const config = {
+ description: "SolidBase documentation",
+ siteUrl: "https://solidbase.dev",
+ title: "SolidBase",
+ };
+ const location = { pathname: "/guide/getting-started" };
+
+ expect(resolveHeadMetadata(config, undefined, location)).toBeUndefined();
+ expect(
+ resolveHeadMetadata(
+ config,
+ { frontmatter: { title: "Getting Started" } },
+ location,
+ ),
+ ).toEqual({
+ canonicalUrl: "https://solidbase.dev/guide/getting-started",
+ description: "SolidBase documentation",
+ title: "Getting Started - SolidBase",
+ });
+ });
+});