2902 words
15 minutes
Hugo | 在博客中嵌入Mastodon时间流

前言#

本想着重新做一个完整的NeoDB书影音页面,在实践途中因为图片处理太复杂转而被其他地方迷住了眼,Mastodon时间流就是其中一个副产物。

本文借鉴eallion的代码,对Timeline样式做了Blowfish主题适配,并且添加了实时监听Hugo当前明暗色主题状态来自动切换Timeline的明暗色。对于一些细节,你可以参考这篇:Hugo博客集成Mastodon

示例:Toots

导入mastodon-embed-timeline包#

原项目在Gitlab,官方的安装教程在这里。简单来说就是有三种办法安装,分别是下载本地/CDN/导入npm包,hugo能用前两种。涉及到的文件是一个css文件mastodon-timeline.min.css和一个js文件mastodon-timeline.umd.js

因为我自己没用过Gitlab,为了方便维护我采用eallion做的官方Gitlab仓库的Github镜像,用git子模块方式安装。

eallion
/
mastodon-embed-timeline
Waiting for api.github.com...
00K
0K
0K
Waiting...

在博客根目录执行:

git submodule add https://github.com/eallion/mastodon-embed-timeline.git assets/lib/mastodon-embed-timeline

mastodon-embed-timeline下载到博客的assets/lib这个目录,这个目录可以随便选,只要记得这个路径就可以了。

注意事项

在另外的电脑上Clone博客后,要执行git submodule update --init --recursive才能同步克隆子模块。 子模板有更新时,执行git submodule update --remote --merge更新子模块。

使用#

在博客的根目录的layouts/_default下(没有可以创建),新建一个toots.html文件,并修改下方代码中myTimeline初始化时指定的配置。

toots.html
{{ define "main" }}
{{ .Scratch.Set "scope" "single" }}
{{ $mastodonCss := resources.Get "lib/mastodon-embed-timeline/dist/mastodon-timeline.min.css" | minify | fingerprint "sha256" }}
<link rel="stylesheet" href="{{ $mastodonCss.RelPermalink }}" integrity="{{ $mastodonCss.Data.Integrity }}" crossorigin="anonymous">
{{ $mastodonCustomCss := resources.Get "css/mastodon-timeline-custom.scss" | toCSS | minify | fingerprint "sha256" }}
<link rel="stylesheet" href="{{ $mastodonCustomCss.RelPermalink }}" integrity="{{ $mastodonCustomCss.Data.Integrity }}" crossorigin="anonymous">
<article>
{{ if .Params.showHero | default (.Site.Params.article.showHero | default false) }}
{{ $heroStyle := .Params.heroStyle }}
{{ if not $heroStyle }}{{ $heroStyle = .Site.Params.article.heroStyle }}{{ end }}
{{ $heroStyle := print "hero/" $heroStyle ".html" }}
{{ if templates.Exists ( printf "partials/%s" $heroStyle ) }}
{{ partial $heroStyle . }}
{{ else }}
{{ partial "hero/basic.html" . }}
{{ end }}
{{ end }}
<header id="single_header" class="mt-5 max-w-prose">
{{ if .Params.showBreadcrumbs | default (.Site.Params.article.showBreadcrumbs | default false) }}
{{ partial "breadcrumbs.html" . }}
{{ end }}
<h1 class="mt-0 text-4xl font-extrabold text-neutral-900 dark:text-neutral">
{{ .Title }}
</h1>
<div class="mt-1 mb-6 text-base text-neutral-500 dark:text-neutral-400 print:hidden">
{{ partial "article-meta/basic.html" (dict "context" . "scope" "single") }}
</div>
</header>
<section class="flex flex-col max-w-full mt-0 prose dark:prose-invert lg:flex-row">
<div class="min-w-0 min-h-0 max-w-fit">
<div class="article-content max-w-full mb-20">
{{ .Content }}
<div id="mt-container"></div>
</div>
</div>
</section>
</article>
{{ $js := resources.Get "lib/mastodon-embed-timeline/dist/mastodon-timeline.esm.js" }}
<script type="module">
import * as MastodonTimeline from '{{ $js.RelPermalink }}';
// 检测Hugo博客的当前主题
function getCurrentHugoTheme() {
const isDark = document.documentElement.classList.contains('dark');
return isDark ? 'dark' : 'light';
}
const myTimeline = new MastodonTimeline.Init({
// Id of the <div> containing the timeline
// 包含时间轴的 <div> 的 ID
mtContainerId: "mt-container",
// Mastodon instance Url including https://
// Mastodon 实例地址,需包含 https://
instanceUrl: "https://instance.site",
// Choose type of posts to show in the timeline: 'local', 'profile', 'hashtag'
// Default: local
// 选择在时间轴中显示的嘟文类型:'local'(本地)、'profile'(个人)、'hashtag'(话题标签)
// 默认值:local,推荐使用 profile 因为其他人并不一定愿意将嘟文展示到你的网站
timelineType: "profile",
// Your user ID number on Mastodon instance
// Leave it empty if you didn't choose 'profile' as type of timeline
// Mastodon 实例上的用户 ID 号
// 如果没有选择 'profile' 作为时间轴类型,则留空
userId: "1141xxxxxxxxxxx",
// Your user name on Mastodon instance (including the @ symbol at the beginning)
// Leave it empty if you didn't choose 'profile' as type of timeline
// Mastodon 实例上的用户名(包括开头的 @ 符号)
// 如果没有选择 'profile' 作为时间轴类型,则留空
profileName: "@username",
// The name of the hashtag (not including the # symbol)
// Leave it empty if you didn't choose 'hashtag' as type of timeline
// 话题标签名称(不包括 # 符号)
// 如果没有选择 'hashtag' 作为时间轴类型,则留空
hashtagName: "",
// Class name for the loading spinner (also used in CSS file)
// 加载动画的 CSS 类名(在 CSS 文件中也会用到)
spinnerClass: "mt-loading-spinner",
// Preferred color theme: 'light', 'dark' or 'auto'
// Default: auto
// 首选颜色主题:'light'(亮色)、'dark'(暗色) 或 'auto'(自动)
// 默认值:auto
// 使用Hugo博客的当前主题,而不是'auto'
defaultTheme: getCurrentHugoTheme(),
// Maximum number of posts to request to the server
// Default: 20
// 向服务器请求的最大嘟文数量
// 默认值:20
maxNbPostFetch: "40",
// Maximum number of posts to show in the timeline
// Default: 20
// 在时间轴中显示的最大嘟文数量
// 默认值:20
maxNbPostShow: "20",
// Specifies the format of the date according to the chosen language/country
// Default: British English (day-month-year order)
// 根据所选语言/国家指定日期格式
// 默认值:英式英语(日 - 月 - 年顺序)
dateLocale: "zh-CN",
// Customize the date format using the options
// Default: DD MMM YYYY
// 使用选项自定义日期格式
// 默认值:DD MMM YYYY
dateOptions: {
day: "2-digit", // 日期
month: "short", // 月份
year: "numeric", // 年份
},
// Hide unlisted posts
// Default: don't hide
// 隐藏未公开的嘟文
// 默认值:不隐藏,如果你经常发非公开嘟文并想展示在此,需要关闭此功能。
// 非公开的意思是unlisted,本质也是公开嘟文。锁嘟不算在此列,因此你可以自由选择开启或关闭
hideUnlisted: false,
// Hide boosted posts
// Default: don't hide
// 隐藏转发的嘟文
// 默认值:不隐藏
hideReblog: true,
// Hide replies posts
// Default: don't hide
// 隐藏回复的嘟文
// 默认值:不隐藏
hideReplies: true,
// Hide pinned posts from the profile timeline
// Default: don't hide
// 在个人资料时间轴中隐藏置顶嘟文
// 默认值:不隐藏
hidePinnedPosts: true,
// Hide the user account under the user name
// Default: don't hide
// 隐藏用户名下方的账号信息
// 默认值:不隐藏
hideUserAccount: false,
// Show only posts with the selected language (ISO 639-1)
// Use "en" to show only posts in English
// Default: "" (don't filter by language)
// 仅显示选定语言的嘟文(ISO 639-1 标准)
// 使用"en"只显示英文嘟文
// 默认值:""(不按语言过滤)
filterByLanguage: "",
// Limit the text content to a maximum number of lines
// Use "0" to show no text
// Default: "" (unlimited)
// 限制文本内容的最大行数
// 使用"0"表示不显示文本
// 默认值:""(无限制)
txtMaxLines: "",
// Customize the text of the button used for showing/hiding sensitive/spoiler text
// 自定义显示/隐藏敏感内容/剧透文本的按钮文字
btnShowMore: "SHOW MORE",
btnShowLess: "SHOW LESS",
// Converts Markdown symbol ">" at the beginning of a paragraph into a blockquote HTML tag
// Default: false (don't apply)
// 将段落开头的 Markdown 符号 ">" 转换为 blockquote HTML 标签
// 默认值:false(不转换)
markdownBlockquote: false,
// Hide custom emojis available on the server
// Default: don't hide
// 隐藏服务器上可用的自定义表情符号
// 默认值:不隐藏
hideEmojos: false,
// Customize the text of the button used for showing a sensitive/spoiler media content
// 自定义显示敏感内容/剧透媒体内容的按钮文字
btnShowContent: "SHOW CONTENT",
// Hide video image preview and load video player instead
// Default: don't hide
// 隐藏视频图片预览并直接加载视频播放器
// 默认值:不隐藏
hideVideoPreview: true,
// Customize the text of the button used for the image preview to play the video
// 自定义图片预览中用于播放视频的按钮文字
btnPlayVideoTxt: "Load and play video",
// Hide preview card if post contains a link, photo or video from a Url
// Default: don't hide
// 如果嘟文包含链接、照片或视频 URL,则隐藏预览卡片
// 默认值:不隐藏
hidePreviewLink: true,
// Limit the preview text description to a maximum number of lines
// Use "0" to show no text
// Default: "" (unlimited)
// 限制预览文本描述的最大行数
// 使用"0"表示不显示文本
// 默认值:""(无限制)
previewMaxLines: "",
// Hide replies, boosts and favourites posts counter
// Default: don't hide
// 隐藏回复、转发和收藏计数器
// 默认值:不隐藏
hideCounterBar: false,
// Show a carousel/lightbox when the user clicks on a picture in a post
// Default: not disabled
// 当用户点击嘟文中的图片时显示轮播/灯箱效果
// 默认值:不禁用
disableCarousel: false,
// Customize the text of the buttons used for the carousel/lightbox
// 自定义轮播/灯箱按钮的文字
carouselCloseTxt: "Close carousel",
carouselPrevTxt: "Previous media item",
carouselNextTxt: "Next media item",
// Customize the text of the button pointing to the Mastodon page placed at the end of the timeline
// Leave the value empty to hide it
// 自定义时间轴底部指向 Mastodon 页面的按钮文字
// 留空则隐藏此按钮
// btnSeeMore: "See more posts at fairy.id",
btnSeeMore: "",
// Customize the text of the button reloading the list of posts placed at the end of the timeline
// Leave the value empty to hide it
// 自定义时间轴底部重新加载嘟文列表的按钮文字
// 留空则隐藏此按钮
// btnReload: "Refresh",
btnReload: "",
// Keep searching for the main <div> container before building the timeline
// Useful in some cases where extra time is needed to render the page
// Default: don't apply
// 在构建时间轴之前持续搜索主 <div> 容器
// 在某些需要额外时间渲染页面的情况下很有用
// 默认值:不启用
insistSearchContainer: true,
// Defines the maximum time to continue searching for the main <div> container
// Default: 3 seconds
// 定义持续搜索主 <div> 容器的最大时间
// 默认值:3 秒
insistSearchContainerTime: "3000",
});
// 监听Hugo主题变化,同步更新Mastodon timeline主题
function syncMastodonTheme() {
const newTheme = getCurrentHugoTheme();
myTimeline.mtColorTheme(newTheme);
}
// 监听 <html class="dark"> 变化
const observer = new MutationObserver(syncMastodonTheme);
observer.observe(document.documentElement, { attributes: true, attributeFilter: ['class'] });
</script>
<noscript>
Please enable JavaScript to view the Mastodon Timeline powered by <a href="https://gitlab.com/idotj/mastodon-embed-timeline" target="_blank">Mastodon embed timeline widget</a>.
</noscript>
{{- end -}}

关于查询Mastodon的userID,可以参考How to find your Mastodon account ID此文。具体来说是调用mastodon API,有两种方法都会返回userID

一是直接浏览器访问:

https://INSTANCE/api/v1/accounts/lookup?acct=USERNAME

二是命令行执行:

curl https://INSTANCE/api/v1/accounts/lookup?acct=USERNAME

除此之外defaultTheme参数本身有三个值可以定义,分别是dark/light/auto,此处auto是获取当前设备的偏好颜色,并不会同步Hugo主题的明暗色,所以我写了个函数syncMastodonTheme直接监听Hugo主题状态。如果你想保留监听功能,那么defaultTheme此项不要更改

项目原生的css样式不是很满意,于是新建assets/css/mastodon-timeline-custom.scss修改一点样式。

mastodon-timeline-custom.scss
/* mastodon timeline custom */
/* Main container */
.mt-container {
container: unset;
background-color: transparent;
}
.mt-body>.mt-loading-spinner {
position: relative;
}
.mt-body {
padding: 0;
}
.mt-body[role="none"] {
min-height: 15rem;
}
.mt-post {
margin: .25rem 0;
padding: 1rem 1.25rem;
}
.mt-post-avatar-standard {
width: 2.75rem;
height: 2.75rem;
}
.mt-post-avatar-image-big img {
margin-top: 0.5rem;
margin-bottom: 0;
width: 2.75rem;
height: 2.75rem;
border-radius: 50%;
}
.mt-post-media>img,
.mt-post-media>video {
max-width: 310px;
max-height: 310px;
position: relative;
top: unset;
left: unset;
transform: unset;
margin-top: 0;
margin-bottom: 0;
}
.mt-post-media {
padding-top: 0 !important;
}
.mt-post-counter-bar-favorites, .mt-post-counter-bar-reblog, .mt-post-counter-bar-replies {
font-size: 1rem;
opacity: 1;
}
[data-theme="light"] .mt-post-header-user-account {
--mt-color-contrast-gray: rgb(var(--color-neutral-500)) !important;
}
[data-theme="dark"] .mt-post-header-user-account {
--mt-color-contrast-gray: rgb(var(--color-neutral-400)) !important;
}
[data-theme="light"] .mt-post-header-date>a {
--mt-color-contrast-gray: rgb(var(--color-neutral-500)) !important;
}
[data-theme="dark"] .mt-post-header-date>a {
--mt-color-contrast-gray: rgb(var(--color-neutral-400)) !important;
}
.mt-post-counter-bar {
--mt-color-contrast-gray: rgb(var(--color-neutral-400)) !important;
}
.mt-post-txt {
color: var(--tw-prose-body) !important;
}
.mt-post-txt .mt-custom-emoji {
margin-bottom: 0.25rem;
display: inline-block;
margin-top: 0;
}
.mt-post-header-user>a {
color: var(--tw-prose-links) !important;
}
// 用户名中的 emoji
.mt-post-header-user-name .mt-custom-emoji {
display: inline-block;
width: 1em;
height: 1em;
vertical-align: text-bottom;
margin: 0 !important;
padding: 0;
line-height: 1;
object-fit: contain;
}
.mt-post-avatar-image-small img {
margin-top: 0;
margin-bottom: 0;
}
/* === 链接颜色适配 Hugo Blowfish 主题 === */
// .mt-post-txt a {
// color: var(--color-primary); // Blowfish 默认主色(亮模式的主链接色)
// text-decoration: underline;
// transition: color 0.2s ease;
// }
// .dark .mt-post-txt a {
// color: var(--color-primary); // Blowfish 暗模式的链接色
// }
/* 链接颜色(亮模式) */
[data-theme="light"] .mt-post-txt a {
--mt-color-link: rgb(var(--color-primary-600));
}
/* 链接颜色(夜间模式) */
[data-theme="dark"] .mt-post-txt a {
--mt-color-link: rgb(var(--color-primary-400));
}
/* === 按钮颜色适配 Hugo Blowfish === */
/* 时间轴底部按钮 */
// .mt-footer .mt-btn-violet,
// .mt-footer a.mt-btn-violet,
// .mt-footer button.mt-btn-violet {
// --mt-color-btn-bg: var(--color-primary-600);
// color: white;
// }
// .mt-footer .mt-btn-violet:hover,
// .mt-footer a.mt-btn-violet:hover,
// .mt-footer button.mt-btn-violet:hover {
// --mt-color-btn-bg-hover: var(--color-primary-700);
// }
/* toot images start */
.toot-images {
display: grid;
gap: 10px;
}
.grid-1 {
grid-template-columns: 150px;
}
.grid-3,
.grid-5,
.grid-6,
.grid-7,
.grid-8,
.grid-9 {
grid-template-columns: repeat(3, 150px);
}
.grid-2,
.grid-4 {
grid-template-columns: repeat(2, 150px);
}
@media (max-width: 540px) {
/* iPhone se width */
.grid-3,
.grid-5,
.grid-6,
.grid-7,
.grid-8,
.grid-9 {
grid-template-columns: repeat(3, 96px);
}
.grid-2,
.grid-4 {
grid-template-columns: repeat(2, 96px);
}
.single {
.content {
figure {
img {
width: 96px;
height: 96px;
}
}
}
}
}
/* toot images end */

在博客项目content目录下新建toots/_index.md。比如我的文件内容是:

---
title: "Toots"
date: 2025-07-03T20:20:37+09:00
draft: false
showTableOfContents: false
---
碎碎念也会装进漂流瓶里。
<div id="mt-container" class="mt-container">
<div class="mt-body" role="feed">
<div class="mt-loading-spinner"></div>
</div>
</div>

保存全部修改之后本地调试hugo server -D,即可在http://localhost:1313/toots/浏览嵌入Hugo博客里的Mastodon时间流了。这样即使是静态博客,也可以实时获取到最新嘟文信息。

Hugo | 在博客中嵌入Mastodon时间流
https://blog.tantalum.life/posts/embed-mastodon-timeline-feed-in-hugo/
Author
Zokiio
Published at
2025-07-04
License
CC BY-NC-SA 4.0