Running your blog on HubSpot but your shop on Shopify? You do not have to choose between the two, or copy posts across by hand. With a small custom Shopify section you can pull your latest HubSpot articles straight into any page of your storefront, styled to match your theme.
This guide walks through a lightweight, dependency-free approach that reads your HubSpot blog’s RSS feed in the browser and renders it as native-looking article cards.
Why an RSS-based approach
HubSpot publishes an RSS feed for every blog automatically. That feed is a simple, standard format that already contains everything a card needs: title, link, publish date, category and a description with the featured image. Reading it directly means no app subscriptions and no monthly cost, no server or middleware to maintain, content that is always current because the feed is fetched fresh on each page load, and markup that stays on-brand because you reuse your theme’s existing article styles.
Step 1: find your HubSpot RSS feed URL
Every HubSpot blog exposes a feed at a predictable address. It usually follows one of these patterns:
https://blog.yourdomain.com/rss.xml
https://www.yourdomain.com/blog/rss.xml
Open the URL in a browser. If you see XML with a series of <item> blocks, you have the right address. Keep it handy for the section settings.
Step 2: create the section file
In your theme, add a new file under sections/ named hubspot-blog-posts.liquid. This single file holds the markup, the styling hook and the script. The section renders a heading, an optional “view all” link and a list container that the script fills in. Every setting is passed to the browser through data attributes so the script can read them without any inline scripting.
<section
class="section hs-blog-feed"
data-section-id="{{ section.id }}"
data-section-type="hubspot-blog-posts"
data-feed-url="{{ section.settings.feed_url | escape }}"
data-limit="{{ section.settings.posts_count }}"
data-show-category="{{ section.settings.show_category }}"
data-show-date="{{ section.settings.show_date }}"
data-show-excerpt="{{ section.settings.show_excerpt }}"
data-stack-mobile="{{ section.settings.stack_mobile }}"
data-link-url="{{ section.settings.link_url }}"
data-read-more="{{ section.settings.read_more_label | escape }}">
<div class="container">
{%- if section.settings.title != blank -%}
<header class="section__header">
<h2 class="section__title heading h3">{{ section.settings.title | escape }}</h2>
{%- if section.settings.link_title != blank and section.settings.link_url != blank -%}
<a href="{{ section.settings.link_url }}" class="section__action-link link" target="_blank" rel="noopener">{{ section.settings.link_title | escape }}</a>
{%- endif -%}
</header>
{%- endif -%}
<div class="block-list block-list--loose hs-blog-feed__list" data-hs-feed-list aria-live="polite">
<div class="hs-blog-feed__status">Loading…</div>
</div>
</div>
</section>
Scope the CSS to the section instance so it never leaks into the rest of the theme. This sets the image aspect ratio and a neutral placeholder for posts without a featured image.
<style>
#shopify-section-{{ section.id }} .hs-blog-feed__ratio { padding-bottom: 54%; }
#shopify-section-{{ section.id }} .hs-blog-feed__ratio--placeholder { background: #1a1a1a; }
#shopify-section-{{ section.id }} .hs-blog-feed__image--logo { object-fit: contain; padding: 12%; background: #1a1a1a; }
#shopify-section-{{ section.id }} .hs-blog-feed__status { padding: 1.5rem 0; opacity: .7; }
#shopify-section-{{ section.id }} .hs-blog-feed__list { min-height: 4rem; }
</style>
Step 3: fetch and parse the feed
The script does four things: fetch the feed, parse the XML, map each item to a tidy object, then build a card for each one. Because RSS descriptions contain HTML, use DOMParser to safely pull out the first image and a plain-text excerpt.
function firstImage(html) {
var doc = new DOMParser().parseFromString(html || '', 'text/html');
var img = doc.querySelector('img');
return img ? img.getAttribute('src') : '';
}
function textExcerpt(html, limit) {
var doc = new DOMParser().parseFromString(html || '', 'text/html');
var imgs = doc.querySelectorAll('img');
for (var i = 0; i < imgs.length; i++) { imgs[i].remove(); }
var text = (doc.body.textContent || '').replace(/\s+/g, ' ').trim();
if (text.length > limit) { text = text.slice(0, limit).trim() + '\u2026'; }
return text;
}
function formatDate(value) {
var d = new Date(value);
if (isNaN(d.getTime())) { return ''; }
return d.toLocaleDateString('en-GB', { day: 'numeric', month: 'short', year: 'numeric' });
}
The fetch itself is a standard request. Read the response as text, hand it to DOMParser, then slice the items down to the configured limit.
fetch(feedUrl, { headers: { Accept: 'application/rss+xml, text/xml' } })
.then(function (res) {
if (!res.ok) { throw new Error('HTTP ' + res.status); }
return res.text();
})
.then(function (xmlText) {
var xml = new DOMParser().parseFromString(xmlText, 'text/xml');
var nodes = Array.prototype.slice.call(xml.querySelectorAll('item')).slice(0, opts.limit);
var items = nodes.map(function (node) {
var get = function (tag) {
var el = node.querySelector(tag);
return el ? (el.textContent || '').trim() : '';
};
var desc = get('description');
return {
title: get('title'),
link: get('link'),
category: get('category'),
date: formatDate(get('pubDate')),
image: firstImage(desc),
excerpt: textExcerpt(desc, 236)
};
});
listEl.innerHTML = '';
items.forEach(function (item, index) {
listEl.appendChild(buildCard(item, opts, index));
});
})
.catch(function () {
listEl.innerHTML = '';
});
Step 4: build the cards
Rather than injecting a big HTML string, build each card with createElement. It keeps user-provided text safe, because it is set with textContent and never innerHTML, and lets you reuse your theme’s existing article classes so the cards inherit your site’s look.
function buildCard(item, opts, index) {
var wrap = document.createElement('div');
wrap.className = 'block-list__item 1/2--tablet 1/3--lap-and-up';
var card = document.createElement('div');
card.className = 'article-item';
var imageLink = document.createElement('a');
imageLink.className = 'article-item__image-container';
imageLink.href = item.link;
imageLink.target = '_blank';
imageLink.rel = 'noopener';
var ratio = document.createElement('div');
ratio.className = 'aspect-ratio hs-blog-feed__ratio';
var img = document.createElement('img');
img.className = 'article-item__image';
img.loading = 'lazy';
img.alt = item.title;
function useFallback() {
img.onerror = null;
img.src = fallbackImage;
img.className = 'article-item__image hs-blog-feed__image--logo';
ratio.className = 'aspect-ratio hs-blog-feed__ratio hs-blog-feed__ratio--placeholder';
}
if (item.image) {
img.onerror = useFallback;
img.src = item.image;
} else {
useFallback();
}
ratio.appendChild(img);
imageLink.appendChild(ratio);
card.appendChild(imageLink);
var title = document.createElement('h3');
title.className = 'article-item__title heading h4';
var titleLink = document.createElement('a');
titleLink.className = 'link';
titleLink.href = item.link;
titleLink.target = '_blank';
titleLink.rel = 'noopener';
titleLink.textContent = item.title;
title.appendChild(titleLink);
card.appendChild(title);
var meta = document.createElement('div');
meta.className = 'article-item__meta';
if (opts.showCategory && item.category) {
var cat = document.createElement('span');
cat.className = 'article-item__meta-item';
cat.textContent = item.category;
meta.appendChild(cat);
}
if (opts.showDate && item.date) {
var time = document.createElement('time');
time.className = 'article-item__meta-item';
time.textContent = item.date;
meta.appendChild(time);
}
if (meta.childNodes.length) { card.appendChild(meta); }
if (opts.showExcerpt && item.excerpt) {
var excerpt = document.createElement('div');
excerpt.className = 'article-item__excerpt rte';
excerpt.textContent = item.excerpt;
card.appendChild(excerpt);
}
var more = document.createElement('a');
more.className = 'a1';
more.href = item.link;
more.target = '_blank';
more.rel = 'noopener';
more.textContent = opts.readMore;
card.appendChild(more);
wrap.appendChild(card);
return wrap;
}
Set a fallbackImage variable near the top of the script to a neutral placeholder hosted in your own Shopify files, so posts without a featured image still look intentional.
Step 5: wire up the settings schema
The schema block turns the section into something a merchant can configure without touching code. Note the feed_url default is left as a generic placeholder for you to replace.
{% schema %}
{
"name": "HubSpot blog posts",
"settings": [
{ "type": "text", "id": "feed_url", "label": "RSS feed URL", "default": "https://blog.yourdomain.com/rss.xml" },
{ "type": "text", "id": "title", "label": "Heading", "default": "From our blog" },
{ "type": "text", "id": "link_title", "label": "Link title", "default": "VIEW ALL BLOG POSTS" },
{ "type": "url", "id": "link_url", "label": "Link URL", "info": "Where the heading link and cards point." },
{ "type": "text", "id": "read_more_label", "label": "Card link label", "default": "Find out more" },
{ "type": "range", "id": "posts_count", "label": "Posts to show", "min": 3, "max": 12, "step": 1, "default": 3 },
{ "type": "checkbox", "id": "show_category", "label": "Show category", "default": true },
{ "type": "checkbox", "id": "show_date", "label": "Show date", "default": true },
{ "type": "checkbox", "id": "show_excerpt", "label": "Show excerpt", "default": true },
{ "type": "checkbox", "id": "stack_mobile", "label": "Stack on mobile", "default": true }
],
"presets": [
{ "category": "Blog", "name": "HubSpot blog posts" }
]
}
{% endschema %}
Step 6: add it to a page
Save the file and open the Shopify theme editor. Add a new section, choose HubSpot blog posts, paste your RSS feed URL, and adjust the heading, post count and toggles. The cards will appear as soon as the feed loads.
A note on cross-origin requests
Browsers block requests to other domains unless the server allows them. HubSpot blog feeds are typically served with permissive CORS headers, so the fetch works directly. If your feed is locked down, route the request through a tiny serverless proxy, for example a Cloudflare Worker, that adds an Access-Control-Allow-Origin header, and point feed_url at the proxy instead.
Wrapping up
With one self-contained section you get a live, on-brand HubSpot blog feed inside Shopify: no apps, no scheduled syncs and no duplicated content. Because the cards reuse your theme’s own article styles, they will keep matching your storefront as your design evolves. Point it at your feed, tweak the toggles, and your latest articles will follow your shop automatically.