solidjs / solid-meta

Write meta tags to the document head
127 stars 16 forks source link

[Proposal] An alternate design #17

Open nathan-alden-sr opened 1 year ago

nathan-alden-sr commented 1 year ago

I spent some time today coming up with a design that is closer to Helmet. It's a naive implementation, but it works well and can provide more flexibility than solid-meta's current design.

HeadProvider.tsx:

import _ from "lodash-es";
import { Component, createContext, createRenderEffect, ParentComponent, useContext } from "solid-js";

type TitleFormatter = (text: string | null) => string | null;

export interface HeadContextType {
  setMeta: (name: string, content: string) => void;
  setTitle: (text: string | null) => void;
  setTitleFormatter: (formatter: TitleFormatter) => void;
}

const HeadContext = createContext<HeadContextType>();

const HeadProvider: ParentComponent = props => {
  let titleFormatter: TitleFormatter = (text: string | null) => text;

  const value: HeadContextType = {
    setMeta(name, content) {
      const metaElements: NodeListOf<HTMLMetaElement> = document.head.querySelectorAll(`meta[name='${name}']`);

      if (metaElements.length > 0) {
        for (const metaElement of metaElements) {
          metaElement.content = content;
        }
      } else {
        const metaElement = document.createElement("meta");

        metaElement.name = name;
        metaElement.content = content;

        document.head.appendChild(metaElement);
      }
    },

    setTitle(text) {
      const titleElements = document.head.getElementsByTagName("title");

      if (titleElements.length > 0) {
        for (const titleElement of titleElements) {
          titleElement.textContent = titleFormatter(text);
        }
      } else {
        const titleElement = document.createElement("title");

        titleElement.textContent = titleFormatter(text);

        document.head.appendChild(titleElement);
      }
    },

    setTitleFormatter(formatter) {
      titleFormatter = formatter;
    }
  };

  return <HeadContext.Provider value={value}>{props.children}</HeadContext.Provider>;
};

export { HeadProvider };

export interface TitleProps {
  formatter?: TitleFormatter;
  text?: string | null;
}

export const Title: Component<TitleProps> = props => {
  const context = useContext(HeadContext);

  if (_.isNil(context)) {
    throw new Error("Must use within a HeadContext");
  }

  createRenderEffect(() => {
    if (!_.isNil(props.formatter)) {
      context.setTitleFormatter(props.formatter);
    }

    if (!_.isUndefined(props.text)) {
      context.setTitle(props.text);
    }
  });

  return null;
};

export interface MetaProps {
  name: string;
  content: string;
}

export const Meta: Component<MetaProps> = props => {
  const context = useContext(HeadContext);

  if (_.isNil(context)) {
    throw new Error("Must use within a HeadContext");
  }

  createRenderEffect(() => {
    context.setMeta(props.name, props.content);
  });

  return null;
};

Example usage:

<HeadProvider>
  <Title formatter={text => `${text} - My Site`} />
  <Title text="My Page" />
  <Meta name="og:title" content="The Title" />
</HeadProvider>

The <Title> component looks for a <title> elements inside <head> and then overwrites their textContent with the value of the text prop. It also allows the developer to provide a formatter function that is passed the desired text--say, a specific page's title--and, in my example, append the website name to the end of it. The code sets the <title> element's textContent property to My Page - My Site. I'm wondering if it would be better to change document.title directly, but I'm not sure of the pros and cons.

The <Meta> component is similar in that it looks for matching elements inside <head> and, if they exist, overwrites their content properties. If they don't exist, a new <meta> element is created.

The design of each head tag component could be improved to allow for all props similar to your existing code. I didn't do that for the sake of brevity.

I don't have any performance-related code in my example (e.g., no refs). I'm also not sure if my reactivity code is correct (I tried to write code similar to your current implementation). I am still very new to SolidJS.