I’ll build this as a standalone advanced related-posts JavaScript module for your RX Theme path, with safe defaults, performance features, accessibility, AJAX-ready hooks, and graceful fallback behavior.
My dear friend, create this file:
assets/static-js/static-chunks-js/chunk-030-related-posts.js
Then paste this advanced code inside it.
/*!
* RX Theme - Related Posts Advanced Chunk
* File: assets/static-js/static-chunks-js/chunk-030-related-posts.js
* Purpose: Advanced related posts loader, renderer, carousel/grid, cache, accessibility, REST API support.
* Author: RX Theme
*/
(function () {
"use strict";
/**
* RX Related Posts
* -------------------------------------------------------
* Works with:
* 1. Static HTML related posts already printed by PHP
* 2. AJAX/REST API related posts
* 3. Category/tag based related post query
* 4. Grid, slider, compact list, sidebar widget layout
*
* Required container example:
*
* <section
* class="rx-related-posts"
* data-rx-related-posts
* data-post-id="123"
* data-categories="12,14"
* data-tags="5,9"
* data-limit="6"
* data-layout="grid"
* data-fetch="true">
* </section>
*/
var RXRelatedPosts = {
version: "1.0.0",
defaults: {
selector: "[data-rx-related-posts]",
itemSelector: ".rx-related-post",
fetchEnabled: true,
endpoint: "/wp-json/wp/v2/posts",
customEndpoint: "",
limit: 6,
excludeCurrent: true,
layout: "grid",
columns: 3,
mobileColumns: 1,
tabletColumns: 2,
imageSize: "medium_large",
showImage: true,
showCategory: true,
showDate: true,
showAuthor: false,
showExcerpt: true,
showReadingTime: true,
showViewMore: true,
viewMoreText: "View more related articles",
loadingText: "Loading related articles...",
emptyText: "No related articles found.",
errorText: "Related articles could not be loaded.",
cache: true,
cacheType: "sessionStorage",
cacheTTL: 1000 * 60 * 20,
lazyImages: true,
enableCarousel: false,
enableKeyboard: true,
enableAnalyticsEvents: true,
scrollReveal: true,
minTitleLength: 3,
excerptLength: 140,
requestTimeout: 9000,
debug: false
},
state: {
containers: [],
initialized: false,
memoryCache: {},
resizeTimer: null
},
init: function () {
if (this.state.initialized) {
return;
}
this.state.initialized = true;
this.state.containers = Array.prototype.slice.call(
document.querySelectorAll(this.defaults.selector)
);
if (!this.state.containers.length) {
return;
}
this.injectBaseStyle();
this.bindGlobalEvents();
this.state.containers.forEach(
function (container, index) {
this.prepareContainer(container, index);
}.bind(this)
);
this.log("RX Related Posts initialized.");
},
prepareContainer: function (container, index) {
if (!container || container.dataset.rxRelatedInitialized === "true") {
return;
}
container.dataset.rxRelatedInitialized = "true";
container.dataset.rxRelatedIndex = String(index);
var config = this.getContainerConfig(container);
this.setAccessibility(container, config);
this.applyLayoutClass(container, config);
var existingItems = container.querySelectorAll(config.itemSelector);
if (existingItems.length) {
this.enhanceExistingPosts(container, config);
this.finishContainer(container, config);
return;
}
if (config.fetchEnabled) {
this.loadRelatedPosts(container, config);
} else {
this.renderEmpty(container, config);
}
},
getContainerConfig: function (container) {
var data = container.dataset || {};
var config = this.extend({}, this.defaults);
config.fetchEnabled = this.toBoolean(data.fetch, config.fetchEnabled);
config.endpoint = data.endpoint || config.endpoint;
config.customEndpoint = data.customEndpoint || config.customEndpoint;
config.limit = this.toNumber(data.limit, config.limit);
config.layout = data.layout || config.layout;
config.columns = this.toNumber(data.columns, config.columns);
config.mobileColumns = this.toNumber(data.mobileColumns, config.mobileColumns);
config.tabletColumns = this.toNumber(data.tabletColumns, config.tabletColumns);
config.postId = this.toNumber(data.postId, 0);
config.categories = this.parseNumberList(data.categories);
config.tags = this.parseNumberList(data.tags);
config.imageSize = data.imageSize || config.imageSize;
config.showImage = this.toBoolean(data.showImage, config.showImage);
config.showCategory = this.toBoolean(data.showCategory, config.showCategory);
config.showDate = this.toBoolean(data.showDate, config.showDate);
config.showAuthor = this.toBoolean(data.showAuthor, config.showAuthor);
config.showExcerpt = this.toBoolean(data.showExcerpt, config.showExcerpt);
config.showReadingTime = this.toBoolean(data.showReadingTime, config.showReadingTime);
config.showViewMore = this.toBoolean(data.showViewMore, config.showViewMore);
config.enableCarousel = this.toBoolean(data.carousel, config.enableCarousel);
config.lazyImages = this.toBoolean(data.lazyImages, config.lazyImages);
config.cache = this.toBoolean(data.cache, config.cache);
config.emptyText = data.emptyText || config.emptyText;
config.errorText = data.errorText || config.errorText;
config.loadingText = data.loadingText || config.loadingText;
config.viewMoreText = data.viewMoreText || config.viewMoreText;
config.excerptLength = this.toNumber(data.excerptLength, config.excerptLength);
return config;
},
loadRelatedPosts: function (container, config) {
var cacheKey = this.getCacheKey(config);
var cached = config.cache ? this.getCache(cacheKey, config) : null;
if (cached && Array.isArray(cached.posts)) {
this.renderPosts(container, cached.posts, config);
this.finishContainer(container, config);
this.dispatch(container, "rx-related-posts-cache-hit", {
posts: cached.posts,
config: config
});
return;
}
this.renderLoading(container, config);
var url = this.buildRequestUrl(config);
this.fetchWithTimeout(url, {
method: "GET",
credentials: "same-origin",
headers: {
"Accept": "application/json"
}
}, config.requestTimeout)
.then(
function (response) {
if (!response.ok) {
throw new Error("HTTP error " + response.status);
}
return response.json();
}
)
.then(
function (data) {
var posts = this.normalizePosts(data, config);
if (config.excludeCurrent && config.postId) {
posts = posts.filter(function (post) {
return Number(post.id) !== Number(config.postId);
});
}
posts = posts.slice(0, config.limit);
if (config.cache) {
this.setCache(cacheKey, { posts: posts }, config);
}
if (!posts.length) {
this.renderEmpty(container, config);
} else {
this.renderPosts(container, posts, config);
}
this.finishContainer(container, config);
this.dispatch(container, "rx-related-posts-loaded", {
posts: posts,
config: config
});
}.bind(this)
)
.catch(
function (error) {
this.log("Related posts error:", error);
this.renderError(container, config);
this.dispatch(container, "rx-related-posts-error", {
error: error,
config: config
});
}.bind(this)
);
},
buildRequestUrl: function (config) {
var endpoint = config.customEndpoint || config.endpoint;
var url;
try {
url = new URL(endpoint, window.location.origin);
} catch (error) {
url = new URL(this.defaults.endpoint, window.location.origin);
}
url.searchParams.set("per_page", String(Math.max(config.limit + 2, config.limit)));
url.searchParams.set("_embed", "1");
if (config.postId) {
url.searchParams.set("exclude", String(config.postId));
}
if (config.categories.length) {
url.searchParams.set("categories", config.categories.join(","));
}
if (config.tags.length) {
url.searchParams.set("tags", config.tags.join(","));
}
return url.toString();
},
normalizePosts: function (data, config) {
if (!Array.isArray(data)) {
if (data && Array.isArray(data.posts)) {
data = data.posts;
} else {
return [];
}
}
return data.map(
function (item) {
var embedded = item._embedded || {};
var media = embedded["wp:featuredmedia"] && embedded["wp:featuredmedia"][0];
var terms = embedded["wp:term"] || [];
var author = embedded.author && embedded.author[0];
var image = this.getImageFromMedia(media, config.imageSize);
var categoryName = this.getFirstCategoryName(terms);
return {
id: item.id || 0,
title: this.cleanText(item.title && item.title.rendered ? item.title.rendered : item.title || ""),
url: item.link || "#",
excerpt: this.cleanText(item.excerpt && item.excerpt.rendered ? item.excerpt.rendered : item.excerpt || ""),
date: item.date || "",
modified: item.modified || "",
image: image,
category: categoryName,
author: author && author.name ? author.name : "",
readingTime: this.estimateReadingTime(
this.cleanText(item.content && item.content.rendered ? item.content.rendered : item.excerpt || "")
)
};
}.bind(this)
).filter(
function (post) {
return post.title && post.title.length >= config.minTitleLength;
}
);
},
getImageFromMedia: function (media, imageSize) {
if (!media) {
return "";
}
if (
media.media_details &&
media.media_details.sizes &&
media.media_details.sizes[imageSize] &&
media.media_details.sizes[imageSize].source_url
) {
return media.media_details.sizes[imageSize].source_url;
}
if (media.source_url) {
return media.source_url;
}
return "";
},
getFirstCategoryName: function (terms) {
if (!Array.isArray(terms)) {
return "";
}
for (var i = 0; i < terms.length; i++) {
var group = terms[i];
if (!Array.isArray(group)) {
continue;
}
for (var j = 0; j < group.length; j++) {
if (group[j] && group[j].taxonomy === "category" && group[j].name) {
return group[j].name;
}
}
}
return "";
},
renderLoading: function (container, config) {
container.innerHTML = "";
var wrapper = document.createElement("div");
wrapper.className = "rx-related-posts__loading";
wrapper.setAttribute("role", "status");
wrapper.setAttribute("aria-live", "polite");
var text = document.createElement("span");
text.className = "rx-related-posts__loading-text";
text.textContent = config.loadingText;
wrapper.appendChild(text);
for (var i = 0; i < Math.min(config.limit, 6); i++) {
var skeleton = document.createElement("div");
skeleton.className = "rx-related-posts__skeleton";
skeleton.innerHTML =
'<span class="rx-related-posts__skeleton-image"></span>' +
'<span class="rx-related-posts__skeleton-line"></span>' +
'<span class="rx-related-posts__skeleton-line rx-related-posts__skeleton-line--short"></span>';
wrapper.appendChild(skeleton);
}
container.appendChild(wrapper);
},
renderPosts: function (container, posts, config) {
container.innerHTML = "";
var heading = this.getOrCreateHeading(container);
var list = document.createElement("div");
list.className = "rx-related-posts__list";
list.setAttribute("data-rx-related-list", "true");
list.style.setProperty("--rx-related-columns", String(config.columns));
list.style.setProperty("--rx-related-tablet-columns", String(config.tabletColumns));
list.style.setProperty("--rx-related-mobile-columns", String(config.mobileColumns));
posts.forEach(
function (post, index) {
list.appendChild(this.createPostCard(post, config, index));
}.bind(this)
);
container.appendChild(heading);
container.appendChild(list);
if (config.showViewMore && posts.length >= config.limit) {
container.appendChild(this.createViewMoreButton(config));
}
},
getOrCreateHeading: function () {
var heading = document.createElement("div");
heading.className = "rx-related-posts__header";
heading.innerHTML =
'<h2 class="rx-related-posts__title">Related Articles</h2>' +
'<p class="rx-related-posts__subtitle">Carefully selected articles that may help you continue reading.</p>';
return heading;
},
createPostCard: function (post, config, index) {
var article = document.createElement("article");
article.className = "rx-related-post";
article.setAttribute("data-rx-related-item", "true");
article.setAttribute("data-post-id", String(post.id || ""));
article.style.setProperty("--rx-related-index", String(index));
var html = "";
if (config.showImage) {
html += this.createImageHtml(post, config);
}
html += '<div class="rx-related-post__body">';
if (config.showCategory && post.category) {
html += '<span class="rx-related-post__category">' + this.escapeHtml(post.category) + "</span>";
}
html +=
'<h3 class="rx-related-post__title">' +
'<a class="rx-related-post__link" href="' + this.escapeAttribute(post.url) + '">' +
this.escapeHtml(post.title) +
"</a>" +
"</h3>";
html += this.createMetaHtml(post, config);
if (config.showExcerpt && post.excerpt) {
html +=
'<p class="rx-related-post__excerpt">' +
this.escapeHtml(this.truncate(post.excerpt, config.excerptLength)) +
"</p>";
}
html +=
'<a class="rx-related-post__readmore" href="' + this.escapeAttribute(post.url) + '" aria-label="Read more about ' + this.escapeAttribute(post.title) + '">' +
"Read more" +
"</a>";
html += "</div>";
article.innerHTML = html;
return article;
},
createImageHtml: function (post, config) {
var image = post.image || "";
var title = post.title || "";
if (!image) {
return (
'<a class="rx-related-post__media rx-related-post__media--placeholder" href="' + this.escapeAttribute(post.url) + '" aria-label="' + this.escapeAttribute(title) + '">' +
'<span class="rx-related-post__placeholder-icon" aria-hidden="true">+</span>' +
"</a>"
);
}
var loadingAttr = config.lazyImages ? ' loading="lazy" decoding="async"' : ' decoding="async"';
return (
'<a class="rx-related-post__media" href="' + this.escapeAttribute(post.url) + '" aria-label="' + this.escapeAttribute(title) + '">' +
'<img class="rx-related-post__image" src="' + this.escapeAttribute(image) + '" alt="' + this.escapeAttribute(title) + '"' + loadingAttr + ">" +
"</a>"
);
},
createMetaHtml: function (post, config) {
var parts = [];
if (config.showDate && post.date) {
parts.push(
'<time datetime="' + this.escapeAttribute(post.date) + '">' +
this.escapeHtml(this.formatDate(post.date)) +
"</time>"
);
}
if (config.showAuthor && post.author) {
parts.push(
'<span class="rx-related-post__author">' +
this.escapeHtml(post.author) +
"</span>"
);
}
if (config.showReadingTime && post.readingTime) {
parts.push(
'<span class="rx-related-post__reading-time">' +
this.escapeHtml(post.readingTime) +
"</span>"
);
}
if (!parts.length) {
return "";
}
return '<div class="rx-related-post__meta">' + parts.join("<span aria-hidden='true'>•</span>") + "</div>";
},
createViewMoreButton: function (config) {
var wrapper = document.createElement("div");
wrapper.className = "rx-related-posts__footer";
var link = document.createElement("a");
link.className = "rx-related-posts__view-more";
link.href = this.getArchiveUrl(config);
link.textContent = config.viewMoreText;
wrapper.appendChild(link);
return wrapper;
},
getArchiveUrl: function (config) {
if (config.categories && config.categories.length) {
return "/?cat=" + encodeURIComponent(config.categories[0]);
}
if (config.tags && config.tags.length) {
return "/?tag_id=" + encodeURIComponent(config.tags[0]);
}
return "/blog/";
},
renderEmpty: function (container, config) {
container.innerHTML =
'<div class="rx-related-posts__empty" role="status">' +
this.escapeHtml(config.emptyText) +
"</div>";
},
renderError: function (container, config) {
container.innerHTML =
'<div class="rx-related-posts__error" role="alert">' +
this.escapeHtml(config.errorText) +
"</div>";
},
enhanceExistingPosts: function (container, config) {
var items = Array.prototype.slice.call(container.querySelectorAll(config.itemSelector));
items.forEach(
function (item, index) {
item.setAttribute("data-rx-related-item", "true");
item.style.setProperty("--rx-related-index", String(index));
var image = item.querySelector("img");
if (image && config.lazyImages) {
image.loading = "lazy";
image.decoding = "async";
}
var link = item.querySelector("a");
if (link) {
link.addEventListener(
"click",
function () {
this.dispatch(container, "rx-related-post-click", {
url: link.href,
index: index
});
}.bind(this)
);
}
}.bind(this)
);
},
finishContainer: function (container, config) {
container.classList.add("rx-related-posts--ready");
this.observeImages(container);
this.bindItemEvents(container, config);
if (config.enableCarousel) {
this.setupCarousel(container, config);
}
if (config.scrollReveal) {
this.setupReveal(container);
}
},
bindItemEvents: function (container) {
var links = Array.prototype.slice.call(
container.querySelectorAll(".rx-related-post__link, .rx-related-post__readmore, .rx-related-post__media")
);
links.forEach(
function (link, index) {
link.addEventListener(
"click",
function () {
this.dispatch(container, "rx-related-post-click", {
url: link.href,
index: index,
text: link.textContent.trim()
});
}.bind(this)
);
}.bind(this)
);
},
setupCarousel: function (container) {
var list = container.querySelector("[data-rx-related-list]");
if (!list) {
return;
}
container.classList.add("rx-related-posts--carousel");
var controls = document.createElement("div");
controls.className = "rx-related-posts__controls";
var prev = document.createElement("button");
prev.type = "button";
prev.className = "rx-related-posts__control rx-related-posts__control--prev";
prev.setAttribute("aria-label", "Previous related post");
prev.textContent = "‹";
var next = document.createElement("button");
next.type = "button";
next.className = "rx-related-posts__control rx-related-posts__control--next";
next.setAttribute("aria-label", "Next related post");
next.textContent = "›";
controls.appendChild(prev);
controls.appendChild(next);
container.appendChild(controls);
prev.addEventListener("click", function () {
list.scrollBy({
left: -Math.max(280, list.clientWidth * 0.75),
behavior: "smooth"
});
});
next.addEventListener("click", function () {
list.scrollBy({
left: Math.max(280, list.clientWidth * 0.75),
behavior: "smooth"
});
});
},
setupReveal: function (container) {
var items = Array.prototype.slice.call(container.querySelectorAll("[data-rx-related-item]"));
if (!("IntersectionObserver" in window)) {
items.forEach(function (item) {
item.classList.add("rx-related-post--visible");
});
return;
}
var observer = new IntersectionObserver(
function (entries) {
entries.forEach(function (entry) {
if (entry.isIntersecting) {
entry.target.classList.add("rx-related-post--visible");
observer.unobserve(entry.target);
}
});
},
{
rootMargin: "0px 0px -8% 0px",
threshold: 0.1
}
);
items.forEach(function (item) {
observer.observe(item);
});
},
observeImages: function (container) {
var images = Array.prototype.slice.call(container.querySelectorAll("img"));
images.forEach(function (image) {
image.addEventListener("load", function () {
image.classList.add("rx-related-post__image--loaded");
});
image.addEventListener("error", function () {
image.classList.add("rx-related-post__image--error");
image.setAttribute("alt", "");
});
});
},
setAccessibility: function (container) {
if (!container.getAttribute("role")) {
container.setAttribute("role", "region");
}
if (!container.getAttribute("aria-label")) {
container.setAttribute("aria-label", "Related posts");
}
},
applyLayoutClass: function (container, config) {
container.classList.add("rx-related-posts--" + config.layout);
if (config.enableCarousel) {
container.classList.add("rx-related-posts--has-carousel");
}
},
bindGlobalEvents: function () {
window.addEventListener(
"resize",
this.debounce(
function () {
document.documentElement.style.setProperty(
"--rx-related-window-width",
String(window.innerWidth)
);
},
150
)
);
document.addEventListener(
"keydown",
function (event) {
if (!this.defaults.enableKeyboard) {
return;
}
var active = document.activeElement;
if (!active || !active.closest) {
return;
}
var container = active.closest(this.defaults.selector);
if (!container) {
return;
}
if (event.key === "ArrowRight") {
this.focusNextRelatedLink(container, active);
}
if (event.key === "ArrowLeft") {
this.focusPrevRelatedLink(container, active);
}
}.bind(this)
);
},
focusNextRelatedLink: function (container, active) {
var links = Array.prototype.slice.call(container.querySelectorAll("a, button"));
var index = links.indexOf(active);
if (index > -1 && links[index + 1]) {
links[index + 1].focus();
}
},
focusPrevRelatedLink: function (container, active) {
var links = Array.prototype.slice.call(container.querySelectorAll("a, button"));
var index = links.indexOf(active);
if (index > 0 && links[index - 1]) {
links[index - 1].focus();
}
},
fetchWithTimeout: function (url, options, timeout) {
if ("AbortController" in window) {
var controller = new AbortController();
var timer = setTimeout(function () {
controller.abort();
}, timeout);
options.signal = controller.signal;
return fetch(url, options).finally(function () {
clearTimeout(timer);
});
}
return Promise.race([
fetch(url, options),
new Promise(function (_, reject) {
setTimeout(function () {
reject(new Error("Request timeout"));
}, timeout);
})
]);
},
getCacheKey: function (config) {
return [
"rx_related_posts",
"v" + this.version,
"post_" + config.postId,
"cat_" + config.categories.join("-"),
"tag_" + config.tags.join("-"),
"limit_" + config.limit,
"layout_" + config.layout
].join(":");
},
getCache: function (key, config) {
try {
if (config.cacheType === "memory") {
return this.state.memoryCache[key] || null;
}
var raw = window.sessionStorage.getItem(key);
if (!raw) {
return null;
}
var cached = JSON.parse(raw);
if (!cached || !cached.createdAt) {
return null;
}
if (Date.now() - cached.createdAt > config.cacheTTL) {
window.sessionStorage.removeItem(key);
return null;
}
return cached.value;
} catch (error) {
return null;
}
},
setCache: function (key, value, config) {
try {
if (config.cacheType === "memory") {
this.state.memoryCache[key] = value;
return;
}
window.sessionStorage.setItem(
key,
JSON.stringify({
createdAt: Date.now(),
value: value
})
);
} catch (error) {
this.state.memoryCache[key] = value;
}
},
injectBaseStyle: function () {
if (document.getElementById("rx-related-posts-base-style")) {
return;
}
var style = document.createElement("style");
style.id = "rx-related-posts-base-style";
style.textContent = `
.rx-related-posts {
--rx-related-gap: 24px;
--rx-related-radius: 18px;
--rx-related-border: rgba(15, 23, 42, 0.10);
--rx-related-bg: #ffffff;
--rx-related-soft-bg: rgba(15, 23, 42, 0.035);
--rx-related-text: #0f172a;
--rx-related-muted: #64748b;
--rx-related-accent: #0f766e;
margin-block: 32px;
position: relative;
}
.rx-related-posts__header {
margin-bottom: 18px;
}
.rx-related-posts__title {
margin: 0 0 6px;
font-size: clamp(1.35rem, 2vw, 1.8rem);
line-height: 1.25;
color: var(--rx-related-text);
}
.rx-related-posts__subtitle {
margin: 0;
color: var(--rx-related-muted);
font-size: 0.96rem;
line-height: 1.65;
}
.rx-related-posts__list {
display: grid;
grid-template-columns: repeat(var(--rx-related-columns, 3), minmax(0, 1fr));
gap: var(--rx-related-gap);
}
.rx-related-post {
overflow: hidden;
border: 1px solid var(--rx-related-border);
border-radius: var(--rx-related-radius);
background: var(--rx-related-bg);
transform: translateY(12px);
opacity: 0;
transition: transform 280ms ease, opacity 280ms ease, box-shadow 280ms ease, border-color 280ms ease;
transition-delay: calc(var(--rx-related-index, 0) * 35ms);
}
.rx-related-post--visible,
.rx-related-posts:not(.rx-related-posts--ready) .rx-related-post {
transform: translateY(0);
opacity: 1;
}
.rx-related-post:hover {
border-color: rgba(15, 118, 110, 0.35);
box-shadow: 0 18px 46px rgba(15, 23, 42, 0.12);
}
.rx-related-post__media {
display: block;
position: relative;
aspect-ratio: 16 / 9;
overflow: hidden;
background: var(--rx-related-soft-bg);
text-decoration: none;
}
.rx-related-post__image {
width: 100%;
height: 100%;
display: block;
object-fit: cover;
transform: scale(1.01);
transition: transform 380ms ease, opacity 260ms ease;
}
.rx-related-post:hover .rx-related-post__image {
transform: scale(1.06);
}
.rx-related-post__media--placeholder {
display: flex;
align-items: center;
justify-content: center;
}
.rx-related-post__placeholder-icon {
width: 42px;
height: 42px;
border-radius: 999px;
display: inline-flex;
align-items: center;
justify-content: center;
background: rgba(15, 118, 110, 0.12);
color: var(--rx-related-accent);
font-size: 1.4rem;
font-weight: 700;
}
.rx-related-post__body {
padding: 18px;
}
.rx-related-post__category {
display: inline-flex;
margin-bottom: 10px;
font-size: 0.78rem;
font-weight: 700;
letter-spacing: 0.02em;
color: var(--rx-related-accent);
}
.rx-related-post__title {
margin: 0 0 10px;
font-size: clamp(1.05rem, 1.5vw, 1.25rem);
line-height: 1.35;
}
.rx-related-post__link {
color: var(--rx-related-text);
text-decoration: none;
}
.rx-related-post__link:hover,
.rx-related-post__link:focus {
color: var(--rx-related-accent);
text-decoration: underline;
text-underline-offset: 3px;
}
.rx-related-post__meta {
display: flex;
flex-wrap: wrap;
gap: 7px;
align-items: center;
margin-bottom: 10px;
color: var(--rx-related-muted);
font-size: 0.83rem;
line-height: 1.5;
}
.rx-related-post__excerpt {
margin: 0 0 14px;
color: var(--rx-related-muted);
font-size: 0.94rem;
line-height: 1.7;
}
.rx-related-post__readmore {
display: inline-flex;
align-items: center;
gap: 6px;
color: var(--rx-related-accent);
font-weight: 700;
font-size: 0.9rem;
text-decoration: none;
}
.rx-related-post__readmore:hover,
.rx-related-post__readmore:focus {
text-decoration: underline;
text-underline-offset: 3px;
}
.rx-related-posts__footer {
margin-top: 24px;
text-align: center;
}
.rx-related-posts__view-more {
display: inline-flex;
align-items: center;
justify-content: center;
min-height: 44px;
padding: 10px 18px;
border-radius: 999px;
background: var(--rx-related-accent);
color: #ffffff;
font-weight: 700;
text-decoration: none;
}
.rx-related-posts__view-more:hover,
.rx-related-posts__view-more:focus {
filter: brightness(0.95);
}
.rx-related-posts__loading {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: var(--rx-related-gap);
}
.rx-related-posts__loading-text {
grid-column: 1 / -1;
color: var(--rx-related-muted);
}
.rx-related-posts__skeleton {
border-radius: var(--rx-related-radius);
border: 1px solid var(--rx-related-border);
padding: 14px;
background: var(--rx-related-bg);
}
.rx-related-posts__skeleton-image,
.rx-related-posts__skeleton-line {
display: block;
border-radius: 12px;
background: linear-gradient(90deg, rgba(15,23,42,.05), rgba(15,23,42,.10), rgba(15,23,42,.05));
background-size: 200% 100%;
animation: rxRelatedShimmer 1.4s infinite;
}
.rx-related-posts__skeleton-image {
aspect-ratio: 16 / 9;
margin-bottom: 14px;
}
.rx-related-posts__skeleton-line {
height: 14px;
margin-bottom: 10px;
}
.rx-related-posts__skeleton-line--short {
width: 68%;
}
.rx-related-posts__empty,
.rx-related-posts__error {
padding: 18px;
border-radius: var(--rx-related-radius);
background: var(--rx-related-soft-bg);
color: var(--rx-related-muted);
}
.rx-related-posts--carousel .rx-related-posts__list {
display: flex;
overflow-x: auto;
scroll-snap-type: x mandatory;
scrollbar-width: thin;
padding-bottom: 8px;
}
.rx-related-posts--carousel .rx-related-post {
min-width: min(86vw, 330px);
scroll-snap-align: start;
}
.rx-related-posts__controls {
display: flex;
justify-content: flex-end;
gap: 10px;
margin-top: 14px;
}
.rx-related-posts__control {
width: 42px;
height: 42px;
border-radius: 999px;
border: 1px solid var(--rx-related-border);
background: var(--rx-related-bg);
color: var(--rx-related-text);
cursor: pointer;
font-size: 1.5rem;
line-height: 1;
}
.rx-related-posts__control:hover,
.rx-related-posts__control:focus {
border-color: var(--rx-related-accent);
color: var(--rx-related-accent);
}
@keyframes rxRelatedShimmer {
0% { background-position: 200% 0; }
100% { background-position: -200% 0; }
}
@media (max-width: 900px) {
.rx-related-posts__list {
grid-template-columns: repeat(var(--rx-related-tablet-columns, 2), minmax(0, 1fr));
}
.rx-related-posts__loading {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
}
@media (max-width: 640px) {
.rx-related-posts__list {
grid-template-columns: repeat(var(--rx-related-mobile-columns, 1), minmax(0, 1fr));
}
.rx-related-posts__loading {
grid-template-columns: 1fr;
}
.rx-related-post__body {
padding: 16px;
}
}
@media (prefers-reduced-motion: reduce) {
.rx-related-post,
.rx-related-post__image,
.rx-related-posts__skeleton-image,
.rx-related-posts__skeleton-line {
animation: none !important;
transition: none !important;
}
}
`;
document.head.appendChild(style);
},
estimateReadingTime: function (text) {
if (!text) {
return "";
}
var words = text.trim().split(/\s+/).filter(Boolean).length;
var minutes = Math.max(1, Math.ceil(words / 220));
return minutes + " min read";
},
formatDate: function (dateString) {
if (!dateString) {
return "";
}
try {
var date = new Date(dateString);
return date.toLocaleDateString(undefined, {
year: "numeric",
month: "short",
day: "numeric"
});
} catch (error) {
return dateString;
}
},
cleanText: function (html) {
if (!html) {
return "";
}
var div = document.createElement("div");
div.innerHTML = String(html);
return (div.textContent || div.innerText || "")
.replace(/\s+/g, " ")
.trim();
},
truncate: function (text, length) {
text = String(text || "").trim();
if (text.length <= length) {
return text;
}
return text.slice(0, length).replace(/\s+\S*$/, "") + "…";
},
parseNumberList: function (value) {
if (!value) {
return [];
}
return String(value)
.split(",")
.map(function (item) {
return parseInt(item.trim(), 10);
})
.filter(function (num) {
return !isNaN(num) && num > 0;
});
},
toNumber: function (value, fallback) {
var number = parseInt(value, 10);
return isNaN(number) ? fallback : number;
},
toBoolean: function (value, fallback) {
if (typeof value === "undefined") {
return fallback;
}
if (value === true || value === "true" || value === "1" || value === "yes") {
return true;
}
if (value === false || value === "false" || value === "0" || value === "no") {
return false;
}
return fallback;
},
escapeHtml: function (value) {
return String(value || "")
.replace(/&/g, "&")
.replace(/</g, "<")
.replace(/>/g, ">")
.replace(/"/g, """)
.replace(/'/g, "'");
},
escapeAttribute: function (value) {
return this.escapeHtml(value).replace(/`/g, "`");
},
extend: function () {
var output = {};
for (var i = 0; i < arguments.length; i++) {
var source = arguments[i] || {};
Object.keys(source).forEach(function (key) {
output[key] = source[key];
});
}
return output;
},
debounce: function (callback, delay) {
var timer;
return function () {
var context = this;
var args = arguments;
clearTimeout(timer);
timer = setTimeout(function () {
callback.apply(context, args);
}, delay);
};
},
dispatch: function (container, name, detail) {
if (!this.defaults.enableAnalyticsEvents) {
return;
}
var event;
try {
event = new CustomEvent(name, {
bubbles: true,
detail: detail || {}
});
} catch (error) {
event = document.createEvent("CustomEvent");
event.initCustomEvent(name, true, true, detail || {});
}
container.dispatchEvent(event);
},
log: function () {
if (!this.defaults.debug) {
return;
}
if (window.console && console.log) {
console.log.apply(console, arguments);
}
}
};
window.RXRelatedPosts = RXRelatedPosts;
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", function () {
RXRelatedPosts.init();
});
} else {
RXRelatedPosts.init();
}
})();
Use this HTML/PHP output area where related posts should appear:
<section
class="rx-related-posts"
data-rx-related-posts
data-post-id="123"
data-categories="12,14"
data-tags="5,9"
data-limit="6"
data-layout="grid"
data-columns="3"
data-tablet-columns="2"
data-mobile-columns="1"
data-fetch="true"
data-carousel="false">
</section>
For WordPress dynamic single post, your PHP can print like this:
<section
class="rx-related-posts"
data-rx-related-posts
data-post-id="<?php echo esc_attr( get_the_ID() ); ?>"
data-categories="<?php echo esc_attr( implode( ',', wp_get_post_categories( get_the_ID() ) ) ); ?>"
data-tags="<?php echo esc_attr( implode( ',', wp_get_post_tags( get_the_ID(), array( 'fields' => 'ids' ) ) ) ); ?>"
data-limit="6"
data-layout="grid"
data-columns="3"
data-tablet-columns="2"
data-mobile-columns="1"
data-fetch="true">
</section>
Enqueue it in functions.php:
function rx_theme_enqueue_related_posts_chunk() {
if ( is_single() ) {
wp_enqueue_script(
'rx-related-posts-chunk',
get_template_directory_uri() . '/assets/static-js/static-chunks-js/chunk-030-related-posts.js',
array(),
wp_get_theme()->get( 'Version' ),
true
);
}
}
add_action( 'wp_enqueue_scripts', 'rx_theme_enqueue_related_posts_chunk' );
This file is now advanced enough for your RX Theme: REST API loading, category/tag matching, cache, skeleton loading, responsive grid, optional carousel, lazy images, keyboard support, accessibility, event hooks, and fallback handling.