alepha@docs:~/docs/guides/frontend$
cat 3-head.md
2 min read
Last commit:

#Head Management

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/router for SSR rendering of head tags.

#Installation

Head management is a separate module from router:

typescript
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:

typescript
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});

#Global Head with $head

Set default head values for your entire app:

tsx
 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.

#Full Options

typescript
 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});

#Page-Level Head

Override head values per page:

tsx
 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}

#Inheriting from Parent

The head function receives the previous head as second argument:

tsx
1head: (props, previous) => ({2  ...previous,  // Keep parent values3  title: `${props.product.name} | ${previous?.title}`,4}),

#Dynamic Head with useHead

Update head from any component:

tsx
 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};

#Reading Current Head

The hook returns the current head state:

tsx
 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};

#SEO Expander

The SeoExpander helper fills in missing values automatically:

typescript
 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// }

#How It Works

  1. Server: $head and page heads merge into final state
  2. Server: React renders, head tags injected into HTML <head>
  3. Client: useHead updates sync to document.title and meta tags
  4. Navigation: Head updates on route change, no flash

The head is part of the router state, so it survives hydration cleanly.

#Common Patterns

#Blog Post

tsx
 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}),

#E-commerce Product

tsx
 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}),

#User Profile

tsx
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}),

#Quick Reference

typescript
 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

On This Page
No headings found...
ready
mainTypeScript
UTF-8guides_frontend_head.md