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