Every site out there is going to require some level of JavaScript to be added to the pages. From Analytics to Personalization, to Media, to even some basic animations…it’s going to be there.

For things like Google Tag Manager, there’s already some strong opinions on how to implement. Here’s a few:

For other items, you could simply put things in the /public folder. That’s pretty easy.

But each of these require developer intervention to accomplish any updates. For me, this platform should be able to accommodate the changing needs of the marketing group. That translates into “I want to control this through the CMS” which is a little more challenging. But, it can be done!

First, you’re going to need a template for your script. Now, scripts come in a variety of flavors: aync, defer, inline, file include… and you can mix and match!

Always…

This Script template would look something like this:

If you were adding inline code, put it in “Code”

Once you have the Script template, you need to add fields where you can select these script items. If you wanted to have them on each page, that’s one thing, but you want them globally. You’re looking at two fields, each going in a different part of the DOM (more on that later), selected from a TreelistEx. In action, it looks like this:

Ok, but HOW do you get them from the field and onto the page itself? It’s a two step process, actually. First, you need to add it to the route (so it’s global) and then second, you need to add it to the Layout.

Here’s the code for the first part:

namespace MyProject.PipelineProcessors
{
    public class MetaScriptsProcessor : IGetLayoutServiceContextProcessor
    {
        private class ScriptData
        {
            public string url { get; set; }

            public string code { get; set; }

            public bool async { get; set; }

            public bool defer { get; set; }

            public bool anonymous { get; set; }

            public bool json { get; set; }

            public string id { get; set; }
        }

        public void Process(GetLayoutServiceContextArgs args)
        {
            var siteItem = Context.Database.GetItem(Context.Site.StartPath).Parent;

            if (!args.ContextData.ContainsKey("ScriptTagsBody"))
            {
                var items = new List<ScriptData>();

                if (siteItem?.Fields["BodyScriptTags"] != null)
                {
                    MultilistField tagsField = siteItem.Fields["BodyScriptTags"];

                    if (tagsField.Count > 0)
                    {


                        foreach (var tagItem in tagsField.GetItems())
                        {
                            items.Add(new ScriptData
                            {
                                url = tagItem["Url"],
                                code = tagItem["Code"],
                                async = tagItem["Async"] == "1",
                                defer = tagItem["Defer"] == "1",
                                anonymous = tagItem["Anonymous"] == "1",
                                json = tagItem["Json"] == "1",
                                id = tagItem.ID.ToShortID().ToString()
                            });
                        }
                    }
                }

                //Process Tags at Page Level
                var currentItem = Context.Item;

                if (currentItem?.Fields["BodyScriptTags"] != null)
                {
                    MultilistField tagsField = currentItem.Fields["BodyScriptTags"];

                    if (tagsField.Count > 0)
                    {
                        foreach (var tagItem in tagsField.GetItems())
                        {
                            //We don't want to put dupe tags in place
                            if (!items.Any(s => s.id == tagItem.ID.ToShortID().ToString()))
                            {
                                items.Add(new ScriptData
                                {
                                    url = tagItem["Url"],
                                    code = tagItem["Code"],
                                    async = tagItem["Async"] == "1",
                                    defer = tagItem["Defer"] == "1",
                                    anonymous = tagItem["Anonymous"] == "1",
                                    json = tagItem["Json"] == "1",
                                    id = tagItem.ID.ToShortID().ToString()
                                });
                            }
                        }
                    }
                }

                args.ContextData.Add("ScriptTagsBody", items);
            }


            if (!args.ContextData.ContainsKey("ScriptTagsHead"))
            {

                var items = new List<ScriptData>();

                if (siteItem?.Fields["HeadScriptTags"] != null)
                {
                    MultilistField tagsField = siteItem.Fields["HeadScriptTags"];

                    if (tagsField.Count > 0)
                    {


                        foreach (var tagItem in tagsField.GetItems())
                        {
                            items.Add(new ScriptData
                            {
                                url = tagItem["Url"],
                                code = tagItem["Code"],
                                async = tagItem["Async"] == "1",
                                defer = tagItem["Defer"] == "1",
                                anonymous = tagItem["Anonymous"] == "1",
                                json = tagItem["Json"] == "1",
                                id = tagItem.ID.ToShortID().ToString()
                            });
                        }
                    }
                }

                //Process Tags at Page Level
                var currentItem = Context.Item;

                if (currentItem?.Fields["HeadScriptTags"] != null)
                {
                    MultilistField tagsField = currentItem.Fields["HeadScriptTags"];

                    if (tagsField.Count > 0)
                    {
                        foreach (var tagItem in tagsField.GetItems())
                        {
                            //We don't want to put dupe tags in place
                            if (!items.Any(s => s.id == tagItem.ID.ToShortID().ToString()))
                            {
                                items.Add(new ScriptData
                                {
                                    url = tagItem["Url"],
                                    code = tagItem["Code"],
                                    async = tagItem["Async"] == "1",
                                    defer = tagItem["Defer"] == "1",
                                    anonymous = tagItem["Anonymous"] == "1",
                                    json = tagItem["Json"] == "1",
                                    id = tagItem.ID.ToShortID().ToString()
                                });
                            }
                        }
                    }
                }

                args.ContextData.Add("ScriptTagsHead", items);
            }
        }
    }
}

I know that I said you should put this into your Site item, but I actually want to put it on my Base Page AND on my Site. Some scripts are global, some are specific to a template, some are specific to an individual page. This approach addresses that need!

Once we’ve got the code file knocked out, we need to patch our getLayoutServiceContext pipeline:

<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <pipelines>
      <group groupName="layoutService">
        <pipelines>
          <getLayoutServiceContext>
            <processor type="MyProject.PipelineProcessors.MetaScriptsProcessor, MyProject" />
          </getLayoutServiceContext>
        </pipelines>
      </group>
    </pipelines>
  </sitecore>
</configuration>

Now that you have this being added to the context, you should see the following in your Graph Playground:

Soooooo now what? Well. We eat it.

I’ve said this too much IRL…

Ok, we need to consume it. For this part, let’s talk the Layout.tsx. This is really the highest place to get Sitecore context. If you go up to _document.tsx, you’re working a world that doesn’t know Sitecore is a thing. So what do we do in our Layout? Put the following right after your </Head>. Putting Scripts into the next/head is a no-no.

<ScriptList location={ScriptListLocation.HEAD} />
<ScriptList location={ScriptListLocation.BODY} />

I realize that’s not super duper helpful. Here’s what my ScriptList looks like:

import Script from 'next/script';
import { ScriptTag } from 'src/types/ScriptTag';
import { useSitecoreContext } from '@sitecore-jss/sitecore-jss-nextjs';

interface ScriptData {
  ScriptTagsBody?: ScriptTag[];
  ScriptTagsHead?: ScriptTag[];
}

export enum ScriptListLocation {
  HEAD = 'head',
  BODY = 'body',
}

type LocationProps = {
  location: ScriptListLocation;
};

const ScriptList = (props: LocationProps): JSX.Element => {
  const sitecoreContext = useSitecoreContext();

  if (!sitecoreContext?.sitecoreContext) return <></>;

  const scriptData = sitecoreContext.sitecoreContext as ScriptData;

  let tagList;

  switch (props.location) {
    case ScriptListLocation.HEAD:
      tagList = scriptData?.ScriptTagsHead;
      break;
    case ScriptListLocation.BODY:
      tagList = scriptData.ScriptTagsBody;
      break;
  }

  const ary: JSX.Element[] = [];

  tagList?.forEach((tag) => {
    ary.push(
      <Script
        type={tag.json ? 'application/ld+json' : 'text/javascript'}
        src={tag.url ? tag.url : undefined}
        key={tag.id}
        id={tag.id}
        defer={tag.defer ? true : undefined}
        async={tag.async ? true : undefined}
        strategy={
          props.location === ScriptListLocation.HEAD
            ? 'beforeInteractive'
            : 'afterInteractive'
        }
      >
        {tag.code}
      </Script>
    );
  });

  return <>{ary}</>;
};

export default ScriptList;

A few things to take away here:

  1. strategy: There are a few options here:
    • beforeInteractive puts the script in the head of your page, and it will execute before any of the NextJS code does. Some script libraries require this, especially if they are manipulating the DOM.
    • afterInteractive is akin to placing it later in the DOM. For things like handling onclick events in your front end, this is going to be sufficient. This is a non-blocking insertion, always
  2. We support Schema.org here. You may want something like an organization tag on all pages. This can be set globally and updated via the CMS. Just check the box.
  3. We don’t need to use a SetDangerously, as the Script JSX has the ability to contain code ootb.
  4. We need a “key” property to keep our scripts unique.
  5. We’re using a custom type here to contain all the fields for easy reading. See below
export type ScriptTag = {
  url: string;
  code: string;
  async: boolean;
  defer: boolean;
  anonymous: boolean;
  json: boolean;
  id: string;
};

So there you have it….

It’s a longer journey, but in reality, it’s going to make your implementation more nimble than static includes or hard-coding. Cheers!

Facebooktwitterredditlinkedinmail