JSON Feed 与 Podcast Feed 构建实践

JSON Feed 让订阅变得现代化,Podcast Feed 连接音频与听众,Hugo 通过统一模板让多格式订阅变得简单而强大。

多格式订阅概述

在内容订阅领域,传统的 RSS/Atom 格式虽然成熟稳定,但 JSON Feed 以其现代化的数据结构和更好的解析性能,正在成为越来越多应用的选择。对于播客内容,Podcast Feed(基于 RSS 的扩展)则是行业标准。本站通过 Hugo 的灵活模板系统,同时提供这两种订阅格式,确保内容在各种平台和应用上都能被正确消费。

订阅格式演进

内容订阅格式经历了从 RSS 到 Atom,再到现代 JSON Feed 的演进:

格式发布时间主要特点适用场景优势局限性
RSS 2.02002XML 结构化,扩展性强通用内容订阅标准化程度高,支持丰富解析复杂度较高
Atom2005XML 标准化,语义明确专业内容发布规范性强,语义清晰学习成本较高
JSON Feed 1.12017JSON 现代化,解析简单现代 Web 应用性能优异,易于处理标准化程度较低
表 1: 内容订阅格式对比

多格式同步策略

Hugo 通过统一的 Front Matter 数据源和模板系统,实现多格式订阅的同步生成:

图 1: Hugo 多格式订阅生成流程
图 1: Hugo 多格式订阅生成流程

JSON Feed 构建实践

JSON Feed 1.1 规范提供了一种现代化的订阅格式,特别适合现代 Web 应用和移动端消费。

JSON Feed 规范结构

JSON Feed 的核心结构包含 feed 信息和 items 列表:

{
  "version": "https://jsonfeed.org/version/1.1",
  "title": "Feed 标题",
  "home_page_url": "https://example.com/",
  "feed_url": "https://example.com/feed.json",
  "description": "Feed 描述",
  "author": {
    "name": "作者姓名",
    "url": "https://example.com/author",
    "avatar": "https://example.com/avatar.jpg"
  },
  "items": [
    {
      "id": "唯一标识符",
      "url": "内容页面 URL",
      "title": "内容标题",
      "content_text": "纯文本内容",
      "content_html": "HTML 内容",
      "summary": "内容摘要",
      "date_published": "2025-01-01T00:00:00Z",
      "tags": ["标签 1", "标签 2"]
    }
  ]
}

Hugo JSON Feed 模板实现

本站的 layouts/index.json 模板实现了完整的 JSON Feed 1.1 支持:

{{- $.Scratch.Add "pagesIndex" slice -}}
{{- range $index, $page := .Site.Pages -}}
  {{- if in (slice "blog" "notice" "book" "trans" "podcast" "ai") $page.Section -}}
    {{- if and (not .Draft) (not .Params.private) | and (ne .Params.searchable false) -}}
      {{- if gt (len $page.Content) 0 -}}
        {{- /* Generate page description. */ -}}
        {{- $desc := "" -}}
        {{- if .Params.description -}}
          {{- $desc = .Params.description -}}
        {{- else -}}
          {{- $desc = .Summary -}}
        {{- end -}}
        {{- /* Add page data including the date, type, tags and categories fields */ -}}
        {{- $pageData := (dict
          "title" $page.Title
          "relpermalink" $page.RelPermalink
            "summary" (
            (
              $desc
              | plainify
            )
            )
            "content" (
            (
              $page.Content
              | replaceRE "(?is)<!--.*?-->" ""
              | replaceRE "(?is)<script[^>]*>.*?</script>" ""
              | replaceRE "(?is)<details[^>]*>.*?</details>" ""
              | replaceRE "(?is)<div[^>]*class=\"highlight\"[^>]*>.*?</div>" ""
              | replaceRE "(?is)<style[^>]*>.*?</style>" ""
              | replaceRE "(?is)<pre[^>]*>.*?</pre>" ""
              | replaceRE "(?is)<figure[^>]*>.*?</figure>" ""
              | replaceRE "(?is)<nav[^>]*>.*?</nav>" ""
              | replaceRE "(?is)<aside[^>]*>.*?</aside>" ""
              | replaceRE "(?is)<footer[^>]*>.*?</footer>" ""
              | plainify
              | htmlEscape
              | truncate 200
            )
            )
          "date" $page.Date
          "section" $page.Section
          "tags" .Params.tags
          "categories" .Params.categories
        ) -}}
        {{- $.Scratch.Add "pagesIndex" $pageData -}}
      {{- end -}}
    {{- end -}}
  {{- end -}}
{{- end -}}
{{- $.Scratch.Get "pagesIndex" | jsonify -}}

关键实现特性

  1. 内容过滤:移除 HTML 标签、脚本、样式,保留纯文本
  2. 长度控制:内容截断为 200 字符,优化加载性能
  3. 类型筛选:只索引指定章节的内容
  4. 隐私保护:跳过草稿和私有内容

JSON Feed 增强功能

基于 JSON Feed 规范的扩展支持:

字段名称描述Hugo 实现应用场景
attachments附件信息播客音频文件Podcast Feed
external_url外部链接文章引用链接内容扩展
cover横幅图片特色图片视觉增强
authors多作者信息作者数组协作内容
表 2: JSON Feed 增强字段支持

Podcast Feed 构建实践

播客内容需要特殊的 RSS 扩展来支持音频文件和播客特有的元数据。

Podcast RSS 规范要求

Apple Podcasts 和其他播客平台对 RSS Feed 有特定要求:

<rss xmlns:itunes="http://www.itunes.com/dtds/podcast-1.0.dtd" version="2.0">
  <channel>
    <!-- 频道级别元数据 -->
    <title>播客标题</title>
    <description>播客描述</description>
    <link>https://example.com/podcast</link>
    <language>zh-cn</language>
    <copyright>版权信息</copyright>

    <!-- iTunes 特定标签 -->
    <itunes:author>主持人姓名</itunes:author>
    <itunes:image href="封面图片URL"/>
    <itunes:category text="分类名称"/>
    <itunes:explicit>no</itunes:explicit>
    <itunes:owner>
      <itunes:name>所有者姓名</itunes:name>
      <itunes:email>联系邮箱</itunes:email>
    </itunes:owner>
  </channel>
</rss>

Hugo Podcast RSS 模板实现

本站的 layouts/podcast/rss.xml 模板实现了完整的播客 RSS 规范:

<rss xmlns:itunes="http://www.itunes.com/dtds/podcast-1.0.dtd"
     xmlns:atom="http://www.w3.org/2005/Atom"
     xmlns:content="http://purl.org/rss/1.0/modules/content/" version="2.0">
  <channel>
    {{/* Get podcast params from site config, section front matter, or defaults */}}
    {{ $podcastParams := .Site.Params.podcast | default dict }}
    {{ $sectionParams := .Params | default dict }}

    {{ $podcastTitle := $sectionParams.podcast_title | default ($podcastParams.title | default .Site.Title) }}
    {{ $podcastLink := $sectionParams.podcast_website_url | default ($podcastParams.website_url | default .Permalink) }}
    {{ $podcastDescription := $sectionParams.podcast_description | default ($podcastParams.description | default .Site.Params.description) }}
    {{ $languageCode := $sectionParams.podcast_language_code | default ($podcastParams.language_code | default .Site.Language.Lang | default .Site.LanguageCode) }}
    {{ $copyrightText := $sectionParams.podcast_copyright_text | default ($podcastParams.copyright_text | default .Site.Copyright) }}
    {{ $podcastAuthor := $sectionParams.podcast_author | default ($podcastParams.author | default .Site.Params.author) }}
    {{ $podcastCoverImage := $sectionParams.podcast_cover_image_url | default $podcastParams.cover_image_url }}
    {{ $podcastExplicit := $sectionParams.podcast_explicit | default ($podcastParams.explicit | default "no") }}
    {{ $podcastType := $sectionParams.podcast_type | default ($podcastParams.type | default "episodic") }}
    {{ $ownerName := $sectionParams.podcast_owner_name | default ($podcastParams.owner_name | default $podcastAuthor) }}
    {{ $ownerEmail := $sectionParams.podcast_owner_email | default ($podcastParams.owner_email | default .Site.Params.email) }}
    {{ $categories := $sectionParams.podcast_categories | default $podcastParams.categories }}

    <title>{{ $podcastTitle }}</title>
    <link>{{ $podcastLink }}</link>
    <description>{{ $podcastDescription }}</description>
    <generator>Hugo -- gohugo.io</generator>
    {{ with $languageCode }}<language>{{ . }}</language>{{ end }}
    {{ with $copyrightText }}<copyright>{{ . }}</copyright>{{ end }}
    {{ if not .Date.IsZero }}<lastBuildDate>{{ .Date.Format "Mon, 02 Jan 2006 15:04:05 GMT" | safeHTML }}</lastBuildDate>{{ else }}<lastBuildDate>{{ now.Format "Mon, 02 Jan 2006 15:04:05 GMT" | safeHTML }}</lastBuildDate>{{ end }}

    <atom:link href="{{ .Site.BaseURL | relURL }}/podcast/index.xml" rel="self" type="application/rss+xml"/>

    {{ with $podcastCoverImage }}<itunes:image href="{{ . | absURL }}"/>{{ end }}
    {{ if $categories }}
      {{ range $categories }}
        <itunes:category text="{{ . }}"/>
      {{ end }}
    {{ else }}
      {{/* Default category if none provided */}}
      <itunes:category text="Society & Culture"/>
    {{ end }}
    <itunes:explicit>{{ $podcastExplicit }}</itunes:explicit>
    {{ with $podcastAuthor }}<itunes:author>{{ . }}</itunes:author>{{ end }}
    <itunes:owner>
      {{ with $ownerName }}<itunes:name>{{ . }}</itunes:name>{{ end }}
      {{ with $ownerEmail }}<itunes:email>{{ . }}</itunes:email>{{ end }}
    </itunes:owner>
    <itunes:type>{{ $podcastType }}</itunes:type>
  </channel>
</rss>

单集内容处理

每集播客的详细元数据处理:

{{ range where $pages "Params.audio_url" "!=" nil }}
<item>
  <title>{{ .Title | plainify }}</title>
  {{ $episodeLink := .Params.episode_link | default .Permalink }}
  <link>{{ $episodeLink }}</link>
  <pubDate>{{ .Date.Format "Mon, 02 Jan 2006 15:04:05 GMT" | safeHTML }}</pubDate>
  {{ $itemGuid := .Params.guid | default .Permalink }}
  {{ $isPermaLink := eq (.Params.guid | default "") "" }}
  <guid isPermaLink="{{ $isPermaLink }}">{{ $itemGuid }}</guid>

  {{ if and .Params.audio_url .Params.audio_length .Params.audio_type }}
  <enclosure url="{{ .Params.audio_url | absURL }}" length="{{ .Params.audio_length }}" type="{{ .Params.audio_type }}"/>
  {{ end }}

  {{ $descriptionContent := .Summary }}
  {{ if .Params.description }}
    {{ $descriptionContent = .Params.description }}
  {{ end }}
  {{ $description := $descriptionContent | default .Summary | default .Content }}
  {{ $description = replaceRE "(?is)<!--.*?-->" "" $description }}
  {{ $description = replaceRE "(?is)<script[^>]*>.*?</script>" "" $description }}
  {{ $description = replaceRE "(?is)<details[^>]*>.*?</details>" "" $description }}
  {{ $description = $description | plainify | truncate 500 | htmlEscape }}
  {{ $description = replaceRE `]]>` "]]&gt;" $description }}
  <description>{{ $description }}</description>

  {{ $encodedContent := .Content }}
  {{ if .Params.content_encoded }}
    {{ $encodedContent = .Params.content_encoded }}
  {{ end }}
  {{ $encodedContent = replaceRE "(?is)<!--.*?-->" "" $encodedContent }}
  {{ $encodedContent = replaceRE "(?is)<script[^>]*>.*?</script>" "" $encodedContent }}
  {{ $encodedContent = replaceRE "(?is)<details[^>]*>.*?</details>" "" $encodedContent }}
  <content:encoded>{{ $encodedContent }}</content:encoded>

  {{ with .Params.itunes_episode_type }}<itunes:episodeType>{{ . }}</itunes:episodeType>{{ end }}
  {{ with .Params.itunes_episode_number }}<itunes:episode>{{ . }}</itunes:episode>{{ end }}
  {{ with .Params.itunes_season }}<itunes:season>{{ . }}</itunes:season>{{ end }}

  {{ with .Params.itunes_keywords }}
    <itunes:keywords>
      {{ range $index, $keyword := split . "," }}
        {{ if $index }},{{ end }}{{ $keyword | trim " \t\n\r" | safeHTML }}
      {{ end }}
    </itunes:keywords>
  {{ end }}

  {{ with .Params.duration }}<itunes:duration>{{ . }}</itunes:duration>{{ end }}
  {{ $episodeImage := .Params.episode_image_url | default $podcastCoverImage }}
  {{ with $episodeImage }}<itunes:image href="{{ . | absURL }}"/>{{ end }}

  {{ $itemAuthor := .Params.itunes_author_item | default $podcastAuthor }}
  {{ with $itemAuthor }}<itunes:author>{{ . }}</itunes:author>{{ end }}

  {{ $itemExplicit := .Params.itunes_explicit_item | default $podcastExplicit }}
  <itunes:explicit>{{ $itemExplicit }}</itunes:explicit>
</item>
{{ end }}

Front Matter 数据结构

播客内容的 Front Matter 结构:

---
title: "北京小客车摇号深度探讨"
date: 2025-05-18 00:12:03+00:00
draft: false
description: "关于摇号政策的深度分析..."
categories:
- Jimmy 的播客
audio_url: https://assets.jimmysong.io/podcasts/audios/ep-001.mp3
audio_type: audio/mpeg
duration: 486
audio_length: '4533959'
episode_image_url: https://assets.jimmysong.io/podcasts/images/ep-001.jpg
---
字段类型必需说明
audio_urlstring音频文件完整 URL
audio_typestringMIME 类型(如 audio/mpeg)
audio_lengthstring文件字节数
durationnumber时长(秒)
episode_image_urlstring集封面图片
itunes_episodenumber集号
itunes_seasonnumber季号
itunes_keywordsstring关键词(逗号分隔)
表 3: 播客 Front Matter 字段说明

多格式同步策略

统一元数据管理

通过 Hugo 的 Front Matter 系统实现多格式同步:

图 2: 多格式订阅元数据同步
图 2: 多格式订阅元数据同步

内容处理策略

不同格式对内容的处理要求:

格式内容处理长度限制HTML 支持特殊字符处理
JSON Feed保留纯文本,移除 HTML 标签200 字符摘要content_html 字段JSON 转义
Podcast RSS支持 HTML,content:encoded500 字符摘要完整 HTMLCDATA 封装
Atom FeedXML 结构化内容无限制标准 XMLXML 实体编码
表 4: 多格式内容处理策略

构建流程集成

多格式输出的构建配置:

[outputs]
# 首页输出多种格式
home = ["HTML", "RSS", "JSON"]
# 播客章节专门输出
podcast = ["HTML", "RSS"]
# 博客章节输出标准格式
blog = ["HTML", "RSS", "JSON"]

验证与调试

Feed 验证工具

工具名称适用格式验证类型网址
W3C Feed ValidatorRSS/Atom语法结构https://validator.w3.org/feed/
JSON Feed ValidatorJSON Feed格式规范https://validator.jsonfeed.org/
Cast Feed ValidatorPodcast RSS播客标准https://castfeedvalidator.com/
Feed Validator多格式通用验证https://www.feedvalidator.org/
表 5: 订阅格式验证工具

常见问题诊断

# 检查 JSON Feed 语法
curl -s "https://example.com/index.json" | jq .

# 验证 RSS 结构
curl -s "https://example.com/podcast/index.xml" | xmllint --format -

# 检查音频文件可访问性
curl -I "https://assets.example.com/audio/ep001.mp3"

性能优化建议

  1. 内容缓存:对大型 Feed 实施适当的缓存策略
  2. 渐进式加载:考虑分页或分批加载大量内容
  3. 压缩传输:启用 gzip 压缩减少传输大小
  4. CDN 分发:使用 CDN 加速 Feed 文件访问

平台分发策略

Apple Podcasts 提交

<!-- Apple Podcasts 要求的核心标签 -->
<itunes:category text="Technology"/>
<itunes:image href="https://example.com/podcast-cover.jpg"/>
<itunes:explicit>no</itunes:explicit>
<enclosure url="https://example.com/ep001.mp3"
           length="4533959"
           type="audio/mpeg"/>

其他平台适配

平台RSS 要求特殊配置审核周期
Apple Podcasts完整 iTunes 标签分类、封面、时长24-48 小时
Spotify标准 RSS + iTunes字幕文件支持即时
Pocket Casts基本 RSS 字段无特殊要求即时
小宇宙标准 RSS中文优化即时
表 6: 主流播客平台要求

最佳实践

内容质量保证

  1. 元数据完整性:确保所有必需字段都已填写
  2. 音频文件优化:使用合适的比特率和格式
  3. 描述准确性:提供吸引人的节目描述
  4. 时长标记:准确记录每集时长

技术维护

  1. 定期验证:使用验证工具检查 Feed 格式
  2. 链接监控:确保所有音频文件链接有效
  3. 更新同步:及时更新各平台的 Feed URL
  4. 备份策略:保存重要的元数据和音频文件

用户体验优化

  1. 渐进式增强:提供基本的 RSS,同时支持现代格式
  2. 跨平台兼容:确保在各种播客客户端正常工作
  3. 离线可用性:音频文件支持离线下载
  4. 搜索友好:提供丰富的元数据便于搜索

通过精心设计的多格式订阅系统,本站不仅满足了传统 RSS 用户的需求,还为现代 Web 应用和播客平台提供了优质的内容分发体验。Hugo 的灵活模板系统让同时维护多种订阅格式变得简单而高效。

总结

JSON Feed 和 Podcast Feed 是现代内容订阅生态的重要组成部分。JSON Feed 以其简洁高效的 JSON 结构成为现代 Web 应用的理想选择,而 Podcast Feed 则通过 RSS 的扩展满足了播客内容复杂的元数据需求。

Hugo 通过统一的 Front Matter 系统和灵活的模板机制,让同时维护这两种订阅格式变得简单而高效。本站的实现展示了如何通过精心设计的模板系统,在保证内容质量的同时,为用户提供多样化的订阅体验。

关键在于理解不同格式的规范要求,合理组织元数据,并在性能和功能之间取得最佳平衡。随着内容订阅生态的不断演进,这些多格式支持的能力将成为现代静态网站的重要竞争力。

参考文献