Home
Wiki
Home
Wiki
  1. 2. Components
  • Back to home
  • 1. Themes
  • Vs Code
    • Getting Started
  • Kitchenware
    • Layout
      • New Layout
      • Legacy Layout
    • Components
      • Announcement
      • Banner Carousel
      • Banner With Products Carousel
      • Blog Category List
      • Blog List
      • Brand List
      • Brands Carousel
      • Breadcrumb
      • Call To Action
      • Cart
      • Categories List
      • Change Password
      • Checkout
      • Cookie Manager
      • Filter list
      • Footer
      • Forgot Password
      • Form
      • Hero Carousel
      • Icon Block
      • Invitation
      • Last Visited Products
      • Layout
      • Login
      • Map
      • Nav Bar
      • Offer
      • Product Attachments
      • Product Attributes
      • Product Documentation
      • Product Expected
      • Product Modal
      • Products Block
      • Products Carousel
      • Product Single
      • Profile
      • Quote
      • Register
      • Related Products
      • Search
      • Stores
      • Subscribe Newsletter
      • Text with Image
      • Top Bar
      • Video
    • Reusables
      • Getting Started
    • Assets
      • Getting Started
    • SDK
      • Products
        • _findProductsByCategory
        • _findProductsByIds
        • _findProductsByTitle
        • _findProductsByFilter
        • _findProductsByCriteria
        • _findProductsAndCalculate
        • _findProductsThenCalculate
        • _getProductAttributeSet
        • _setLastVisited
      • Categories
        • _findCategoryTreeById
        • _findCategoriesByIds
        • _findCategoryByAlias
        • _findCategoryTreeByAlias
        • _getCategoryContent
      • Collections
        • _getCollectionContent
        • _findCollectionsByIds
        • _findCollectionsByIdsThenCalculate
      • Brands
        • _getBrandContent
        • _findBrandsByIds
      • Cart
        • _addToCartMulti
        • _addToCart
        • _setCart
        • _clearCart
        • _setCartListener
        • _removeFromCart
        • _calculateCart
      • Checkout
        • _startCheckout
        • _updateCheckout
        • _completeCheckout
      • Shopping Lists
        • _getShoppingLists
        • _updateShoppingList
        • _createShoppingList
        • _deleteShoppingList
        • _getShoppingListByAlias
      • Navigation
        • _getFooterMenu
        • _getHeaderMenu
      • Users
        • _getUserById
      • Utils
        • _calculateCurrency
        • _getCurrencySymbol
        • _getCulture
        • _subscribeToNewsletter
        • _findUnitsByIds
  • Noir
    • 0. Introduction
    • 1. Structure
      • Overview
      • LayoutA.liquid
      • ComponentsList.liquid
      • Metas.liquid
      • CssVariables.liquid
      • Json.liquid
      • GoogleTagManager.liquid
      • StagingButton.liquid
    • 2. Components
      • Overview
      • Announcement
      • BannerCarousel
      • BlogCategoryList
      • BlogList
      • BrandList
      • Breadcrumb
      • Cart
      • CategoriesList
      • ChangePassword
      • Checkout
      • CookieManager
      • FilterList
      • Footer
      • ForgotPassword
      • Form
      • IconBlock
      • Invitation
      • LastVisitedProducts
      • Login
      • Map
      • NavBar
      • ProductAttachments
      • ProductAttributes
      • ProductComparison
      • ProductDocumentation
      • ProductMixList
      • ProductsBlock
      • ProductsCarousel
      • ProductSingle
      • Profile
      • Register
      • RelatedProducts
      • SingleBlog
      • Stores
      • TextWithImage
      • ThankYouPage
      • TopBar
      • Wishlist
    • 3. Reusables
      • Overview
      • Addresses
      • BillingRetail
      • AddressForm
      • AnnouncementModal
      • BackToTop
      • Company
      • General
      • Login
      • LoginModal
      • Orders
      • Payment
      • ProductAttachments
      • ProductAttributes
      • ProductComparisonButton
      • ProductComparisonFloatingButton
      • ProductGridItem
      • ProductListItem
      • ShoppingListsButton
      • ProductModal
      • ProfileInfo
      • PromptModal
      • Register
      • Shipping
      • ShoppingLists
      • ShoppingListsNavbar
      • Toast
      • Users
      • VariantContent
      • WishlistButton
      • Services
    • 4. Assets
      • Fonts
      • Images
      • Templates
      • Javascript
        • Overview
        • theme.js
      • Css / Scss
        • Overview
        • ThemeClasses
    • 5. SDK
      • Overview
      • LiquidGlobals
      • ServicesSDK
  1. 2. Components

BlogList

Purpose#

The BlogList component renders a blog listing page section with:
A title (“Stay informed”)
Optional category description and category/general header image
A category navigation list (including an “All” entry)
A grid of blog post cards
Pagination (with a sliding window UI and accessible labels)

Inputs (model contract)#

Model shape (storefront example)#

{
  "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",
    "...": "..."
  }
}

Required fields#

settings.id
Used for the wrapper id: comp-{{ id }}
pagination.pageNumber (number)
Current page.
pagination.numberOfPages (number)
Total pages used to build pagination UI.

Optional fields#

settings.cssClass
Applied to the wrapper only when non-empty and not (UNDEFINED).
categories[]
If present, renders the category navigation links.
generalImage (object or null)
Fallback image when the current category doesn’t provide an image.
blogList[]
The list of blog items to render. If empty, a “not found” message is shown.

JavaScript#

Global object#

The component exposes a global object:
It’s used from the template in two places:
Per-card reading time:
x-data='bloglistdefault.singlePost({{ blog.content | serialize }})'
Pagination navigation:
@click="bloglistdefault.redirectToState(pageNumber)"

singlePost(content)#

What it does
Returns an Alpine component object that computes and stores reading time for a single blog post.
It exposes:
time: { hours: -1, minutes: -1 }
init()
getReadingTime()
Returned state (time) conventions
time.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)
Advice
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.
If you want a more accurate estimate, you can strip HTML tags before counting words (only if you’re sure it won’t break languages/encoding).
The 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)#

What it does
Runs automatically when Alpine mounts the per-card scope.
Calls:
this.getReadingTime() once.
Advice
Keep it lightweight: it runs once per blog card. On very large listings, expensive parsing here can impact render time.

getReadingTime() (inside singlePost)#

What it does
If content is falsy, it returns early.
Uses the given content as raw text:
const text = content;
Sets an assumed reading speed:
wps = 3.8 (words per second)
Counts words:
words = text.trim().split(/\\s+/).length
Computes total reading time in seconds (ceiling):
timeInSeconds = ceil(words / wps)
Derives:
hours = floor(timeInSeconds / 3600)
minutes = floor((timeInSeconds % 3600) / 60)
seconds = timeInSeconds % 60
Output rules
If hours > 0:
sets this.time.hours = hours
If minutes === 0 and seconds > 0:
sets this.time.minutes = -2 (template renders “< 1 minute”)
Else if minutes > 0:
sets this.time.minutes = minutes + ceil(seconds / 60)
Important detail (rounding)
The minutes logic intentionally rounds up when there are remaining seconds.
Example: 2m 1s becomes 3 minutes.
Advice
The current minutes rounding can add +1 minute even when seconds are already within the same minute bucket due to Math.floor + Math.ceil.
If you ever want simpler rounding, replace the logic with “ceil(totalSeconds / 60)” and then split into hours/minutes.
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)#

What it does
Client-side navigation helper used by pagination.
Case-by-case behavior:
1.
If newPage is a string
Treats it as a full URL and redirects to it:
window.location.href = newPage
This supports cases where pagination buttons already have a full URL precomputed.
2.
If newPage is a number
Creates a fresh querystring containing only page:
const params = new URLSearchParams();
params.set("page", Number(newPage));
Redirects to:
${window.location.pathname}?page=<n>
3.
If newPage is null/undefined
Reloads the current URL (same path + existing querystring):
window.location.href = window.location.pathname + window.location.search
Advice
This implementation drops all existing query params when you click a numeric page (it only keeps page because it creates a new URLSearchParams()).
If the blog list can ever have additional query params (e.g. 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.

Dependencies#

Alpine.js (for x-data, x-show, reactive time fields)
Theme styles for pagination, grid, and typography

Notes#

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.
Pagination UI is generated server-side (Liquid) but navigation is performed client-side by building a ?page= URL.
Category selection depends on Root.Page.Data.BlogCategory; if the page data doesn’t set it consistently, the active link highlighting/description/image may not behave as expected.

Extras#

Template behavior (Liquid + Alpine)#

Title and category description#

Title uses translations.stayInformed
If Root.Page.Data.BlogCategory.description exists, it is rendered under the title.

Category/general header image#

The component can render a header image (different layout for mobile vs desktop).
It chooses the image source in this order:
1.
Current category image (if currentCategory.mediaItem.link exists)
2.
Otherwise generalImage.link (if provided)
Alt text falls back to translations.noImageAltAvailable when needed.

Categories navigation#

Always renders an “All” link to /blog/posts
Renders each category link to /blog/posts/<alias>
The active category link becomes disabled via pointer-events-none + secondary text class.

Blog cards grid#

Renders a grid of links to /blog/post/<blog.alias>
If the blog has mediaItem.link, it shows an image.
Summary/description behavior:
If summaryHtml exists, it renders that.
Else, it renders content.
Uses line clamping; clamping differs depending on whether an image exists.

Reading time indicator (per blog card)#

Each blog card initializes an Alpine scope to compute reading time:
x-data='singlePost({{ blog.content | serialize }})'
Then prints:
hours (if > 0)
minutes (if > 0)
< 1 minute when minutes are effectively below 1
Modified at 2026-04-14 13:18:56
Previous
BlogCategoryList
Next
BrandList
Built with