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
StateDescription
draftWork in progress, not visible to public
publishedLive on the website
scheduledWill auto-publish at publish_on date
archivedRemoved from public view

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

FieldPurposePublic Display
published_atOriginal publication dateAlways shown in feeds/schema
revised_atSubstantial content revisionShown as “last updated” when present
updated_atInternal 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)

FieldDescriptionCharacter Limit
subjectMain title (H1 on page)70 characters
titlePage title (browser tab)65 characters
descriptionSummary/excerptNo limit (keep concise)
solutionMain content body (HTML/Liquid)No limit
meta_descriptionSEO description override160 characters recommended
meta_keywordsSEO keywords overrideOptional

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 TypeWhen Extracted
FAQPageQ&A content (unless embedded FAQ exists)
HowToStep-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
OptionDescription
Sanitize URLsFixes broken links, adds https scheme
Sanitize HTMLCleans inline styles, adds Bootstrap classes
Auto-update revised_atUpdates 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)

TabContents
SummaryTitle, author, dates, preview image
ContentRich text editor (TinyMCE)
TaggingBreadcrumbs, tags, meta fields
RelatedUp to 4 related post links
AdvancedContent processing, TOC, schema markup, inline JS
Product LinesAssociated 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
ServicePurpose
BlogSchemaExtractorAI-powered schema extraction
PostPresenterView presentation logic, JSON-LD generation
BlogPreviewTokenServiceSecure preview URLs
Sitemap::SitemapGeneratorXML sitemap integration

Last updated: January 2026