Routing to a page in SaaS CMS

More early findings from using a SaaS CMS instance; setting up Graph queries that works for both visitor pageviews and editor previews.

These queries are tested with the Content Type Package in the CLI tool from the previous blog post.

Outside of Optimizely's docs, these resources have been very useful to look around in.

The parameters are gathered either from parsing out the locale from the first part of the path + the full path, or from the loc, key or ver querystring parameters sent by Edit Mode.

An example in Blazor Static SSR when based on the full path.

@page "/{*pageRoute:nonfile}"
..
[Parameter] public string? PageRoute { get; set; }
..
// Fix Graph query parameter to get English start page on root URL
if (path == "/")
{
    path = "/en/";
}

In this example, set en as value on the Locale parameter as well.

query ContentByPath($path: String!, $locale: [Locales]!) {
    _Content(
      where: {
        _or: [
          { _metadata: { url: { default: { eq: $path } } } }
          { _metadata: { url: { hierarchical: { eq: $path } } } }
        ]
      }
      locale: $locale
    ) {
      item {
        _metadata {
            key
            locale
            types
            displayName
            version
            url {
                default
                hierarchical
            }
        }
        _type: __typename
      }
    }
}

If edit mode usage is detected, instead add the preview_token sent by Edit Mode as a HTTP header on the Graph HttpClient request message.

public class GraphClientDelegatingHandler(IHttpContextAccessor httpContextAccessor) : DelegatingHandler
{
    protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
    {
        // Add the caching headers and parameter
        request.Headers.Add("cg-stored-query", "template");

        var uriBuilder = new UriBuilder(request.RequestUri!);
        uriBuilder.Query += "&stored=true";
        request.RequestUri = uriBuilder.Uri;

        // Check for preview_token
        var httpContext = httpContextAccessor.HttpContext;

        if (httpContext != null)
        {
            if (httpContext.Request.Query.TryGetValue("preview_token", out var previewToken))
            {
                request.Headers.Add("Authorization", $"Bearer {previewToken}");
            }
        }

        return base.SendAsync(request, cancellationToken);
    }
}

Then use this query that has identical result structure as ContentByPath, but along with the token allows for getting data from unpublished versions.

query ContentByKeyAndVersion($key: String!, $version: String!, $locale: [Locales]!) {
    _Content(
            locale: $locale,
            where: { _metadata: { key: { eq: $key}, version: { eq: $version }  } }
        ) {
        item {
            _metadata {
                key
                locale
                types
                displayName
                version
                url {
                    default
                    hierarchical
                }
            }
            _type: __typename
        }
    }
}

If we get an empty result we can show the 404 page and avoid sending the larger query with all fragments.

With a result, the first item in the types property is most suitable to use for picking the component to use.

If we see that it is our StandardExperience type we can now use the same query for both cases, for a visitor pageview we just omit the version parameter.

fragment heading1Element on Heading1 {
  Heading
}

fragment heading2Element on Heading2 {
  Heading
}

fragment heading3Element on Heading3 {
  Heading
}

fragment proseElement on Prose {
  Body {
    html
  }
}

fragment singleParagraphElement on SingleParagraph {
  Text
  Highlight
}

fragment preambleElement on Preamble {
  Text
}

fragment singleImageElement on SingleImage {
  ImageReference
  {
    url
    {
      default
    }
  }
  AlternativeText
}

fragment callToActionLinksElement on CallToActionLinks {
  Links {
    text
    url {
      default
    }
  }
}

fragment personElement on Person {
  FullName
  Title
  Telephone
  ImageReference
  {
    url
    {
      default
    }
  }
}

fragment compositionStructureNode on CompositionStructureNode {
  displaySettings { key, value }
  displayTemplateKey
  key
  displayName
  nodeType
  type
}

fragment compositionComponentNode on CompositionComponentNode {
  displaySettings { key, value }
  displayTemplateKey
  key
  displayName
  nodeType
  type
  component {
    ...heading1Element
    ...heading2Element
    ...heading3Element
    ...proseElement
    ...singleParagraphElement
    ...preambleElement
    ...singleImageElement
    ...callToActionLinksElement
    ...personElement
  }
}

query StandardExperienceByKeyAndVersion($key: String!, $version: String, $locale: [Locales]!) {
  StandardExperience(
    locale: $locale
    where: { _metadata: { key: { eq: $key}, version: { eq: $version }  } }
  ) {
    item {
      composition {
        ...compositionStructureNode
        grids: nodes {
          ...compositionComponentNode
          ...compositionStructureNode
          ... on CompositionStructureNode {
            rows: nodes {
              ...compositionStructureNode
              ... on CompositionStructureNode {
                ...compositionStructureNode
                columns: nodes {
                  ...compositionStructureNode
                  ... on CompositionStructureNode {
                    ...compositionStructureNode
                    elements: nodes {
                      ...compositionComponentNode
                    }
                  }
                }
              }
            }
          }
        }
      }
      PageTitle
      _metadata {
        published
        url {
          default
        }
      }
    }
  }
}

Now we have what we need to start rendering Grid Sections or Section level Shared Blocks (the CLI repo has a Person content type for testing that).

The flow works for classic Page Types as well but as I pointed out in my first SaaS CMS blog post I rarely see the need for those in new setups.

Note that this is still very much in the exploring stage but with a slight delay in the JS contentSaved event code when previewing it's stable in getting the correct version showing, both when having Head App locally and deployed to environments.

<script src="@(this.CmsUrl)/util/javascript/communicationinjector.js"></script>

<script>
    window.addEventListener('optimizely:cms:contentSaved', (event) => {
        const message = event.detail;
        setTimeout(function () {
            document.location.href = message.previewUrl;
        }, 600);
    });
</script>

Comments?

Published and tagged with these categories: Optimizely