{
"totalPages": 1,
"page": 1,
"categories": [
{
"id": "Category Id",
"title": "Sample category title",
"alias": "sample-category-alias"
},
...
],
"generalImage": null,
"blogList": [
{
"title": "Sample blog title",
"content": "<p>Sample content</p>",
"summaryHtml": "<p>Sample summary</p>",
"alias": "sample-blog-alias",
"publishedAt": "2025-01-01T00:00:00+00:00",
"mediaItem": {
"id": "Image Id",
"link": "https://example.com/sample",
"mediaType": "Image"
}
},
...
],
"name": "BlogList",
"view": "Default",
"section": "SectionA",
"settings": {
"id": "Component Id",
"section": "SectionA",
"type": "NoirBlogList",
"name": "BlogList",
"configuredInContentApi": true,
"view": "Default",
"displayName": "",
"cssClass": ""
},
"translations": {
"stayInformed": "Sample translation",
"previousPage": "Sample translation",
"nextPage": "Sample translation",
"...": "..."
}
}settings.idcomp-{{ id }}pagination.pageNumber (number)pagination.numberOfPages (number)settings.cssClass(UNDEFINED).categories[]generalImage (object or null)blogList[]x-data='bloglistdefault.singlePost({{ blog.content | serialize }})'@click="bloglistdefault.redirectToState(pageNumber)"singlePost(content)time: { hours: -1, minutes: -1 }init()getReadingTime()time) conventionstime.hours = -1 means “don’t show hours”.time.minutes values:-1 → “don’t show minutes yet / not computed”-2 → special marker used by the template for: “< 1 minute”>= 1 → show minutes (rounded up)content is expected to be a string. If it’s HTML, the current implementation counts HTML tokens as words too, so reading time can be slightly overestimated.time sentinel values (-1, -2) are a template contract. If you change them, update the Liquid conditions that render the time label.init() (inside singlePost)this.getReadingTime() once.getReadingTime() (inside singlePost)content is falsy, it returns early.content as raw text:const text = content;wps = 3.8 (words per second)words = text.trim().split(/\\s+/).lengthtimeInSeconds = ceil(words / wps)hours = floor(timeInSeconds / 3600)minutes = floor((timeInSeconds % 3600) / 60)seconds = timeInSeconds % 60hours > 0:this.time.hours = hoursminutes === 0 and seconds > 0:this.time.minutes = -2 (template renders “< 1 minute”)minutes > 0:this.time.minutes = minutes + ceil(seconds / 60)2m 1s becomes 3 minutes.Math.floor + Math.ceil.text.trim() will throw if content isn’t a string (e.g., null/object). If the model might contain non-string content, guard with if (typeof content !== "string") return;.redirectToState(newPage = null)newPage is a stringwindow.location.href = newPagenewPage is a numberpage:const params = new URLSearchParams();params.set("page", Number(newPage));${window.location.pathname}?page=<n>newPage is null/undefinedwindow.location.href = window.location.pathname + window.location.searchpage because it creates a new URLSearchParams()).category, tag, search), you should start from the current search params instead:const params = new URLSearchParams(window.location.search); params.set("page", ...)Number(newPage) can produce NaN if the input can’t be converted. If you want safer behavior, validate with Number.isFinite(...) before redirecting.x-data, x-show, reactive time fields)blog.content is used for reading time calculation. If it contains HTML, the word count will include markup tokens as words, which can slightly affect the reading time estimate.?page= URL.Root.Page.Data.BlogCategory; if the page data doesn’t set it consistently, the active link highlighting/description/image may not behave as expected.translations.stayInformedRoot.Page.Data.BlogCategory.description exists, it is rendered under the title.currentCategory.mediaItem.link exists)generalImage.link (if provided)translations.noImageAltAvailable when needed./blog/posts/blog/posts/<alias>pointer-events-none + secondary text class./blog/post/<blog.alias>mediaItem.link, it shows an image.summaryHtml exists, it renders that.content.x-data='singlePost({{ blog.content | serialize }})'< 1 minute when minutes are effectively below 1