前言
本想着重新做一个完整的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子模块方式安装。
在博客根目录执行:
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
初始化时指定的配置。
{{ 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 */
/* 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:00draft: falseshowTableOfContents: 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时间流了。这样即使是静态博客,也可以实时获取到最新嘟文信息。