Compare commits
13 Commits
a40e585c0c
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0596c18939 | ||
|
|
8e74981e63 | ||
|
|
a399bb04fd | ||
|
|
b57796b7b5 | ||
|
|
15377b9c66 | ||
|
|
ec47643c32 | ||
|
|
529cf82206 | ||
|
|
f659eb3c81 | ||
|
|
5ba2769037 | ||
|
|
ae078f3a49 | ||
|
|
d4541950a9 | ||
|
|
41bb1677ca | ||
|
|
ab8ec68e77 |
36
.github/workflows/docker.yaml
vendored
Normal file
36
.github/workflows/docker.yaml
vendored
Normal file
@@ -0,0 +1,36 @@
|
||||
name: Build Docker Image
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
permissions:
|
||||
packages: write
|
||||
|
||||
jobs:
|
||||
docker:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: Log in to Gitea Container Registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: gitea.lol3r.com
|
||||
username: leon
|
||||
password: ${{ secrets.DOCKER_PAT }}
|
||||
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
file: ./Dockerfile
|
||||
push: true
|
||||
tags: |
|
||||
gitea.lol3r.com/leon/blog:latest
|
||||
gitea.lol3r.com/leon/blog:${{ gitea.sha }}
|
||||
@@ -6,9 +6,11 @@ description = "Deploying my new IPv4 expose setup"
|
||||
[taxonomies]
|
||||
categories = ["Homelab"]
|
||||
tags = ["Homelab", "IPv4", "IPv6"]
|
||||
series = ["homelab-ipv4-expose"]
|
||||
|
||||
[extra]
|
||||
toc = true
|
||||
series_order = 2
|
||||
+++
|
||||
|
||||
Implementing the ideas and conclusions from my initial post about exposing my IPv6 Homelab to IPv4 clients ([Homelab IPv4 Expose](@/homelab-ipv4-expose/index.md)) in the real world and deploying the entire system.
|
||||
|
||||
@@ -6,9 +6,11 @@ description = "How I expose my IPv6 only Homelab for IPv4 clients"
|
||||
[taxonomies]
|
||||
categories = ["Homelab"]
|
||||
tags = ["Homelab", "IPv4", "IPv6"]
|
||||
series = ["homelab-ipv4-expose"]
|
||||
|
||||
[extra]
|
||||
toc = true
|
||||
series_order = 1
|
||||
+++
|
||||
|
||||
How I plan to expose the services running in my homelab to IPv4 users despite not having a publicly routable IPv4 address/subnet.
|
||||
|
||||
19
content/nomad-consul-servicemesh/index.md
Normal file
19
content/nomad-consul-servicemesh/index.md
Normal file
@@ -0,0 +1,19 @@
|
||||
+++
|
||||
title = "Nomad + Consul ServiceMesh"
|
||||
date = 2026-10-01
|
||||
description = "TODO"
|
||||
draft = true
|
||||
|
||||
[taxonomies]
|
||||
categories = ["Homelab"]
|
||||
tags = ["Homelab", "Nomad", "Consul"]
|
||||
|
||||
[extra]
|
||||
toc = true
|
||||
+++
|
||||
|
||||
TODO
|
||||
|
||||
<!-- more -->
|
||||
|
||||
TODO2
|
||||
@@ -7,9 +7,11 @@ draft = true
|
||||
[taxonomies]
|
||||
categories = ["Programming"]
|
||||
tags = ["Factorio", "Rust"]
|
||||
series = ["quickfacts"]
|
||||
|
||||
[extra]
|
||||
toc = true
|
||||
series_order = 3
|
||||
+++
|
||||
|
||||
The first real entry in my series about my quickfacts project.
|
||||
|
||||
@@ -7,9 +7,11 @@ draft = true
|
||||
[taxonomies]
|
||||
categories = ["Programming"]
|
||||
tags = ["Factorio", "Rust"]
|
||||
series = ["quickfacts"]
|
||||
|
||||
[extra]
|
||||
toc = true
|
||||
series_order = 2
|
||||
+++
|
||||
|
||||
Testing
|
||||
|
||||
@@ -7,9 +7,11 @@ draft = true
|
||||
[taxonomies]
|
||||
categories = ["Programming"]
|
||||
tags = ["Factorio", "Rust", "Quickfacts"]
|
||||
series = ["quickfacts"]
|
||||
|
||||
[extra]
|
||||
toc = true
|
||||
series_order = 1
|
||||
+++
|
||||
|
||||
A small introduction to my ongoing quickfacts project.
|
||||
|
||||
28
content/reversi-intro/index.md
Normal file
28
content/reversi-intro/index.md
Normal file
@@ -0,0 +1,28 @@
|
||||
+++
|
||||
title = "Reversi - Intro"
|
||||
date = 2026-10-31
|
||||
description = "An introduction to reversi and my goals of this series"
|
||||
draft = true
|
||||
|
||||
[taxonomies]
|
||||
categories = ["Programming"]
|
||||
tags = ["Rust", "Reversi", "Optimization"]
|
||||
series = ["reversi"]
|
||||
|
||||
[extra]
|
||||
toc = true
|
||||
series_order = 1
|
||||
+++
|
||||
|
||||
So this first entry in the series will be an introduction into the game reversi, and the modified rules for this case, and talk a bit about the context and goals of this series overall.
|
||||
|
||||
<!-- more -->
|
||||
|
||||
# Motivation
|
||||
TODO
|
||||
|
||||
# Reversi the Game
|
||||
TODO
|
||||
|
||||
# Goal
|
||||
TODO
|
||||
@@ -1,32 +1,106 @@
|
||||
+++
|
||||
title = "Streaming setup"
|
||||
date = 2026-06-01
|
||||
title = "Streaming for Teamspeak3"
|
||||
date = 2026-10-18
|
||||
description = "Custom streaming setup to complement teamspeak"
|
||||
draft = true
|
||||
|
||||
[taxonomies]
|
||||
categories = ["Homelab"]
|
||||
tags = ["Homelab"]
|
||||
tags = ["Homelab", "Teamspeak3"]
|
||||
|
||||
[extra]
|
||||
toc = true
|
||||
+++
|
||||
|
||||
## Motivation
|
||||
Recently my friends and I decided to move away from discord as much as we can.
|
||||
The voice stuff was easy, just self host a Teamspeak3 instance on my Homelab and you are good to go
|
||||
(with arguably better results).
|
||||
Setting up a custom streaming service for me and my friends to have screensharing on teamspeak.
|
||||
|
||||
However we often had screenshares running in our discord, especially when everyone is just chilling
|
||||
in the same voice channel and someone shares their game/whatever in case anyone wants to check in.
|
||||
But as the experienced teamspeak user might be aware, TS3 does not support screen sharing or anything
|
||||
the like natively.
|
||||
<!-- more -->
|
||||
|
||||
## OvenMediaEngine + OBS
|
||||
Luckily I found OvenMediaEngine, which is an entire software setup to basically host your own streaming
|
||||
platform. This is obviously overkill for our goal, but works better than I would have expected at first.
|
||||
# Motivation
|
||||
Recently my friends and I decided to move away from discord as much as we can, for a variety of reasons.
|
||||
Replacing the simple voice stuff was easy, as I can just self host a Teamspeak3 instance on my servers and we are good to go (with arguably better results).
|
||||
|
||||
## Teamspeak3 Plugin
|
||||
To make the setup for my friends easier and to add some form of security/authentication, I wrote a small
|
||||
teamspeak plugin, which generates the credentials and urls needed to both stream yourself and watch streams.
|
||||
However we were often using screensharing / streaming on discord as well, whenever we were just hanging out and doing different things or just to quickly share something.
|
||||
But as anyone that has used teamspeak before might know, Teamspeak3 does not support screensharing or anything the like natively.
|
||||
|
||||
## Custom Website
|
||||
But given that teamspeak allows for creating your own plugins and makes it relatively easy to distribute them, I thought it might be interesting to try and basically add this functionality myself using a plugin.
|
||||
|
||||
## Conclusion
|
||||
# The Plan
|
||||
So the plan is to have a teamspeak plugin, which we use to authenticate users, automate the streaming setup and allow for easily watching streams.
|
||||
In practice that means you are required to log into my self-hosted auth server, which both ensures that you are actually allowed to stream and allows me to send dynamic information about the streaming setup to the client (like some secret key to use for generating the actual streaming key later on).
|
||||
If someone wants to start streaming, the plugin provides you the URL and stream-key for manual use, but can also start OBS itself and properly configure the streaming details, if the OBS websocket server has been enabled before.
|
||||
As a viewer you can just click on someone in teamspeak and click on "watch", which opens up a new window containing just the stream.
|
||||
|
||||
# OvenMediaEngine
|
||||
OvenMediaEngine[^OvenMediaEngine] is a project I found, that allows you to basically setup your own streaming platform with all kinds of features.
|
||||
However for this use-case the setup will be limited to the most basic configuration of just directly rebroadcasting the incoming streams, without any reencoding or the like.
|
||||
|
||||
So it accepts streams over rtmp from streaming sources, like OBS, and then listens for any "consumer" and forwards the stream using WebRTC over UDP to them.
|
||||
This allows for low-latency streaming, which is very important as you are likely talking to the person streaming and any large delays in the stream would greatly reduce the overall enjoyability of this plugin.
|
||||
|
||||
# OBS
|
||||
OBS[^OBS] in this case is just the default choice for streaming, as it is open-source and relatively easy to use by everyone.
|
||||
The main requirement is that it needs to be able to stream over rtmp, but a benefit of using OBS is that you can (once enabled) interact with OBS programmatically over a websocket server.
|
||||
This means that users do not have to manually enter the stream credentials every time they want to start the stream, but instead the plugin can actually configure these settings for the user.
|
||||
|
||||
# Teamspeak3 Plugin
|
||||
The plugin itself is relatively simple, as all it has to do is authenticate the user and give them two new buttons for starting a stream and watching streams.
|
||||
Teamspeak plugins[^ts3plugin] in general are relatively straight forward to build, as they are just dynamically linked library, exposing a specific set of functions, which are loaded at runtime by the teamspeak client.
|
||||
|
||||
To ease the development of the plugin itself, I actually also wrote my own small rust crate that provides a nicer API for specific features.
|
||||
This includes easier creation of menus and their corresponding handlers and providing a tracing subscriber that prints out the messages to the teamspeak log, for easier debugging.
|
||||
|
||||
The authentication works very roughly, by starting a local webserver and providing a link to my auth server.
|
||||
This is a simple OAuth flow, where the user gets redirected to the local webserver of the plugin, allowing it to get all the information it needs.
|
||||
This information includes both the endpoint to stream to and a secret key, which gets used together with the users ID to generate the stream-key, which authenticates them against OvenMediaEngine.
|
||||
|
||||
Starting streaming is relatively simple, the plugin uses the secret key and the users current teamspeak ID and generates the stream-key to use in OBS or other programs.
|
||||
For manual usage, the plugin outputs the streaming endpoint and the stream-key in a message to the user itself.
|
||||
For automatic usage, the plugin tries to launch OBS and connect to it over the websocket server, if that fails it will just ignore the failure, but if it can connect it will automatically update the streaming settings to the correct information.
|
||||
|
||||
Watching is the easiest part, as the plugin just launches the bundled custom viewer, with the teamspeak ID of the person they want to watch and the secret-key.
|
||||
The custom viewer will then take care of the authentication for watching the stream and actually begin the video player, which is actually using the provided player implementation of OvenMediaEngine.
|
||||
|
||||
## Custom Viewer
|
||||
The custom viewer, is based on a react site statically bundled into an electron app.
|
||||
I chose this approach, as OvenMediaEngine provides a library for embedding a stream in a website, which handles all the player and related logic making it very easy to use.
|
||||
|
||||
One detail is that it requires some extra information provided as command line arguments, like the ID of the stream to watch and a secret key to authenticate against OvenMediaEngine.
|
||||
|
||||
# Limitations
|
||||
Although the current system works, there are still quite a bit of short comings.
|
||||
|
||||
## IPv6 only
|
||||
OvenMediaEngine uses direct UDP streams between the server and the viewers, thus the server requires a publicly reachable address.
|
||||
However in my homelab setup, where this is hosted, I natively only have public IPv6 address and no public IPv4, meaning that only people with IPv6 can use the current setup.
|
||||
|
||||
This could be solved by either getting ahold of some public IPv4 addresses for my servers (very unlikely) or I could maybe use some of the more advanced features of OvenMediaEngine, which includes streaming to "satelite" servers, which then serve the viewers.
|
||||
Basically instead of the source server streaming to the viewers directly, it forwards the stream to these other servers, which then forward it to the viewers.
|
||||
This would allow me to have an external server with both IPv4 and IPv6, which can basically proxy all the streams, but it is unlikely that I will do this as it would incur more costs and maintenance for limited benefits.
|
||||
|
||||
## OBS not bundled
|
||||
Everything for using the plugin to watch streams is already bundled in the single installer, to make it easy to use for everyone.
|
||||
However I have not found a good way to achieve this with OBS and especially certain OBS settings.
|
||||
Like I mentioned before, the websocket server that the plugin is using to configure OBS, has to be enabled before the plugin can actually use it, all parameters about the websocket server can be configured via the cli apart from actually enabling it.
|
||||
This means, that users have to start OBS manually at least once to enable the websocket server, before the plugin can programmatically interact with OBS, which just creates some really unexpected pain-points in my experience.
|
||||
|
||||
## Stream IDs can become mismatched
|
||||
Currently the plugin is just using the teamspeak ID of a user as its stream ID, as that makes it very easy to determine the stream ID of anyone a user wants to watch.
|
||||
However the teamspeak ID changes whenever a user disconnects from the teamspeak server, so if someone:
|
||||
1. starts a stream
|
||||
2. disconnects
|
||||
3. reconnects
|
||||
|
||||
Then noone will be able to view their currently running stream, as it was started under their old teamspeak ID which has now changed.
|
||||
|
||||
# Conclusion
|
||||
Overall I am pretty happy with the setup and plugin right now.
|
||||
Obviously there are things to improve and its kind of hard to explain to someone using it for the first time, but once everything has been setup once, it actually works suprisingly well.
|
||||
The main issue currently is the limitation to IPv6, as I have friends whose internet provider does not give them an IPv6 address for some reason, meaning they cannot use the streaming.
|
||||
|
||||
Apart from that I would generally like to improve the viewer itself some more, as it is currently just the bare minimum to get something working, but I have a couple of ideas to improve the viewing experience.
|
||||
And I would really love to properly open-source the project, however since the plugin currently has the auth server hardcoded into it, it would not really benefit anyone, but I am thinking about how I might be able to remove the blockers for open-source.
|
||||
|
||||
# References
|
||||
[^OvenMediaEngine]: [OvenMediaEngine](https://ovenmedia.com/)
|
||||
[^OBS]: [OBS](https://obsproject.com/de)
|
||||
[^ts3plugin]: [Teamspeak3 Plugin SDK](https://github.com/teamspeak/ts3client-pluginsdk)
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
+++
|
||||
title = "Vault Agents"
|
||||
date = 2026-06-02
|
||||
description = "Setting up vault-agents for my servers"
|
||||
draft = true
|
||||
date = 2026-09-27
|
||||
description = "Setting up vault-agents on my servers"
|
||||
|
||||
[taxonomies]
|
||||
categories = ["Homelab"]
|
||||
@@ -12,29 +11,71 @@ tags = ["Homelab", "Vault"]
|
||||
toc = true
|
||||
+++
|
||||
|
||||
My journey to setting up vault-agents on my servers to handle dynamic secrets in configurations.
|
||||
This is my experience of setting up vault-agents on my servers to handle dynamic secrets in the configurations of my services.
|
||||
|
||||
<!-- more -->
|
||||
|
||||
# Configuring Vault Itself
|
||||
We first need to setup vault such that the vault agents can authenticate to vault itself.
|
||||
# What is vault-agent?
|
||||
Vault-agent[^vault_agent] is a kind of companion tool for vault[^vault], which allows one to start integrating vault and its capabilties into existing systems or systems that cannot easily be changed to integrate vault natively.
|
||||
For my use-case, the most important feature is the option to fetch secrets from vault and use it to template out files, specifically configuration files.
|
||||
This makes it a lot easier to manage secrets that need to be available locally at program startup, without having to update it manually everytime it changes or exipres.
|
||||
|
||||
# Configuring the Vault Server
|
||||
First we need to configure the vault server, such that we can generate the tokens for our agents and assign them the appropriate policies.
|
||||
|
||||
For this purpose, I first create a kind of account for each of the servers, that a vault agent will be installed on.
|
||||
In vault this translates to an identity and an alias for each of the servers.
|
||||
All of these identities are then grouped into a vault-agent group, which will actually have the shared policies assigned to it, such that there is only one place to manage all of the shared access, while also retaining the option to assign policies to individual servers, for example if one of them required extra credentials as it fulfills a unique job not shared by other servers.
|
||||
|
||||
To then allow the vault agents to actually authenticate with vault, I decided to simply use tokens directly instead of generating credentials using something like AppRole, but I still do not love this approach yet.
|
||||
For this I created a backend role in the token auth mechanism, which only allows the previously configured aliases to authenticate against it (as well as allowing orphaned tokens and renewing).
|
||||
|
||||
This means I can now generate a unique vault token for each of my servers and they can use it to authenticate as that server against vault to load any secrets they might need.
|
||||
|
||||
# Deploying Vault Agents
|
||||
TODO
|
||||
Testing
|
||||
Deploying the vault agents to my servers now comes down to:
|
||||
1. installing the vault agent software on the server
|
||||
2. getting a token for the server from vault
|
||||
3. deploying the general config for the vault agents
|
||||
|
||||
# Using them to template Configs
|
||||
A couple of examples how I am going to use the vault agents templating features
|
||||
# Deploying templated Configs
|
||||
Now that vault-agents are running on my servers, it is time to actually use them for something useful.
|
||||
This means I need to make it easy to integrate the deployment of templated configurations for other services, which I decided to do by introducing a simple ansible-role.
|
||||
This role takes care of creating the correct configuration for the vault-agent to template out a file and placing it in a predefined location, where the vault-agent can pick it up.
|
||||
It will then also trigger the vault-agent on the server to restart / reload its configuration to pick up the newly created config and start outputting the templated configuration.
|
||||
|
||||
## Nomad mTLS
|
||||
We can use vault agents to template the TLS certificates for nomads mTLS configuration.
|
||||
1. Get all currently valid CA certificates and store them in the trust bundle
|
||||
2. Get a leaf certificate for the current node
|
||||
So all I need to do, to deploy a service using templated configurations, is give the vault agent user the permissions to write the config to where my service expects it, give it the template to use and providing a command to execute whenever the written out file changes, in case the service does not automatically detect changes to it.
|
||||
|
||||
## Consul ACLs
|
||||
To configure the ACL tokens for consul
|
||||
# Example Uses
|
||||
## Nomad - mTLS
|
||||
My nomad cluster uses mTLS to encrypt its communication over the network, this means every node needs the CA certificate and its own certificate to communicate with other nodes.
|
||||
Previously this meant managing the certificates and their livecycles semi-manual, I automated all the steps, but needed to trigger the updates manually.
|
||||
|
||||
However now with the vault-agents, I can configure them to automatically fetch the shared CA certificate(s) and generate the certificate for the current node dynamically, including renewing it and notifying nomad accordingly.
|
||||
This workflow is a lot easier to maintain, as there is no human involvement to rotate the leaf-certificates, and allows for enforcing best practices, like short lifetimes for the leaf-certificates.
|
||||
|
||||
## Nomad - Consul ACLs
|
||||
My nomad cluster is very tightly integrated into consul, to make use of its service-mesh capabilties and some other features.
|
||||
However because I have ACLs enabled on the consul cluster, every nomad client requires an ACL token to connect to consul.
|
||||
Managing this manually would either mean a lot of maintenance for me or using tokens with very long lifetimes, which are both not good options.
|
||||
|
||||
Instead I can use the consul secret-engine in vault and the vault-agents, to dynamically obtain an ACL token for each server, which contains just enough permissions for nomad and has a short lifetime to follow best-practices as much as possible (for my context).
|
||||
|
||||
# Some Issues
|
||||
I want to cover some of the issues that I encountered / still have, just so I am clear on the downsides of this setup.
|
||||
|
||||
## Vault Token expiring while vault-agent is not running
|
||||
I have one server that likes to randomly freeze on the OS level, which is mostly fine in my setup as all the normal workloads can just migrate and once I get to restarting the server everything just comes back.
|
||||
However because I use a vault token, which expires after at most one day with my current config, it frequently happens that the token for the server expires before I get around to restarting it, meaning I need to run my vault-agent deployment again to generate a new token for the server.
|
||||
This is not a huge issue for me in this case, but mostly annoying as I need to keep that in mind during maintenance and adds another kind of weird thing.
|
||||
|
||||
## One bad config infects the entire agent
|
||||
If I accidentally deploy one bad vault-agent config, for example by writing a custom config for templating a config, then the entire vault-agent on the server is not working properly and thus affects other services as well.
|
||||
I have no good approach to prevent this, besides enforcing the use of the ansible-role, which could be updated to make sure that after every change the agent is still running without issues, but that is not yet implemented.
|
||||
|
||||
# References
|
||||
[^vault]: [Vault](https://developer.hashicorp.com/vault/docs/about-vault/what-is-vault)
|
||||
[^vault_agent]: [Vault Agent](https://developer.hashicorp.com/vault/docs/agent-and-proxy/agent)
|
||||
[^vault_pki_api]: [Vault PKI API](https://developer.hashicorp.com/vault/api-docs/secret/pki)
|
||||
[^consul_template_secrets]: [Consul Template Secrets](https://github.com/hashicorp/consul-template/blob/main/docs/templating-language.md#secret)
|
||||
[^vault_agent_templates]: [Vault Agent Templates](https://developer.hashicorp.com/vault/docs/agent-and-proxy/agent/template)
|
||||
|
||||
28
sass/styles.scss
Normal file
28
sass/styles.scss
Normal file
@@ -0,0 +1,28 @@
|
||||
article[itemprop="articleBody"] {
|
||||
|
||||
h1 {
|
||||
font-size: 1.2rem !important;
|
||||
}
|
||||
}
|
||||
|
||||
article[itemprop="articleBody"] {
|
||||
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
margin-top: 1rem !important;
|
||||
font-size: 1.1rem !important;
|
||||
}
|
||||
}
|
||||
|
||||
.hack h2,
|
||||
.hack h3,
|
||||
.hack h4,
|
||||
.hack h5,
|
||||
.hack h6 {
|
||||
position: relative;
|
||||
margin-bottom: 1.2rem;
|
||||
margin-top: 1.75rem
|
||||
}
|
||||
@@ -21,3 +21,13 @@
|
||||
|
||||
<script src="/js/chart.min.js"></script>
|
||||
{% endblock %}
|
||||
|
||||
{% block styles %}
|
||||
{{ super() }}
|
||||
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="{{ get_url(path='styles.css', trailing_slash=false) | safe }}"
|
||||
/>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
@@ -13,6 +13,14 @@
|
||||
</svg>
|
||||
|
||||
{% if page.date %}Published: {{ page.date | date(format="%F") }}{% endif %}
|
||||
|
||||
{% if page.taxonomies.series %}
|
||||
<br>
|
||||
{% set series_name = page.taxonomies.series | first %}
|
||||
{% set series_term = get_taxonomy_term(kind="series", term=series_name) %}
|
||||
|
||||
Part {{ page.extra.series_order }} of the <a href="{{ series_term.permalink }}">{{ series_name }}</a> series
|
||||
{% endif %}
|
||||
{% endmacro meta %}
|
||||
|
||||
{% macro page_in_list(page) %}
|
||||
|
||||
20
templates/page.html
Normal file
20
templates/page.html
Normal file
@@ -0,0 +1,20 @@
|
||||
{% extends "radion/templates/page.html" %}
|
||||
|
||||
{% block header %}
|
||||
{{ super() }}
|
||||
|
||||
{% if page.taxonomies.series %}
|
||||
{% set series_name = page.taxonomies.series | first %}
|
||||
{% set series_term = get_taxonomy_term(kind="series", term=series_name) %}
|
||||
|
||||
<div class="series">
|
||||
<a href="{{ series_term.permalink }}">
|
||||
Series: {{ series_term.name }}
|
||||
</a>
|
||||
|
||||
{% if page.extra.series_order %}
|
||||
<span> · Part {{ page.extra.series_order }}</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock header %}
|
||||
29
templates/series/list.html
Normal file
29
templates/series/list.html
Normal file
@@ -0,0 +1,29 @@
|
||||
{% extends "index.html" %}
|
||||
|
||||
{% block title %}Browse Series{% endblock title %}
|
||||
{% block og_title %}Browse Series{% endblock og_title %}
|
||||
{% block twitter_title %}Browse Series{% endblock twitter_title %}
|
||||
|
||||
{% block meta_description %}
|
||||
<meta name="description" content="Discover articles organized by series in this site index." />
|
||||
<meta property="og:description" content="Discover articles organized by series in this site index." />
|
||||
<meta property="twitter:description" content="Discover articles organized by series in this site index." />
|
||||
{% endblock meta_description %}
|
||||
|
||||
{% block content %}
|
||||
{% block header %}
|
||||
{{ super() }}
|
||||
{% endblock header %}
|
||||
|
||||
<h1>Series</h1>
|
||||
|
||||
{% if terms %}
|
||||
<ul>
|
||||
{% for term in terms %}
|
||||
<li>
|
||||
<a href="{{ term.permalink | safe }}">{{ term.name }}</a>({{ term.pages | length }})
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
{% endblock content %}
|
||||
23
templates/series/single.html
Normal file
23
templates/series/single.html
Normal file
@@ -0,0 +1,23 @@
|
||||
{% extends "index.html" %}
|
||||
|
||||
{% block title %}Posts in "{{ term.name }}"{% endblock title %}
|
||||
{% block og_title %}Posts in "{{ term.name }}"{% endblock og_title %}
|
||||
{% block twitter_title %}Posts in "{{ term.name }}"{% endblock twitter_title %}
|
||||
|
||||
{% block meta_description %}
|
||||
<meta name="description" content="Discover articles from the '{{ term.name }}' series in this site index." />
|
||||
<meta property="og:description" content="Discover articles from the '{{ term.name }}' series in this site index." />
|
||||
<meta property="twitter:description" content="Discover articles from the '{{ term.name }}' series in this site index." />
|
||||
{% endblock meta_description %}
|
||||
|
||||
{% block content %}
|
||||
{% block header %}
|
||||
{{ super() }}
|
||||
{% endblock header %}
|
||||
|
||||
<h1>{{ term.name }} - Series</h1>
|
||||
|
||||
{% for page in term.pages %}
|
||||
{{ post_macros::page_in_list(page=page) }}
|
||||
{% endfor %}
|
||||
{% endblock content %}
|
||||
@@ -13,6 +13,7 @@ taxonomies = [
|
||||
# You can enable/disable RSS
|
||||
{ name = "categories", feed = true },
|
||||
{ name = "tags", feed = true },
|
||||
{ name = "series", feed = true },
|
||||
]
|
||||
|
||||
author = "Leon"
|
||||
@@ -24,6 +25,8 @@ index_format = "elasticlunr_json"
|
||||
[markdown]
|
||||
external_links_target_blank = true
|
||||
insert_anchor_links = "left"
|
||||
definition_list = true
|
||||
github_alerts = true
|
||||
|
||||
[markdown.highlighting]
|
||||
theme = "catppuccin-mocha"
|
||||
@@ -35,6 +38,7 @@ taxonomies = [
|
||||
# You can enable/disable RSS
|
||||
{ name = "categories", feed = true },
|
||||
{ name = "tags", feed = true },
|
||||
{ name = "series", feed = true },
|
||||
]
|
||||
|
||||
radion_title = "Leon's Personal Blog"
|
||||
@@ -42,6 +46,7 @@ radion_menu = [
|
||||
{ url = "$BASE_URL", name = "Home" },
|
||||
{ url = "$BASE_URL/categories", name = "Categories" },
|
||||
{ url = "$BASE_URL/tags", name = "Tags" },
|
||||
{ url = "$BASE_URL/series", name = "Series" },
|
||||
]
|
||||
theme = "toggle" # options: {light, dark, auto, toggle}
|
||||
toc = true
|
||||
|
||||
Reference in New Issue
Block a user