Since I saw the concept of IndieWeb last year, I’ve been wanting to support it on my Zola blog. Before that my blog had never had a commenting system, and this time with Webmention I was finally able to get a static blog to display responses to articles on the web in plain html. I will document how I did it in this article.
The IndieWeb IndieWeb is a community of independent & personal websites connected by simple standards, based on the principles of: owning your domain & using it as your primary identity, publishing on your own site (optionally syndicating elsewhere), and owning your data.
1. Support IndieWeb Auth🔗
IndieAuth is a federated login protocol for Web sign-in, so by enabling this, we can use our own domain to sign in to other sites and services which support IndieAuth. Learn more about why IndieAuth and how to IndieAuth, here’s how I enabled it:
- By editing my index template file, add this to the head:
<link rel="me" href="https://twitter.com/TheOwenYoung" />
<link rel="me" href="https://github.com/theowenyoung" />
<link rel="me" href="mailto:owen@owenyoung.com" />
-
Editing Twitter/GitHub profile bio, add my website URL
https://www.owenyoung.com
-
Test it at https://indielogin.com/
2. Add Profile Info🔗
When you sign in with IndieAuth or using web mention, some sites will try to get your profile info, like name, image, and bio. So we can add this to our homepage, you can learn more about h-card here. Here’s how I added it:
By editing my index template file, add this in the aside
(Cause I don’t want this to affect the layout of my site, so use css display: none
to hide it for human, but the bot will see it):
<div class="display-none h-card pt">
<img class="u-photo icon" alt="Owen"
src="{{get_url(path="site/images/favicon-96x96.png",cachebust=true)}}" />
<a class="p-name u-url" href="{{ config.base_url }}"
>{{ config.extra.author }}</a
>
<p class="p-note">{{ config.extra.bio }}</p>
</div>
3. Joining IndieWeb Webring🔗
A webring (or web ring) is a collection of websites linked together in a circular structure, and usually organized around a specific theme, often educational or social. They were popular in the 1990s and early 2000s, particularly among amateur websites.
Now, IndieWeb has one IndieWeb Webring! by adding webring in our sites, so people can find (and be found by) other folks with IndieWeb building blocks on their sites!
So, I’m in it! By joining this, my blog has been listed in the IndieWeb Webring Directory, you can see I already have a profile there.
It’s easy to join the webring, just click the link, and login with my domain www.owenyoung.com
, then I can get my webring code, my webring code like this:
<a href="https://xn--sr8hvo.ws/%F0%9F%93%AE%F0%9F%86%99%F0%9F%93%A9/previous"
>←</a
>
An IndieWeb Webring 🕸💍
<a href="https://xn--sr8hvo.ws/%F0%9F%93%AE%F0%9F%86%99%F0%9F%93%A9/next">→</a>
Then, I add this code to my homepage aside, you can see it on the aside footer
4. Adding Webmention Response to articles🔗
Webmention is a web standard for mentions and conversations across the web, a powerful building block that is used for a growing federated network of comments, likes, reposts, and other rich interactions across the decentralized social web.
“An @ mention that works across websites; so that you don’t feel immovable from Twitter or Fb.” — Rony Ngala
Learn more about Webmention and How to support webmention
Basically, I use Webmention.io to collect all webmentions about this blog, and then I use Denoflow to cache them to my blog repo, and then I use Zola load_data
function to load them, and render them.
-
First, go webmention.io and create a new account with my domain
www.owenyoung.com
, then I can get my webmention endpoint, and I connected my twitter and github account to the service. -
Second, let other services know your webmention endpoint. Add this to the head:
<link
rel="webmention"
href="https://webmention.io/www.owenyoung.com/webmention"
/>
- Use Denoflow to cache all webmentions to my blog repo
Workflow file(workflows/fetch-webmention.yml
):
Fetch webmention API to get updates, then save to
webmentions
directory.
sources:
- use: fetch
args:
- https://webmention.io/api/mentions.jf2?domain=www.owenyoung.com&per-page=999&token=${{ctx.env.WEBMENTION_TOKEN}}
run: return ctx.result.json()
itemsPath: children
key: "wm-id"
filter:
run: |
const {ensureDir} = await import("https://deno.land/std@0.121.0/fs/mod.ts");
const { dirname } = await import("https://deno.land/std@0.121.0/path/mod.ts");
for(const item of ctx.items){
const id = item["wm-id"];
const target = new URL(item["wm-target"]);
const pathname = target.pathname;
const filename = pathname.slice(1).replace(/\/$/, "");
const filepath = "webmentions/"+filename+".json";
await ensureDir(dirname(filepath));
let webmentionData = {};
try {
const dataString = await Deno.readTextFile(filepath);
webmentionData = JSON.parse(dataString);
} catch (_e) {
// ignore
}
webmentionData[id] = item;
console.log("write file:", filepath);
await Deno.writeTextFile(filepath, JSON.stringify(webmentionData,null,2));
}
return ctx.items.map(()=>true);
Github Workflow file(.github/workflows/denoflow.yml
):
Run denoflow every day at midnight, if there are any new updates, it will create a new pull request.
name: Denoflow
on:
repository_dispatch:
workflow_dispatch:
# push:
# branches:
# - main
schedule:
- cron: "1 0 * * *"
jobs:
denoflow:
runs-on: ubuntu-latest
concurrency: denoflow
steps:
- name: Check out repository code
uses: actions/checkout@v2
- uses: denoland/setup-deno@v1
with:
deno-version: v1.x
- run: make webmention
env:
WEBMENTION_TOKEN: ${{secrets.WEBMENTION_TOKEN}}
continue-on-error: true
- name: chown
run: sudo chown -R $USER:$USER ./
- name: git config
run: git config --global user.name "github-actions[bot]" && git config --global user.email github-actions-bot@users.noreply.github.com
- name: git add
run: git add data && git add webmentions
- run: git status
- id: isChanged
run: git diff-index --cached --quiet HEAD || echo '::set-output name=changed::true'
- name: Create pull request
uses: peter-evans/create-pull-request@v3
if: ${{ steps.isChanged.outputs.changed == 'true' }}
with:
token: ${{ secrets.PERSONAL_TOKEN }}
labels: automerge
add-paths: data,webmentions
commit-message: "chore: new item"
committer: "github-actions[bot] <github-actions-bot@users.noreply.github.com>"
author: "github-actions[bot] <github-actions-bot@users.noreply.github.com>"
branch: new-item
delete-branch: true
title: New item update
Github auto merge workflow file(.github/workflows/auto-merge.yml
):
When a pull request is created, the workflow will automatically merge it if the pull request is from the same author
name: Auto merge
on:
workflow_dispatch:
pull_request_target:
jobs:
auto-approve:
runs-on: ubuntu-latest
steps:
- name: Merge
if: (github.actor=='theowenyoung') && (startsWith(github.head_ref,'new-item'))
uses: "pascalgn/automerge-action@v0.14.3"
env:
GITHUB_TOKEN: "${{ secrets.PERSONAL_TOKEN }}"
MERGE_DELETE_BRANCH: true
MERGE_LABELS: ""
- Add response block to page tempalte
This part may take a while to finish, here is the main page template code, I’ll only show the main part.
Use Zola load_data function to load the webmention data JSON file.
{% set current_webmention_file_name = current_path | trim_end_matches(pat="/")
%} {% set webmention_data =
load_data(path="webmentions"~current_webmention_file_name~".json",required=false)
%}
Show response:
{% if webmention_data %} {% set_global mentions = [] %} {% for ignored, item in
webmention_data %} {% set_global mentions = mentions | concat(with=item) %} {%
endfor %}
<p class="muted text-sm">
{{trans(key="label_response_description",lang=lang)| markdown(inline=true) |
safe}}
</p>
{% for type, items in mentions | group_by(attribute="wm-property") %}
<h3>
{{trans(key="label_"~type,lang=lang)}} ({{items | length}})<a
href="#{{type}}"
id="{{type}}"
class="zola-anchor"
>🔗</a
>
</h3>
{% if type == 'like-of' or type == "repost-of" or type == "bookmark-of" or type
== "follow-of" %}
<ul class="list-none flex items-center flex-wrap">
{% for item in items %} {{ macro::webmention(type=type, item=item) }} {%
endfor %}
</ul>
{% else %}
<ul class="list-none">
{% for item in items %} {{ macro::webmention(type=type, item=item) }} {%
endfor %}
</ul>
{% endif %} {% endfor %} {% endif %}
Then, I add a webmention form, so if people want to submit their mentions, they can directly submit it.
<form
class="webmention-form"
action="https://webmention.io/www.owenyoung.com/webmention"
method="post"
>
<div class="flex items-center flex-wrap pb">
<input type="url" name="source" class="flex-3 mr-sm w-full py-sm px-sm" />
<input type="submit" class="px py-sm" value="Send Webmention" />
</div>
<input type="hidden" name="target" value="{{current_url}}" />
</form>
Optional: I also added an activity page for aggregating all response, the activity template page is here
Cause I don’t have too many mentions, so I use sebastiandedeyne’s mention data as this article’s webmention data.
5. Send webmention when you publish a new article🔗
When we publish a new article, we want to send a webmention to the mentioned links. We can do this by using Denoflow and the Webmention.app API. Webmention.app can check all the mentioned links in the new article and send all webmentions to them. Before Webmention.app can recognize the mentioned links, we need to add some extra microformats2 to our article html. Basically, it’s some html tag class names. These took me quite a few time to update my templates, so now I have supported h-card, h-entry and h-feed. It looks like this:
<article class="h-entry">
<h1 class="p-name">Microformats are amazing</h1>
<p>
Published by
<a class="p-author h-card" href="http://example.com">W. Developer</a> on
<time class="dt-published" datetime="2013-06-13 12:00:00"
>13<sup>th</sup> June 2013</time
>
</p>
<p class="p-summary">In which I extoll the virtues of using microformats.</p>
<div class="e-content">
<p>Blah blah blah</p>
</div>
</article>
I have updated page.html, index.html, taxonomy_single.html and section.html to support these new microformats. I have to say this is the most demanding job, good luck!
Once finished, I went to indiewebify to test if it can recognize my new microformats. It works!
Next, I went to webmention.app to apply for a token. Then I can add a denoflow workflow file to fetch it’s service every day.
Here is the workflow file(workflows/send-webmention.yml
):
sources:
- from: https://deno.land/x/denoflow@0.0.35/sources/rss.ts
args:
- https://www.owenyoung.com/blog/atom.xml
- from: https://deno.land/x/denoflow@0.0.35/sources/rss.ts
args:
- https://www.owenyoung.com/en/blog/atom.xml
steps:
- use: fetch
args:
- https://webmention.app/check?token=${{ctx.env.WEBMENTION_APP_TOKEN}}&url=${{encodeURIComponent(ctx.item.links[0].href)}}
- method: GET
headers:
Content-Type: application/json
run: |
console.log(ctx.item.links[0].href);
const json = await ctx.result.json();
console.log(json);
Don’t forget to add enviroment variables WEBMENTION_APP_TOKEN
to your denoflow workflow file(.github/workflows/denoflow.yml
).
- run: deno run -A https://deno.land/x/denoflow/cli.ts run
env:
WEBMENTION_TOKEN: ${{secrets.WEBMENTION_TOKEN}}
WEBMENTION_APP_TOKEN: ${{secrets.WEBMENTION_APP_TOKEN}}
Conclusion🔗
I really like the concept of IndieWeb and their API design philosophy, it took some time but I still think it was worth it. It gave me renewed confidence in the Internet.
You can find the all source code of this blog on Github
Resources🔗
- Bird.gy - Bridgy connects your web site to social media. Likes, retweets, mentions, cross-posting
- Indie Webring
- Telegraph - Easily send Webmentions from your website
- Webmention.app - Automate your outgoing webmentions
- Webmention.io - Webmention.io is a hosted service created to easily receive webmentions on any web page.
- Fediring
- Webmentions on a static site with GitHub Actions