SEO. Social sharing. The stuff that makes marketing people happy and developers groan.
The @alepha/react/head module lets you control the document <head> from anywhere in your component tree. Title, meta tags, Open Graph, Twitter cards - all type-safe and SSR-friendly.
Requires Router: This module depends on
@alepha/react/routerfor SSR rendering of head tags.
Head management is a separate module from router:
1import { AlephaReactHead } from "@alepha/react/head";2 3const alepha = Alepha.create()4 .with(AlephaReactRouter) // Required5 .with(AlephaReactHead); // Add head support
Or just use $head in your primitives - it auto-loads:
1import { $module } from "alepha";2import { $page } from "@alepha/react/router";3import { $head } from "@alepha/react/head";4 5export const AppModule = $module({6 name: "my-app",7 primitives: [$page, $head], // Both auto-load their modules8});
$headSet default head values for your entire app:
1import { $head } from "@alepha/react/head"; 2 3class App { 4 head = $head({ 5 title: "My SaaS", 6 titleSeparator: " | ", 7 description: "The best SaaS platform ever built", 8 og: { 9 image: "/og-image.png",10 type: "website"11 },12 twitter: {13 card: "summary_large_image",14 site: "@mysaas"15 }16 });17}
This becomes the baseline. Every page inherits these values unless overridden.
1$head({ 2 // Basic 3 title: "Page Title", 4 titleSeparator: " | ", // "Page | Site Name" 5 description: "Meta description for SEO", 6 keywords: ["saas", "typescript"], 7 author: "Your Name", 8 9 // Favicon10 favicon: "/favicon.ico",11 12 // Open Graph (Facebook, LinkedIn, etc.)13 og: {14 title: "OG Title", // Falls back to title15 description: "OG Desc", // Falls back to description16 image: "/og-image.png",17 type: "website", // website, article, product...18 siteName: "My Site",19 },20 21 // Twitter Cards22 twitter: {23 card: "summary_large_image", // summary, summary_large_image24 site: "@handle",25 creator: "@author",26 },27 28 // Canonical URL29 canonical: "https://example.com/page",30 31 // Robots32 robots: "index, follow",33 34 // Custom meta tags35 meta: [36 { name: "theme-color", content: "#000000" },37 { property: "custom:tag", content: "value" },38 ],39 40 // Custom link tags41 link: [42 { rel: "preconnect", href: "https://fonts.googleapis.com" },43 ],44});
Override head values per page:
1import { $page } from "@alepha/react/router"; 2 3class AppRouter { 4 blog = $page({ 5 path: "/blog/:slug", 6 loader: async ({ params }) => { 7 const post = await db.posts.findBySlug(params.slug); 8 return { post }; 9 },10 // Static head11 head: {12 title: "Blog",13 og: { type: "article" }14 },15 component: BlogPost,16 });17 18 // Dynamic head based on resolved data19 product = $page({20 path: "/products/:id",21 loader: async ({ params }) => {22 const product = await db.products.findById(params.id);23 return { product };24 },25 // Function form - receives resolved props26 head: (props) => ({27 title: props.product.name,28 description: props.product.description,29 og: {30 title: props.product.name,31 image: props.product.image,32 type: "product",33 },34 }),35 component: ProductPage,36 });37}
The head function receives the previous head as second argument:
1head: (props, previous) => ({2 ...previous, // Keep parent values3 title: `${props.product.name} | ${previous?.title}`,4}),
useHeadUpdate head from any component:
1import { useHead } from "@alepha/react/head"; 2 3const ProductPage = ({ product }) => { 4 const [head, setHead] = useHead({ 5 title: product.name, 6 description: product.description, 7 og: { 8 title: product.name, 9 image: product.image,10 }11 });12 13 // Update when product changes14 useEffect(() => {15 setHead({16 title: product.name,17 description: product.description,18 });19 }, [product]);20 21 return <div>{/* ... */}</div>;22};
The hook returns the current head state:
1const ProductMeta = () => { 2 const [head] = useHead(); 3 4 return ( 5 <div> 6 <p>Current title: {head.title}</p> 7 <p>Description: {head.description}</p> 8 </div> 9 );10};
The SeoExpander helper fills in missing values automatically:
1import { SeoExpander } from "@alepha/react/head"; 2 3const expander = new SeoExpander({ 4 baseUrl: "https://example.com", 5 siteName: "My Site", 6 defaultImage: "/default-og.png", 7}); 8 9// Input: minimal head10const input = {11 title: "Hello World",12 description: "A blog post",13};14 15// Output: fully expanded for SEO16const output = expander.expand(input);17// {18// title: "Hello World",19// description: "A blog post",20// og: {21// title: "Hello World",22// description: "A blog post",23// image: "https://example.com/default-og.png",24// siteName: "My Site",25// type: "website",26// },27// twitter: {28// card: "summary_large_image",29// title: "Hello World",30// description: "A blog post",31// image: "https://example.com/default-og.png",32// },33// canonical: "https://example.com/current-path",34// }
$head and page heads merge into final state<head>useHead updates sync to document.title and meta tagsThe head is part of the router state, so it survives hydration cleanly.
1head: (props) => ({ 2 title: props.post.title, 3 description: props.post.excerpt, 4 og: { 5 type: "article", 6 image: props.post.coverImage, 7 }, 8 meta: [ 9 { property: "article:published_time", content: props.post.publishedAt },10 { property: "article:author", content: props.post.author.name },11 ],12}),
1head: (props) => ({ 2 title: `${props.product.name} - Buy Now`, 3 description: props.product.shortDescription, 4 og: { 5 type: "product", 6 image: props.product.images[0], 7 }, 8 meta: [ 9 { property: "product:price:amount", content: props.product.price },10 { property: "product:price:currency", content: "USD" },11 ],12}),
1head: (props) => ({2 title: `${props.user.name} (@${props.user.username})`,3 description: props.user.bio,4 og: {5 type: "profile",6 image: props.user.avatar,7 },8}),
1// Define global head 2const head = $head({ 3 title: "My App", 4 description: "...", 5 og: { ... }, 6}); 7 8// Page-level head (static) 9$page({10 head: { title: "Page" },11});12 13// Page-level head (dynamic)14$page({15 head: (props) => ({ title: props.data.name }),16});17 18// Component-level head19const [head, setHead] = useHead({ title: "Dynamic" });20 21// Read current head22const [head] = useHead();
Previous: State Management | Next: Forms