Skip to content

Blog System Documentation

This document provides a comprehensive guide to the blog system in Heatwave, covering content creation, SEO features, quality checks, and available plugins.

  1. Overview
  2. Date Semantics
  3. Content Structure
  4. SEO & Structured Data
  5. Liquid Tags (Plugins)
  6. Quality Checks
  7. Related Content
  8. Comments System
  9. Revision History
  10. Feeds & Distribution
  11. CRM Administration

Blog posts (Post model) are a specialized type of Article that support:

  • Rich content with Liquid templating
  • Structured data extraction (FAQ, HowTo schemas)
  • Author attribution with bio pages
  • Comments and engagement tracking
  • SEO optimization tools
  • Revision history and content versioning
State Description
draft Work in progress, not visible to public
published Live on the website
scheduled Will auto-publish at publish_on date
archived Removed from public view

The blog system uses three distinct date fields with specific meanings:

Field Purpose Public Display
published_at Original publication date Always shown in feeds/schema
revised_at Substantial content revision Shown as “last updated” when present
updated_at Internal tracking (any edit) Never shown publicly
  1. Blog post page:

    • If revised: “last updated [revised_at]”
    • If not revised: “published on [published_at]”
    • Note: Original publication date is preserved in JSON-LD/Atom for SEO but not shown visually
  2. Blog listings: Show revised_at if present, otherwise published_at

  3. Atom feed:

    • <published>: Always published_at
    • <updated>: revised_at if present, otherwise published_at
  4. JSON-LD BlogPosting:

    • datePublished: Always published_at
    • dateModified: revised_at if present, otherwise published_at
  5. Open Graph meta tags:

    • article:published_time: Always present
    • article:modified_time: Only present when revised_at is set

In the CRM editor:

  • Manual: Set the “Revised at” date picker
  • Automatic: Check “Automatically update revised at” when saving (updates to current time)

Field Description Character Limit
subject Main title (H1 on page) 70 characters
title Page title (browser tab) 65 characters
description Summary/excerpt No limit (keep concise)
solution Main content body (HTML/Liquid) No limit
meta_description SEO description override 160 characters recommended
meta_keywords SEO keywords override Optional

Breadcrumbs provide context for where the blog post fits in the site hierarchy.

Format options:

  • Simple path: /floor-heating
  • With custom title: Radiant Floor Heating@/floor-heating

Rules:

  • Must start with /
  • Auto-sorted by specificity (general → specific)
  • First public tag used if no breadcrumb defined

Tags categorize blog posts. Public tags appear in the URL structure.

Public tags (appear in navigation):

company-news, countertop-heaters, design-trends, example-projects,
general-information, heat-tape-for-pipes, indoor-heating, installation,
led-mirrors, mirror-defoggers, outdoor-heating, press-industry-report,
press-release, product-information, radiant-floor-heating, radiant-panels,
remodeling, roof-and-gutter-deicing, troubleshooting, share-your-story,
shower-kits, snow-melting, towel-warmers, trade-professionals

The system automatically generates:

  1. BlogPosting (via PostPresenter)

    • Title, description, author (Person with profile URL when available, else display name, else publisher org)
    • Publisher (Organization), inLanguage
    • datePublished and dateModified
    • wordCount (integer, schema.org camelCase), article body, featured image
  2. Breadcrumb (via formatted_breadcrumb helper)

    • One JSON-LD BreadcrumbList per page. Blog show renders breadcrumb only inside _post.html.erb (a duplicate hidden breadcrumb was removed — it had caused two identical BreadcrumbList blocks).

The BlogSchemaExtractor service uses AI (GPT-4) to extract:

Schema Type When Extracted
FAQPage Q&A content (unless embedded FAQ exists)
HowTo Step-by-step instructions, tutorials

Note: Article/BlogPosting schemas are NOT extracted (already handled).

In the CRM “Advanced” tab, you can add custom JSON-LD:

[
{ "@type": "FAQPage", "mainEntity": [...] },
{ "@type": "HowTo", "name": "How to Install...", "step": [...] }
]
<meta property="article:published_time" content="...">
<meta property="article:modified_time" content="..."> <!-- Only if revised -->
<meta property="article:author" content="...">
<meta property="article:section" content="...">
<meta property="article:tag" content="...">
<meta property="og:updated_time" content="..."> <!-- Only if revised -->

Available Liquid tags for blog content:

Embeds FAQ with inline JSON-LD schema.

{% faq 1234 %}
{% faq 1234,5678,9012 %}

Embeds a video player.

{% video 123 %}

Embeds Cloudflare Stream video.

{% cloudflare_video abc123def %}

Responsive image with proper sizing.

{% image 456 width:800 alt:"Description" %}

Interactive calculator widget.

{% floor_heating_calculator %}

Interactive snow melt calculator.

{% snow_melting_calculator %}

Include shared content partials.

{% partial "shared/cta_block" %}

  • Maximum 65 characters
  • Displayed with check/ban icon in CRM
Option Description
Sanitize URLs Fixes broken links, adds https scheme
Sanitize HTML Cleans inline styles, adds Bootstrap classes
Auto-update revised_at Updates revision date on save

Enable has_toc to auto-generate navigation from headings:

  • Default selector: h2
  • Custom selector via toc_selector field

The CRM shows an SEO overview panel with:

  • Site visits (30 days)
  • Ranking keywords count
  • Health score (AI-generated)
  • Quick links to locale-specific SiteMap details

Specify up to 4 related posts in the CRM editor.

The system uses semantic embeddings to find similar content:

  • Based on title, body, and product associations
  • Product context from breadcrumbs

Blog posts support moderated comments:

  • Comments stored in post_comments table
  • Accessible via post.post_comments
  • Count displayed in CRM
  • Moderation interface at /posts/:id/post_comments

Published posts support content revisions:

  1. Automatic versioning on significant edits
  2. Preview any historical revision
  3. Restore to previous version (creates new revision)
  4. Change notes for audit trail

Access via CRM post show page “Revision History” panel.


Available at /posts.atom

  • Paginated with next/prev links
  • Entry includes full content
  • Proper published and updated timestamps

Each post auto-generates a Source for referral tracking:

  • Short URL via Bitly
  • Short URL with referral code
  • Source analytics integration

Posts integrate with Cloudflare edge caching:

  • Auto-purge on publish/update
  • Tag-based bulk purging available
  • Purges related pages (index, tag pages)

Tab Contents
Summary Title, author, dates, preview image
Content Rich text editor (TinyMCE)
Tagging Breadcrumbs, tags, meta fields
Related Up to 4 related post links
Advanced Content processing, TOC, schema markup, inline JS
Product Lines Associated products for context
  • Preview: View unpublished posts (token-based, 1-hour expiry)
  • Publish/Unpublish: Change state
  • Duplicate: Create copy as draft
  • Extract Schema: Run AI schema extraction
  • Purge Cache: Clear CDN cache
  • Tag-based cache purge: Post.purge_all_posts_by_tag
  • Schema extraction worker: BlogSchemaExtractionWorker
  • Auto extraction: AutoBlogSchemaExtractionWorker

  1. Title: Keep under 65 characters for full display in search results
  2. Summary: Compelling excerpt that works as meta description
  3. Images: Always include a preview image for social sharing
  4. Breadcrumbs: Set appropriate context for SEO
  1. Revision dates: Use for substantial updates, not typo fixes
  2. Schema markup: Let AI extract, manually add only if needed
  3. Related posts: Link to relevant content to reduce bounce
  4. Tags: Use public tags for proper categorization
  1. Review and update evergreen content periodically
  2. Set revised_at when making substantial updates
  3. This signals freshness to search engines and users

Key associations:

belongs_to :preview_image, class_name: 'Image', optional: true
belongs_to :source, optional: true
belongs_to :original_author, class_name: 'Employee', optional: true
has_many :post_comments, dependent: :destroy
has_many :site_maps, as: :resource

Key methods:

post.effective_published_date # published_at || created_at
post.effective_revised_date # revised_at || published_at
post.primary_tag # First tag
post.main_public_tag # First public tag
post.content_for_embedding # Text for semantic search
Service Purpose
BlogSchemaExtractor AI-powered schema extraction
PostPresenter View presentation logic, JSON-LD generation
BlogPreviewTokenService Secure preview URLs
Sitemap::SitemapGenerator XML sitemap integration

Last updated: January 2026