Repository: ably/docs
Pull request #2621: [WEB-4409] Fix MDX head content & redirects
Author: login: kennethkalmer
Created: 2025-05-20T08:54:17Z | Merged: 2025-05-20T13:37:53Z

## Description

Small teething problems on the MDX rollout as we get more and more pages converted.

This PR addresses:

1. MDX pages didn't use the `Head` component
1. Added missing support for rendering `meta_keywords` in document head (all pages)
1. MDX pages didn't support `redirect_from` in frontmatter

### From Bot

This pull request introduces enhancements to the `createPages` functionality, adds metadata support for SEO, and improves the handling of redirects for MDX files. The key changes include querying and processing MDX redirects, adding `meta_keywords` support across components, and refining metadata handling for improved SEO.

### Enhancements to `createPages` functionality:
* Added a new GraphQL query to fetch MDX nodes with `redirect_from` metadata for handling redirects. (`data/createPages/index.ts`, [data/createPages/index.tsL173-R213](diffhunk://#diff-de097042a77b981c9479c00c99d98940e7f12e1e98e84c2f570d7385a5713cf3L173-R213))
* Introduced an `mdxRedirectCreator` function to process MDX redirects and create both browser and Nginx-compatible redirects. (`data/createPages/index.ts`, [data/createPages/index.tsR331-R361](diffhunk://#diff-de097042a77b981c9479c00c99d98940e7f12e1e98e84c2f570d7385a5713cf3R331-R361))
* Updated the `Promise.all` call to include the new MDX redirect handling logic. (`data/createPages/index.ts`, [data/createPages/index.tsR331-R361](diffhunk://#diff-de097042a77b981c9479c00c99d98940e7f12e1e98e84c2f570d7385a5713cf3R331-R361))

### Metadata support for SEO:
* Added `meta_keywords` support to the `Head` component and included it in the metadata rendering logic. (`src/components/Head.tsx`, [[1]](diffhunk://#diff-fe00a1c868f9f354c1cc34e3dd476e5f9dadb25cdd343acb521f04375ef6ac5bR9-R15) [[2]](diffhunk://#diff-fe00a1c868f9f354c1cc34e3dd476e5f9dadb25cdd343acb521f04375ef6ac5bR25)
* Updated the `Frontmatter` type and related components to include `meta_keywords` and `redirect_from`. (`src/components/Layout/Layout.tsx`, [[1]](diffhunk://#diff-8780de126d54dfa810671169d12e1f8fdf8267c3655f9eb20089aeeeb6ad5fcbR17-R27); `src/components/Layout/MDXWrapper.tsx`, [[2]](diffhunk://#diff-de4b35330c2e7d309d8818891c0a1a100852a1811fc8e4e8c511f6df4827f608L66-R95)
* Enhanced the `base-template` to handle `meta_keywords` for improved SEO metadata. (`src/templates/base-template.tsx`, [[1]](diffhunk://#diff-b4435c11dec135cba52887bdf0f49a32c8b0444b959b109511b86cfe54644dccR30) [[2]](diffhunk://#diff-b4435c11dec135cba52887bdf0f49a32c8b0444b959b109511b86cfe54644dccL50-R51)

### Refinements to MDX files:
* Updated MDX frontmatter to include structured `redirect_from` arrays for better readability and maintainability. (`src/pages/docs/asset-tracking/index.mdx`, [[1]](diffhunk://#diff-9cb490b92fda5c60aed43c41cc920daab71dbdff29644e7913b134660300c8d4L3-R6); `src/pages/docs/chat/index.mdx`, [[2]](diffhunk://#diff-0b954ef12d9d3e69dcff68e627c83ff14f237028168aad91c7da953acac426a2L4-R5); `src/pages/docs/livesync/index.mdx`, [[3]](diffhunk://#diff-d41e4685ed4d9ee4f4295585ad0842974e308bffca2cd30db291a0f917d9f81eL4-R5)

These changes collectively improve the site's SEO capabilities, streamline redirect handling, and enhance metadata management.

### Checklist

- [ ] Commits have been rebased.
- [ ] Linting has been run against the changed file(s).
- [ ] The PR adheres to the [writing style guide](../writing-style-guide.md) and [contribution guide](../CONTRIBUTING.md).


<!-- This is an auto-generated comment: release notes by coderabbit.ai -->

## Summary by CodeRabbit

- **New Features**
  - Added support for handling redirects defined in MDX files, enabling more flexible URL management and redirection.
  - Enhanced SEO metadata handling by supporting `meta_keywords` in page metadata and rendering them as `<meta name="keywords">` tags.
- **Refactor**
  - Improved consistency and structure of frontmatter metadata across documentation pages.
  - Updated components to use a more detailed and standardized frontmatter type.
- **Bug Fixes**
  - Ensured `redirect_from` metadata is consistently formatted as a list for better reliability.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->


Diff:
diff --git a/data/createPages/index.ts b/data/createPages/index.ts
index c960e02f77..08bac2ccba 100644
--- a/data/createPages/index.ts
+++ b/data/createPages/index.ts
@@ -19,6 +19,7 @@ const writeRedirect = writeRedirectToConfigFile('config/nginx-redirects.conf');
 const documentTemplate = path.resolve(`src/templates/document.tsx`);
 const apiReferenceTemplate = path.resolve(`src/templates/apiReference.tsx`);
 const examplesTemplate = path.resolve(`src/templates/examples.tsx`);
+
 interface Edge {
   node: {
     slug: string;
@@ -84,6 +85,22 @@ interface ExampleQueryResult {
   };
 }
 
+interface MdxNode {
+  parent: {
+    relativeDirectory: string;
+    name: string;
+  };
+  frontmatter?: {
+    redirect_from?: string[];
+  };
+}
+
+interface MdxRedirectsQueryResult {
+  allMdx: {
+    nodes: MdxNode[];
+  };
+}
+
 export const createPages: GatsbyNode['createPages'] = async ({ graphql, actions: { createPage, createRedirect } }) => {
   /**
    * It's not ideal to have:
@@ -118,6 +135,7 @@ export const createPages: GatsbyNode['createPages'] = async ({ graphql, actions:
       }
     }
   `);
+
   /**
    * We could log here, the reason we don't right now is that the error should already have been caught and logged.
    * Because Gatsby spawns a bunch of async processes during the onCreateNode step, though, and errors don't prevent
@@ -136,6 +154,7 @@ export const createPages: GatsbyNode['createPages'] = async ({ graphql, actions:
   if (documentResult.data?.allError.nodes && documentResult.data.allError.nodes.length > 0) {
     process.exit(1);
   }
+
   // API REFERENCES TEMPLATE
   const apiReferenceResult = await graphql<ApiReferenceQueryResult>(`
     query {
@@ -156,6 +175,10 @@ export const createPages: GatsbyNode['createPages'] = async ({ graphql, actions:
     }
   `);
 
+  // Query partials used in textile files
+  const retrievePartialFromGraphQL = maybeRetrievePartial(graphql);
+
+  // Query our examples
   const examplesResult = await graphql<ExampleQueryResult>(`
     query {
       allExampleFile {
@@ -170,7 +193,24 @@ export const createPages: GatsbyNode['createPages'] = async ({ graphql, actions:
     }
   `);
 
-  const retrievePartialFromGraphQL = maybeRetrievePartial(graphql);
+  // Query for MDX pages with redirects
+  const mdxRedirectsResult = await graphql<MdxRedirectsQueryResult>(`
+    query {
+      allMdx {
+        nodes {
+          parent {
+            ... on File {
+              relativeDirectory
+              name
+            }
+          }
+          frontmatter {
+            redirect_from
+          }
+        }
+      }
+    }
+  `);
 
   const documentCreator =
     (documentTemplate: string) =>
@@ -191,9 +231,9 @@ export const createPages: GatsbyNode['createPages'] = async ({ graphql, actions:
 
       contentMenuObject[DEFAULT_LANGUAGE] = contentMenu;
 
-      const script = safeFileExists(`static/scripts/${edge.node.slug}.js`);
-
-      const pagePath = `/docs/${edge.node.slug}`;
+      const slug = edge.node.slug;
+      const script = safeFileExists(`static/scripts/${slug}.js`);
+      const pagePath = `/docs/${slug}`;
 
       const redirectFromList = edge.node.meta?.redirect_from;
 
@@ -217,7 +257,6 @@ export const createPages: GatsbyNode['createPages'] = async ({ graphql, actions:
         });
       }
 
-      const slug = edge.node.slug;
       createPage({
         path: pagePath,
         component: documentTemplate,
@@ -250,6 +289,10 @@ export const createPages: GatsbyNode['createPages'] = async ({ graphql, actions:
     throw new Error('API reference result is undefined');
   }
 
+  if (!mdxRedirectsResult.data) {
+    throw new Error('MDX redirects result is undefined');
+  }
+
   const exampleCreator = async (exampleDatum: Example) => {
     if (!examplesResult.data) {
       throw new Error('Examples result is undefined');
@@ -285,9 +328,36 @@ export const createPages: GatsbyNode['createPages'] = async ({ graphql, actions:
     });
   };
 
+  // Handle redirects for MDX files
+  const mdxRedirectCreator = async (node: MdxNode) => {
+    if (node.frontmatter?.redirect_from) {
+      node.frontmatter.redirect_from.forEach((redirectFrom: string) => {
+        const redirectFromUrl = new URL(redirectFrom, siteMetadata.siteUrl);
+        // Here we don't have a slug, so we need to construct the path manually
+        // from the parent node. Mdx nodes are pretty bare
+        const toPath = `/${node.parent.relativeDirectory}${node.parent.name === 'index' ? '' : `/${node.parent.name}`}`;
+
+        if (!redirectFromUrl.hash) {
+          // We need to be prefix aware just like Gatsby's internals so it works
+          // with nginx redirects
+          writeRedirect(redirectFrom, toPath);
+        }
+
+        createRedirect({
+          fromPath: redirectFrom,
+          toPath: toPath,
+          isPermanent: true,
+          force: true,
+          redirectInBrowser: true,
+        });
+      });
+    }
+  };
+
   await Promise.all([
     ...documentResult.data.allFileHtml.edges.map(documentCreator(documentTemplate)),
     ...apiReferenceResult.data.allFileHtml.edges.map(documentCreator(apiReferenceTemplate)),
     ...examples.map(exampleCreator),
+    ...mdxRedirectsResult.data.allMdx.nodes.map(mdxRedirectCreator),
   ]);
 };
diff --git a/data/onCreateNode/create-graphql-schema-customization.ts b/data/onCreateNode/create-graphql-schema-customization.ts
index a583b3c3ab..70b3daca2c 100644
--- a/data/onCreateNode/create-graphql-schema-customization.ts
+++ b/data/onCreateNode/create-graphql-schema-customization.ts
@@ -10,6 +10,7 @@ export const createSchemaCustomization: GatsbyNode['createSchemaCustomization']
     type FileHtmlMetaData {
       languages: [String]
       meta_description: String
+      meta_keywords: String
       title: String
       redirect_from: [String]
       product: String
@@ -18,6 +19,12 @@ export const createSchemaCustomization: GatsbyNode['createSchemaCustomization']
       contentOrderedList: [FileHtmlContentOrderedListItem]
       meta: FileHtmlMetaData
     }
+    type MdxFrontmatter implements Node {
+      title: String
+      meta_description: String
+      meta_keywords: String
+      redirect_from: [String]
+    }
     type HowToHtml implements Node {
       slug: String
       tutorial: Node
diff --git a/src/components/Head.tsx b/src/components/Head.tsx
index 38125f2232..146a5c63a1 100644
--- a/src/components/Head.tsx
+++ b/src/components/Head.tsx
@@ -6,11 +6,13 @@ export const Head = ({
   canonical,
   description,
   metaTitle,
+  keywords,
 }: {
   title: string;
   canonical: string;
   description: string;
   metaTitle?: string;
+  keywords?: string;
 }) => (
   <Helmet>
     <title>{metaTitle || title}</title>
@@ -20,5 +22,6 @@ export const Head = ({
     <meta name="description" content={description} />
     <meta property="og:description" content={description} />
     <meta name="twitter:description" content={description} />
+    {keywords && <meta name="keywords" content={keywords} />}
   </Helmet>
 );
diff --git a/src/components/Layout/Layout.tsx b/src/components/Layout/Layout.tsx
index d853c1b131..9ff46a96ee 100644
--- a/src/components/Layout/Layout.tsx
+++ b/src/components/Layout/Layout.tsx
@@ -14,12 +14,17 @@ import Header from './Header';
 import LeftSidebar from './LeftSidebar';
 import RightSidebar from './RightSidebar';
 
+export type Frontmatter = {
+  title: string;
+  meta_description: string;
+  meta_keywords?: string;
+  redirect_from?: string[];
+};
+
 export type PageContextType = {
   layout: LayoutOptions;
   languages?: string[];
-  frontmatter?: {
-    title: string;
-  };
+  frontmatter: Frontmatter;
 };
 
 type LayoutProps = PageProps<unknown, PageContextType>;
diff --git a/src/components/Layout/MDXWrapper.tsx b/src/components/Layout/MDXWrapper.tsx
index 9da4d740c2..6ceb143674 100644
--- a/src/components/Layout/MDXWrapper.tsx
+++ b/src/components/Layout/MDXWrapper.tsx
@@ -4,7 +4,7 @@ import CodeSnippet, { CodeSnippetProps, SDKType } from '@ably/ui/core/CodeSnippe
 import cn from '@ably/ui/core/utils/cn';
 
 import PageTitle from '../PageTitle';
-import { PageContextType } from './Layout';
+import { Frontmatter, PageContextType } from './Layout';
 import { MarkdownProvider } from '../Markdown';
 import Article from '../Article';
 import If from './mdx/If';
@@ -15,6 +15,10 @@ import { HtmlComponentPropsData } from '../html-component-props';
 import { languageData } from 'src/data/languages';
 import { ActivePage } from './utils/nav';
 import { Table, TableHead, TableBody, TableRow, TableHeader, TableCell } from './mdx/tables';
+import { Head } from '../Head';
+import { useSiteMetadata } from 'src/hooks/use-site-metadata';
+import { ProductName } from 'src/templates/template-data';
+import { getMetaTitle } from '../common/meta-title';
 
 type MDXWrapperProps = PageProps<unknown, PageContextType>;
 
@@ -63,17 +67,32 @@ const WrappedAside = (props: PropsWithChildren<{ 'data-type': string }>) => {
   );
 };
 
-const MDXWrapper: React.FC<MDXWrapperProps> = ({ children, pageContext }) => {
+const META_DESCRIPTION_FALLBACK = `Ably provides a suite of APIs to build, extend, and deliver powerful digital experiences in realtime. Organizations like Toyota, Bloomberg, HubSpot, and Hopin depend on Ably’s platform to offload the growing complexity of business-critical realtime data synchronization at global scale.`;
+const META_PRODUCT_FALLBACK = 'pub_sub';
+
+const getFrontmatter = (frontmatter: Frontmatter, prop: keyof Frontmatter, alternative: string | string[] = '') =>
+  frontmatter?.[prop] ? frontmatter[prop] : alternative;
+
+const MDXWrapper: React.FC<MDXWrapperProps> = ({ children, pageContext, path }) => {
   const { frontmatter } = pageContext;
-  const title = frontmatter?.title;
+
   const { activePage } = useLayoutContext();
   const [sdk, setSdk] = useState<SDKType>(null);
 
+  const title = getFrontmatter(frontmatter, 'title') as string;
+  const description = getFrontmatter(frontmatter, 'meta_description', META_DESCRIPTION_FALLBACK) as string;
+  const keywords = getFrontmatter(frontmatter, 'meta_keywords') as string;
+  const metaTitle = getMetaTitle(title, (activePage.product as ProductName) || META_PRODUCT_FALLBACK) as string;
+
+  const { canonicalUrl } = useSiteMetadata();
+  const canonical = canonicalUrl(path);
+
   // Use the copyable headers hook
   useCopyableHeaders();
 
   return (
     <SDKContext.Provider value={{ sdk, setSdk }}>
+      <Head title={title} metaTitle={metaTitle} canonical={canonical} description={description} keywords={keywords} />
       <Article>
         <MarkdownProvider
           components={{
@@ -88,7 +107,7 @@ const MDXWrapper: React.FC<MDXWrapperProps> = ({ children, pageContext }) => {
             td: TableCell,
           }}
         >
-          {title && <PageTitle>{title}</PageTitle>}
+          <PageTitle>{title}</PageTitle>
           {children}
         </MarkdownProvider>
       </Article>
diff --git a/src/components/common/meta-title.ts b/src/components/common/meta-title.ts
index 609317da7c..f67e0ca17e 100644
--- a/src/components/common/meta-title.ts
+++ b/src/components/common/meta-title.ts
@@ -5,11 +5,15 @@ export const getMetaTitle = (title: string, product: ProductName): string => {
     channels: 'Channels',
     spaces: 'Ably Spaces',
     livesync: 'Ably LiveSync',
+    liveSync: 'Ably LiveSync',
     chat: 'Ably Chat',
     liveobjects: 'Ably LiveObjects',
+    liveObjects: 'Ably LiveObjects',
     'asset-tracking': 'Ably Asset Tracking',
+    assetTracking: 'Ably Asset Tracking',
     'api-reference': 'API References',
     pub_sub: 'Ably Pub/Sub',
+    pubsub: 'Ably Pub/Sub',
     home: 'Home',
   }[product] || 'Ably Realtime') as ProductTitle;
 
diff --git a/src/pages/docs/asset-tracking/example-apps.mdx b/src/pages/docs/asset-tracking/example-apps.mdx
index 1a5cf73be8..049deb2eff 100644
--- a/src/pages/docs/asset-tracking/example-apps.mdx
+++ b/src/pages/docs/asset-tracking/example-apps.mdx
@@ -1,6 +1,5 @@
 ---
 title: Using the example apps
-product: asset-tracking
 meta_description: "Ably Asset Tracking SDKs provide an easy way to track multiple assets with realtime location updates powered by the Ably realtime network and Mapbox Navigation SDK with location enhancement."
 meta_keywords: "Ably, Asset Tracking SDK, Mapbox, Mapbox Navigation SDK, Ably realtime, map matching, location tracking, Publishing SDK, Subscribing SDK"
 ---
diff --git a/src/pages/docs/asset-tracking/index.mdx b/src/pages/docs/asset-tracking/index.mdx
index d81962f30d..23144138b5 100644
--- a/src/pages/docs/asset-tracking/index.mdx
+++ b/src/pages/docs/asset-tracking/index.mdx
@@ -1,9 +1,9 @@
 ---
 title: About Asset Tracking
-product: asset-tracking
 meta_description: "The Ably Asset Tracking solution provides a way to track multiple assets in realtime."
 meta_keywords: "Ably, Asset Tracking SDK, Mapbox, Mapbox Navigation SDK, Mapbox location enhancement, Ably realtime, map matching, location tracking, Publishing SDK, Subscribing SDK"
-redirect_from: /docs/products/asset-tracking
+redirect_from:
+ - /docs/products/asset-tracking
 ---
 
 The Ably Asset Tracking solution provides a way to track the location of multiple assets in realtime. An asset is any object that moves geographically, and whose position needs to be monitored. The Ably Asset Tracking solution is powered by the Ably realtime network, the Ably Asset Tracking SDKs, and the Mapbox Navigation SDK with location enhancement.
diff --git a/src/pages/docs/asset-tracking/using-the-sdks.mdx b/src/pages/docs/asset-tracking/using-the-sdks.mdx
index 7432202b4e..2df43508ee 100644
--- a/src/pages/docs/asset-tracking/using-the-sdks.mdx
+++ b/src/pages/docs/asset-tracking/using-the-sdks.mdx
@@ -1,6 +1,5 @@
 ---
 title: Using the SDKs
-product: asset-tracking
 meta_description: "Ably Asset Tracking SDKs provide an easy way to track multiple assets with realtime location updates powered by the Ably realtime network and Mapbox Navigation SDK with location enhancement."
 meta_keywords: "Ably, Asset Tracking SDK, Mapbox, Mapbox Navigation SDK, Ably realtime, map matching, location tracking, Publishing SDK, Subscribing SDK"
 languages:
diff --git a/src/pages/docs/chat/index.mdx b/src/pages/docs/chat/index.mdx
index 3b8d629f6a..dfa9d13a0e 100644
--- a/src/pages/docs/chat/index.mdx
+++ b/src/pages/docs/chat/index.mdx
@@ -1,7 +1,8 @@
 ---
 title: About Chat
 meta_description: "Learn more about Ably Chat and the features that enable you to quickly build functionality into new and existing applications."
-redirect_from: /docs/products/chat
+redirect_from:
+  - /docs/products/chat
 ---
 
 Ably Chat is a product dedicated to making it quick and easy to build chat functionality into new and existing applications. Use Ably Chat to build things such as a 1:1 customer support feature, or add a chat component to a livestreaming platform that serves hundreds of thousands of users.
diff --git a/src/pages/docs/livesync/index.mdx b/src/pages/docs/livesync/index.mdx
index c2e0f00b38..8b145df32f 100644
--- a/src/pages/docs/livesync/index.mdx
+++ b/src/pages/docs/livesync/index.mdx
@@ -1,7 +1,8 @@
 ---
 title: About LiveSync
 meta_description: "LiveSync enables you to synchronize changes in your database to application clients at scale."
-redirect_from: /docs/products/livesync
+redirect_from:
+  - /docs/products/livesync
 ---
 
 LiveSync is a powerful realtime data synchronization product designed to facilitate broadcasting realtime updates from backend databases to application clients at scale. LiveSync ensures that data updates are propagated reliably, and in order, to all connected clients in realtime.
diff --git a/src/templates/base-template.tsx b/src/templates/base-template.tsx
index 5aab1fb09a..ce61cba6d6 100644
--- a/src/templates/base-template.tsx
+++ b/src/templates/base-template.tsx
@@ -27,6 +27,7 @@ const Template = ({
 }: AblyTemplateData) => {
   const title = getMetaDataDetails(ablyDocument, 'title') as string;
   const description = getMetaDataDetails(ablyDocument, 'meta_description', META_DESCRIPTION_FALLBACK) as string;
+  const keywords = getMetaDataDetails(ablyDocument, 'meta_keywords') as string;
   const { canonicalUrl } = useSiteMetadata();
   const canonical = canonicalUrl(`/docs/${slug}`);
 
@@ -47,7 +48,7 @@ const Template = ({
   return (
     <>
       <PathnameContext.Provider value={pathname}>
-        <Head title={title} metaTitle={metaTitle} canonical={canonical} description={description} />
+        <Head title={title} metaTitle={metaTitle} canonical={canonical} description={description} keywords={keywords} />
         <Article>
           <PageTitle>{title}</PageTitle>
           <div>{elements}</div>
diff --git a/src/templates/template-data.d.ts b/src/templates/template-data.d.ts
index 769839f439..7d6303bcf7 100644
--- a/src/templates/template-data.d.ts
+++ b/src/templates/template-data.d.ts
@@ -4,6 +4,7 @@ import { MenuData } from 'src/components/Sidebar/RightSidebar/menu-data';
 export type AblyDocumentMeta = {
   title: string;
   meta_description: string;
+  meta_keywords: string;
   languages: string[];
   redirect_from: string[];
   product?: string;


Reviews:
user: login: coderabbitai[bot]
body: **Actionable comments posted: 2**

<details>
<summary>🧹 Nitpick comments (5)</summary><blockquote>

<details>
<summary>src/components/Layout/MDXWrapper.tsx (3)</summary><blockquote>

`83-84`: **Add null check for title when computing metaTitle**

If the title is empty, `getMetaTitle` might produce unexpected results. Consider adding a null check or ensuring a default value.

```diff
-const metaTitle = getMetaTitle(title, currentProduct) as string;
+const metaTitle = title ? getMetaTitle(title, currentProduct) as string : '';
```

---

`111-111`: **Consider conditional rendering for PageTitle**

The PageTitle is now always rendered regardless of whether there's a title. This could lead to empty title sections if the title is missing.

```diff
-<PageTitle>{title}</PageTitle>
+{title && <PageTitle>{title}</PageTitle>}
```

---

`70-72`: **Add comments for fallback constants**

The fallback constants would benefit from comments explaining their purpose and when they're used.

```diff
+// Default description used when meta_description is not provided in frontmatter
 const META_DESCRIPTION_FALLBACK = `Ably provides a suite of APIs to build, extend, and deliver powerful digital experiences in realtime. Organizations like Toyota, Bloomberg, HubSpot, and Hopin depend on Ably's platform to offload the growing complexity of business-critical realtime data synchronization at global scale.`;
+// Default product identifier used when product is not specified in frontmatter
 const META_PRODUCT_FALLBACK = 'pub_sub';
```

</blockquote></details>
<details>
<summary>data/createPages/index.ts (2)</summary><blockquote>

`331-355`: **Consider refactoring duplicate redirect logic**

The `mdxRedirectCreator` function contains logic similar to the redirect handling in the `documentCreator` function (lines 240-258). Consider extracting this common logic into a shared utility function.

```diff
+// Extract shared redirect logic to a utility function
+const createRedirectsForPath = (redirectFromList: string[], toPath: string) => {
+  redirectFromList.forEach((redirectFrom) => {
+    const redirectFromUrl = new URL(redirectFrom, siteMetadata.siteUrl);
+
+    if (!redirectFromUrl.hash) {
+      // We need to be prefix aware just like Gatsby's internals so it works
+      // with nginx redirects
+      writeRedirect(redirectFrom, toPath);
+    }
+
+    createRedirect({
+      fromPath: redirectFrom,
+      toPath: toPath,
+      isPermanent: true,
+      force: true,
+      redirectInBrowser: true,
+    });
+  });
+};

 // Handle redirects for MDX files
 const mdxRedirectCreator = async (node: MdxNode) => {
   if (node.frontmatter?.redirect_from) {
-    node.frontmatter.redirect_from.forEach((redirectFrom: string) => {
-      const redirectFromUrl = new URL(redirectFrom, siteMetadata.siteUrl);
-      // Here we don't have a slug, so we need to construct the path manually
-      // from the parent node. Mdx nodes are pretty bare
-      const toPath = `/${node.parent.relativeDirectory}${node.parent.name === 'index' ? '' : `/${node.parent.name}`}`;
-
-      if (!redirectFromUrl.hash) {
-        // We need to be prefix aware just like Gatsby's internals so it works
-        // with nginx redirects
-        writeRedirect(redirectFrom, toPath);
-      }
-
-      createRedirect({
-        fromPath: redirectFrom,
-        toPath: toPath,
-        isPermanent: true,
-        force: true,
-        redirectInBrowser: true,
-      });
-    });
+    // Here we don't have a slug, so we need to construct the path manually
+    // from the parent node. Mdx nodes are pretty bare
+    const toPath = `/${node.parent.relativeDirectory}${node.parent.name === 'index' ? '' : `/${node.parent.name}`}`;
+    createRedirectsForPath(node.frontmatter.redirect_from, toPath);
   }
 };
```

Then update the document creator function too:
```diff
 if (redirectFromList) {
-  redirectFromList.forEach((redirectFrom) => {
-    const redirectFromUrl = new URL(redirectFrom, siteMetadata.siteUrl);
-
-    if (!redirectFromUrl.hash) {
-      // We need to be prefix aware just like Gatsby's internals so it works
-      // with nginx redirects
-      writeRedirect(redirectFrom, pagePath);
-    }
-
-    createRedirect({
-      fromPath: redirectFrom,
-      toPath: pagePath,
-      isPermanent: true,
-      force: true,
-      redirectInBrowser: true,
-    });
-  });
+  createRedirectsForPath(redirectFromList, pagePath);
 }
```

---

`292-294`: **Improve error message for MDX redirects**

The error message for MDX redirects could be more specific to help with debugging.

```diff
-if (!mdxRedirectsResult.data) {
-  throw new Error('MDX redirects result is undefined');
+if (!mdxRedirectsResult.data) {
+  throw new Error('GraphQL query for MDX redirects returned undefined data. Check the query or schema.');
```

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**


<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 5a20ac4f2effbf9abda3ce5169faac633293fbcc and a4bccd8915a1f1893b7672aad473412df8a38ed9.

</details>

<details>
<summary>📒 Files selected for processing (10)</summary>

* `data/createPages/index.ts` (9 hunks)
* `data/onCreateNode/create-graphql-schema-customization.ts` (2 hunks)
* `src/components/Head.tsx` (2 hunks)
* `src/components/Layout/Layout.tsx` (1 hunks)
* `src/components/Layout/MDXWrapper.tsx` (5 hunks)
* `src/pages/docs/asset-tracking/index.mdx` (1 hunks)
* `src/pages/docs/chat/index.mdx` (1 hunks)
* `src/pages/docs/livesync/index.mdx` (1 hunks)
* `src/templates/base-template.tsx` (2 hunks)
* `src/templates/template-data.d.ts` (1 hunks)

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🧬 Code Graph Analysis (3)</summary>

<details>
<summary>src/templates/base-template.tsx (1)</summary><blockquote>

<details>
<summary>src/components/Head.tsx (1)</summary>

* `Head` (4-27)

</details>

</blockquote></details>
<details>
<summary>src/components/Layout/Layout.tsx (1)</summary><blockquote>

<details>
<summary>data/onCreatePage.ts (1)</summary>

* `LayoutOptions` (5-10)

</details>

</blockquote></details>
<details>
<summary>data/createPages/index.ts (2)</summary><blockquote>

<details>
<summary>gatsby-config.ts (1)</summary>

* `siteMetadata` (20-42)

</details>
<details>
<summary>src/data/examples/index.ts (1)</summary>

* `examples` (5-221)

</details>

</blockquote></details>

</details>

</details>

<details>
<summary>🔇 Additional comments (18)</summary><blockquote>

<details>
<summary>data/onCreateNode/create-graphql-schema-customization.ts (2)</summary>

`13-13`: **Added necessary `meta_keywords` field to support SEO enhancements**

This addition to the `FileHtmlMetaData` type aligns well with the PR objective of enhancing SEO by supporting keywords metadata in document heads.

---

`22-28`: **Well-structured `MdxFrontmatter` type implementation**

Adding this new type is essential for proper typing of MDX frontmatter fields and supports the PR's goal of enabling MDX pages to work correctly with metadata and redirects. The implementation correctly includes all necessary fields including title, descriptions, keywords, redirects, and product information.

</details>
<details>
<summary>src/pages/docs/livesync/index.mdx (2)</summary>

`3-3`: **Added product identifier for better classification**

Adding the `product: livesync` field helps with proper product categorization which can improve navigation and display logic in the UI.

---

`5-6`: **Converted redirect_from to array format**

This change from a single string to an array format aligns with the schema definition and enables support for multiple redirect paths in the future, improving the flexibility of the redirect system.

</details>
<details>
<summary>src/templates/template-data.d.ts (1)</summary>

`7-7`: **Added meta_keywords to type definition for consistency**

This addition to the `AblyDocumentMeta` type ensures type safety and consistency with the GraphQL schema updates, supporting the PR's goal of enhancing SEO capabilities through keywords metadata.

</details>
<details>
<summary>src/pages/docs/chat/index.mdx (2)</summary>

`3-3`: **Added product identifier for content categorization**

Adding the `product: chat` field helps with proper categorization of content and supports potential filtering or specialized handling based on product type.

---

`5-6`: **Updated redirect_from to array format**

Converting the redirect path from a single string to an array format brings this file in line with the updated schema and provides flexibility for multiple redirect paths in the future.

</details>
<details>
<summary>src/components/Head.tsx (1)</summary>

`9-9`: **Well implemented SEO enhancement!**

The addition of the `keywords` prop to the `Head` component provides proper support for meta keywords, enhancing SEO capabilities across the site. The implementation follows best practices by making it optional and only rendering the meta tag when keywords are provided.



Also applies to: 15-15, 25-25

</details>
<details>
<summary>src/pages/docs/asset-tracking/index.mdx (1)</summary>

`6-7`: **LGTM - Good refactoring of redirects format**

Converting the `redirect_from` field from a single string to a YAML list format aligns with the PR's objective of enabling MDX pages to support multiple redirect entries. This change provides better flexibility for future redirect management.

</details>
<details>
<summary>src/templates/base-template.tsx (2)</summary>

`30-30`: **SEO metadata extraction looks good**

The extraction of keywords from the document metadata follows the same pattern used for other metadata fields, maintaining consistency in the codebase.

---

`51-51`: **LGTM - Complete implementation of keywords in Head component**

Properly passing the extracted keywords to the Head component completes the implementation of the SEO enhancement. This change ensures metadata from MDX files is correctly passed through to the rendered HTML.

</details>
<details>
<summary>src/components/Layout/Layout.tsx (2)</summary>

`17-23`: **Good type definition for frontmatter**

Creating a structured `Frontmatter` type with required and optional fields provides better type safety and makes the frontmatter structure explicit across the codebase. This is a foundational improvement that supports both the SEO enhancements and redirect functionality.

---

`28-28`: **LGTM - Proper type integration**

Updating `PageContextType` to use the new `Frontmatter` type ensures type safety throughout the component hierarchy.

</details>
<details>
<summary>src/components/Layout/MDXWrapper.tsx (2)</summary>

`96-96`: **Add appropriate spacing between Head and Article components**

For better code readability, consider adding a line break between the Head component and the Article component.

The integration of the Head component for SEO metadata is well-implemented, ensuring proper document head management for MDX pages as specified in the PR objectives.

---

`86-87`: <details>
<summary>❓ Verification inconclusive</summary>

**Add null check for path parameter**

The `path` parameter is used without checking if it exists, which could cause issues if it's undefined.

```diff
-const canonical = canonicalUrl(path);
+const canonical = path ? canonicalUrl(path) : '';
```

Let's verify if the path can be undefined:

---

🏁 Script executed:

```shell
#!/bin/bash
# Check how MDXWrapper is used throughout the codebase to see if path is always provided
rg -A 2 -B 2 "MDXWrapper" --type=tsx --type=jsx
```

Length of output: 75

---

I’d like to inspect how `path` is defined and passed into `MDXWrapper`. Let’s locate and review the props/type declarations and the lines around `canonicalUrl(path)`:

---

🏁 Script executed:

```shell
#!/bin/bash
# 1. Locate the MDXWrapper file
fd MDXWrapper.tsx

# 2. Show its props/type definitions
rg -n "interface MDXWrapperProps" -n src/components/Layout/MDXWrapper.tsx || echo "no explicit interface"

# 3. Show the component signature
rg -n "function MDXWrapper" -n src/components/Layout/MDXWrapper.tsx

# 4. Find where `canonicalUrl` and `path` are used
rg -n "canonicalUrl" -n src/components/Layout/MDXWrapper.tsx

# 5. Print the surrounding lines for context
sed -n '70,110p' src/components/Layout/MDXWrapper.tsx
```

Length of output: 2360

---

Let’s verify how `MDXWrapperProps` (and specifically `path`) is defined at the top of the file:

---

🏁 Script executed:

```shell
#!/bin/bash
# 1. Show the top of MDXWrapper.tsx to find prop definitions/imports
sed -n '1,80p' src/components/Layout/MDXWrapper.tsx

# 2. Search for MDXWrapperProps usage across the repo
rg -n "MDXWrapperProps" -n .
```

Length of output: 3468

---



</details>

**Verify `path` presence in Gatsby’s PageProps**  
We see `MDXWrapper` pulls `path` from `PageProps<unknown, PageContextType>` (imported from Gatsby). You’ll need to confirm in your Gatsby version’s type definitions whether `path` is always a non-nullable string or can be `undefined`.  
- If `path: string` is guaranteed, no null check is needed—keep it as `const canonical = canonicalUrl(path);`.  
- If `path` can be optional, guard it (e.g.):  
  ```ts
  const canonical = path ? canonicalUrl(path) : '';
  ```  
Please verify the `path` declaration in the Gatsby types and adjust accordingly.

</details>
<details>
<summary>data/createPages/index.ts (3)</summary>

`88-102`: **Well-defined TypeScript interfaces for MDX nodes**

The addition of TypeScript interfaces for MDX nodes and query results is well-implemented, providing clear type definitions for the data structure.

---

`196-213`: **Comprehensive GraphQL query for MDX redirects**

The GraphQL query is well-structured to fetch all necessary information for MDX redirects. It correctly retrieves the parent file metadata and frontmatter redirect information.

---

`361-361`: **Efficient parallel processing with Promise.all**

The addition of MDX redirect processing to the existing Promise.all call is a good approach for maintaining performance during the build process.

This implementation successfully enables MDX pages to support `redirect_from` entries in their frontmatter as stated in the PR objectives.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: NONE
submitted_at: 2025-05-20T08:59:03Z
user: login: m-hulbert
body: LGTM!

* Tested redirects on MDX and textile and all worked. 
* Can see all meta properties for MDX and textile.
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-20T09:43:35Z
user: login: jamiehenson
body: Looks good, but we shouldn't need to add `product` to the frontmatter - we have that information in the `LayoutContext` at any given time, accessible via `activePage`.
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-20T10:59:02Z
user: login: jamiehenson
body: Nice. My main theory for why asset tracking and live sync cases don't work but chat does is because of a difference in casing - the outgoing `product` frontmatter entries are kebab case whereas the logic behind the `activePage` stuff is camel case. Perhaps worth wrapping `activePage.product` inclusion in MDXWrapper in something that formats it into a kebab
state: APPROVED
author_association: MEMBER
submitted_at: 2025-05-20T11:43:39Z

Review comments:
diff_hunk: @@ -63,9 +67,24 @@ const WrappedAside = (props: PropsWithChildren<{ 'data-type': string }>) => {
   );
 };
 
-const MDXWrapper: React.FC<MDXWrapperProps> = ({ children, pageContext }) => {
+const META_DESCRIPTION_FALLBACK = `Ably provides a suite of APIs to build, extend, and deliver powerful digital experiences in realtime. Organizations like Toyota, Bloomberg, HubSpot, and Hopin depend on Ably’s platform to offload the growing complexity of business-critical realtime data synchronization at global scale.`;
+const META_PRODUCT_FALLBACK = 'pub_sub';
+
+const getFrontmatter = (frontmatter: Frontmatter, prop: keyof Frontmatter, alternative: string | string[] = '') =>
+  frontmatter?.[prop] ? frontmatter[prop] : alternative;
path: src/components/Layout/MDXWrapper.tsx
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**Improve type safety in `getFrontmatter` utility function**

The utility function doesn't restrict the `alternative` parameter type to match the expected property type, which could lead to type mismatches at runtime.

```diff
-const getFrontmatter = (frontmatter: Frontmatter, prop: keyof Frontmatter, alternative: string | string[] = '') =>
+const getFrontmatter = <T extends string | string[]>(frontmatter: Frontmatter, prop: keyof Frontmatter, alternative: T = '' as T) =>
  frontmatter?.[prop] ? frontmatter[prop] : alternative;
```


> Committable suggestion skipped: line range outside the PR's diff.

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
In src/components/Layout/MDXWrapper.tsx around lines 73 to 74, the
getFrontmatter function's alternative parameter is typed as string or string[],
which does not ensure type safety relative to the property type being accessed.
Update the function signature to make the alternative parameter's type
correspond to the type of the specified Frontmatter property, using generics or
keyof type mapping, so that alternative matches the expected property type and
prevents type mismatches.
```

</details>

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-20T08:59:02Z
author_association: NONE
diff_hunk: @@ -285,9 +328,36 @@ export const createPages: GatsbyNode['createPages'] = async ({ graphql, actions:
     });
   };
 
+  // Handle redirects for MDX files
+  const mdxRedirectCreator = async (node: MdxNode) => {
+    if (node.frontmatter?.redirect_from) {
+      node.frontmatter.redirect_from.forEach((redirectFrom: string) => {
+        const redirectFromUrl = new URL(redirectFrom, siteMetadata.siteUrl);
+        // Here we don't have a slug, so we need to construct the path manually
+        // from the parent node. Mdx nodes are pretty bare
+        const toPath = `/${node.parent.relativeDirectory}${node.parent.name === 'index' ? '' : `/${node.parent.name}`}`;
path: data/createPages/index.ts
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**Ensure path normalization for MDX redirects**

The constructed path might have issues with double slashes or missing slashes when combining directory and name. Consider using `path.join` and ensuring the path starts with a forward slash.

```diff
-const toPath = `/${node.parent.relativeDirectory}${node.parent.name === 'index' ? '' : `/${node.parent.name}`}`;
+const pathParts = [node.parent.relativeDirectory];
+if (node.parent.name !== 'index') {
+  pathParts.push(node.parent.name);
+}
+const toPath = '/' + pathParts.filter(Boolean).join('/');
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

```suggestion
        const pathParts = [node.parent.relativeDirectory];
        if (node.parent.name !== 'index') {
          pathParts.push(node.parent.name);
        }
        const toPath = '/' + pathParts.filter(Boolean).join('/');
```

</details>

<!-- suggestion_end -->

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
In data/createPages/index.ts at line 338, the path construction for toPath may
produce incorrect paths with double or missing slashes. Replace the string
template concatenation with path.join to combine node.parent.relativeDirectory
and node.parent.name properly, then ensure the resulting path starts with a
forward slash. This will normalize the path and prevent issues with slashes.
```

</details>

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-20T08:59:02Z
author_association: NONE

PR comments:
user: login: coderabbitai[bot]
created_at: 2025-05-20T08:54:24Z
body: <!-- This is an auto-generated comment: summarize by coderabbit.ai -->
<!-- This is an auto-generated comment: skip review by coderabbit.ai -->

> [!IMPORTANT]
> ## Review skipped
> 
> Auto reviews are disabled on this repository.
> 
> Please check the settings in the CodeRabbit UI or the `.coderabbit.yaml` file in this repository. To trigger a single review, invoke the `@coderabbitai review` command.
> 
> You can disable this status message by setting the `reviews.review_status` to `false` in the CodeRabbit configuration file.

<!-- end of auto-generated comment: skip review by coderabbit.ai -->
<!-- walkthrough_start -->

## Walkthrough

The changes introduce structured support for SEO metadata and redirects in the documentation site. Frontmatter in MDX files is enhanced to include fields like `meta_keywords`, `redirect_from`, and `product`. Page creation logic now processes MDX-defined redirects, and components are updated to handle and render new metadata fields, ensuring canonical URLs, meta descriptions, and keywords are present.

## Changes

| File(s)                                                                                         | Change Summary |
|-------------------------------------------------------------------------------------------------|---------------|
| `data/createPages/index.ts`                                                                     | Adds MDX redirect handling, new GraphQL types, and integrates redirects into Gatsby page creation. |
| `data/onCreateNode/create-graphql-schema-customization.ts`                                      | Extends GraphQL schema: adds `meta_keywords` to `FileHtmlMetaData` and introduces `MdxFrontmatter` type. |
| `src/components/Head.tsx`                                                                      | Adds optional `keywords` prop to render `<meta name="keywords">` tag. |
| `src/components/Layout/Layout.tsx`                                                             | Introduces `Frontmatter` type and updates `PageContextType` for detailed frontmatter structure. |
| `src/components/Layout/MDXWrapper.tsx`                                                         | Enhances SEO metadata extraction and rendering; always renders `PageTitle`; adds `Head` with new props. |
| `src/pages/docs/asset-tracking/index.mdx`, `src/pages/docs/chat/index.mdx`, `src/pages/docs/livesync/index.mdx` | Updates frontmatter: changes `redirect_from` to a list, adds `product` field where missing. |
| `src/templates/base-template.tsx`                                                              | Passes `meta_keywords` to `Head` for rendering as meta tag. |
| `src/templates/template-data.d.ts`                                                             | Adds `meta_keywords` to `AblyDocumentMeta` type. |

## Sequence Diagram(s)

```mermaid
sequenceDiagram
    participant Gatsby as Gatsby Build
    participant GraphQL as GraphQL Layer
    participant MDX as MDX Files
    participant PageCreator as Page Creator Logic
    participant Head as Head Component

    Gatsby->>GraphQL: Query MDX nodes with frontmatter (meta, redirects)
    GraphQL-->>Gatsby: Returns MDX nodes with metadata
    Gatsby->>PageCreator: For each MDX node, process frontmatter
    PageCreator->>PageCreator: Create pages and process redirects
    PageCreator->>Head: Pass SEO metadata (title, description, keywords, canonical)
    Head-->>Gatsby: Render meta tags in HTML head
```

## Assessment against linked issues

| Objective                                                                                   | Addressed | Explanation                                                                                 |
|---------------------------------------------------------------------------------------------|-----------|---------------------------------------------------------------------------------------------|
| Ensure new pages include canonical URLs and meta descriptions (WEB-4409)                    | ✅        |                                                                                             |
| Open graph and twitter card tags are not a priority (WEB-4409)                              | ✅        |                                                                                             |

## Suggested reviewers

- jamiehenson
- m-hulbert
- aralovelace

## Poem

> 🐇  
> In burrows deep, the pages grow,  
> With meta tags that searchers know.  
> Canonicals shine, descriptions gleam,  
> Redirects guide like a gentle stream.  
> Now keywords join the rabbit’s quest—  
> For SEO, our docs are blessed!  
> 🥕

<!-- walkthrough_end -->
<!-- internal state start -->


<!-- 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= -->

<!-- internal state end -->
<!-- tips_start -->

---



<details>
<summary>🪧 Tips</summary>

### Chat

There are 3 ways to chat with [CodeRabbit](https://coderabbit.ai?utm_source=oss&utm_medium=github&utm_campaign=ably/docs&utm_content=2621):

- Review comments: Directly reply to a review comment made by CodeRabbit. Example:
  - `I pushed a fix in commit <commit_id>, please review it.`
  - `Explain this complex logic.`
  - `Open a follow-up GitHub issue for this discussion.`
- Files and specific lines of code (under the "Files changed" tab): Tag `@coderabbitai` in a new review comment at the desired location with your query. Examples:
  - `@coderabbitai explain this code block.`
  -	`@coderabbitai modularize this function.`
- PR comments: Tag `@coderabbitai` in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
  - `@coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.`
  - `@coderabbitai read src/utils.ts and explain its main purpose.`
  - `@coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.`
  - `@coderabbitai help me debug CodeRabbit configuration file.`

### Support

Need help? Create a ticket on our [support page](https://www.coderabbit.ai/contact-us/support) for assistance with any issues or questions.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

### CodeRabbit Commands (Invoked using PR comments)

- `@coderabbitai pause` to pause the reviews on a PR.
- `@coderabbitai resume` to resume the paused reviews.
- `@coderabbitai review` to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
- `@coderabbitai full review` to do a full review from scratch and review all the files again.
- `@coderabbitai summary` to regenerate the summary of the PR.
- `@coderabbitai generate sequence diagram` to generate a sequence diagram of the changes in this PR.
- `@coderabbitai resolve` resolve all the CodeRabbit review comments.
- `@coderabbitai configuration` to show the current CodeRabbit configuration for the repository.
- `@coderabbitai help` to get help.

### Other keywords and placeholders

- Add `@coderabbitai ignore` anywhere in the PR description to prevent this PR from being reviewed.
- Add `@coderabbitai summary` to generate the high-level summary at a specific location in the PR description.
- Add `@coderabbitai` anywhere in the PR title to generate the title automatically.

### CodeRabbit Configuration File (`.coderabbit.yaml`)

- You can programmatically configure CodeRabbit by adding a `.coderabbit.yaml` file to the root of your repository.
- Please see the [configuration documentation](https://docs.coderabbit.ai/guides/configure-coderabbit) for more information.
- If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: `# yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json`

### Documentation and Community

- Visit our [Documentation](https://docs.coderabbit.ai) for detailed information on how to use CodeRabbit.
- Join our [Discord Community](http://discord.gg/coderabbit) to get help, request features, and share feedback.
- Follow us on [X/Twitter](https://twitter.com/coderabbitai) for updates and announcements.

</details>

<!-- tips_end -->
author_association: NONE
user: login: kennethkalmer
created_at: 2025-05-20T08:55:06Z
body: @coderabbitai full review
author_association: MEMBER
user: login: coderabbitai[bot]
created_at: 2025-05-20T08:55:11Z
body: <!-- This is an auto-generated reply by CodeRabbit -->
<details>
<summary>✅ Actions performed</summary>

Full review triggered.

</details>
author_association: NONE
user: login: kennethkalmer
created_at: 2025-05-20T09:59:13Z
body: ~~@m-hulbert I just realised I forgot to point out that we need to add the correct `product:` frontmatter to all the MDX files for the page titles to work correctly, like they do for textile. I'll summon a bot to do it on top of this PR~~

Not needed as @jamiehenson pointed out below 😅 
author_association: MEMBER
user: login: kennethkalmer
created_at: 2025-05-20T11:03:02Z
body: > Looks good, but we shouldn't need to add `product` to the frontmatter - we have that information in the `LayoutContext` at any given time, accessible via `activePage`.

@jamiehenson ah, I shall redact then :)
author_association: MEMBER
user: login: kennethkalmer
created_at: 2025-05-20T11:38:22Z
body: @jamiehenson I pushed up a fixup (to be squashed after review) where I drop the product from the frontmatter and rely on the `activePage`.

Looking at the page titles, it works for the Chat MDX files but not for LiveSync or Asset Tracking. They revert back to the default value of `Ably Realtime` in `getMetaTitle`.
author_association: MEMBER
user: login: kennethkalmer
created_at: 2025-05-20T13:33:45Z
body: @jamiehenson I thought in the end it simpler to just expend the number of keys with variations on product names, to keep it simple
author_association: MEMBER