1
0

Compare commits

..

22 Commits

Author SHA1 Message Date
8c9da1d33f Fix openQA Helm Chart installation
All checks were successful
Build / build (push) Successful in 19s
2025-06-19 14:39:36 +02:00
c311b67b04 Installing openQA on Kubernetes with Helm Charts
All checks were successful
Build / build (push) Successful in 19s
2025-06-04 13:40:32 +02:00
93c77ebd48 Improve Gitea Action example
All checks were successful
Build / build (push) Successful in 20s
2025-06-03 15:01:46 +02:00
ad3ba54726 Enable automatic build and deployment
All checks were successful
Build / build (push) Successful in 14s
2025-06-03 13:43:13 +02:00
3085297dbe Fix minor typo 2025-06-03 12:51:03 +02:00
9c15c4d4d4 Obfuscate e-mail address 2025-05-19 17:07:54 +02:00
e3dcd4c6b0 Enable smooth scrolling 2025-05-15 21:07:31 +02:00
0f6cc9b1fc Fix indentation 2025-05-15 21:03:52 +02:00
e7d373f886 Display TOC in sidebar 2025-05-15 21:02:19 +02:00
b851a127c7 Improve search
This new approach makes sure the input field is hidden on browsers which
ignore `display:none`, such as w3m or lynx.
2025-05-14 14:51:49 +02:00
a2b355477f Add profile picture 2025-05-06 11:33:53 +02:00
6b245f540d Better mobile nav 2025-05-05 19:02:42 +02:00
85f0e72676 Remove mobile-specific navigation options 2025-05-05 18:53:50 +02:00
ee77ff7c84 Add education in about.md 2025-02-11 21:39:43 +01:00
f0571e9e38 Change event listener init 2025-01-10 09:49:43 +01:00
c19dbf3e44 Fallback to standard chars 2025-01-09 10:47:45 +01:00
2f262dc7d3 Fix indent 2025-01-09 10:46:57 +01:00
e439db1a06 Add a USB-C pinout image 2025-01-09 10:44:49 +01:00
26940964b8 New post about MDP-M905 2025-01-08 20:22:57 +01:00
adff904a1a Add picture of the mentioned camera 2024-11-25 12:23:32 +01:00
56915e99bf Streaming camera output trough UDP 2024-11-14 23:05:22 +01:00
4557625816 Fix paths of static resources 2024-10-07 21:05:59 +02:00
23 changed files with 395 additions and 58 deletions

View File

@@ -0,0 +1,38 @@
name: Build
on:
push:
branches:
- master
env:
ZOLA_VERSION: ${{ vars.ZOLA_VERSION }}
HOST: ${{ vars.HOST }}
HOST_DIR: ${{ vars.HOST_DIR }}
SSH_USERNAME: ${{ secrets.SSH_USERNAME }}
SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }}
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Install Zola
run: |
wget https://github.com/getzola/zola/releases/download/v${ZOLA_VERSION}/zola-v${ZOLA_VERSION}-x86_64-unknown-linux-gnu.tar.gz
tar -xvzf *.tar.gz
- name: Build
run: ./zola build
- name: Deploy
run: |
apt update -y && apt-get install -y --no-install-recommends rsync
eval "$(ssh-agent -s)"
ssh-add - <<< "${SSH_PRIVATE_KEY}"
mkdir -p ~/.ssh/
ssh-keyscan -H ${HOST} >> ~/.ssh/known_hosts
rsync -r --delete-after public/* "${SSH_USERNAME}@${HOST}:${HOST_DIR}"

View File

@@ -21,7 +21,6 @@ nav = [
{title = "Index", path = "/"}, {title = "Index", path = "/"},
{title = "About", path = "/about/"}, {title = "About", path = "/about/"},
{title = "Search", path = "/posts/"}, {title = "Search", path = "/posts/"},
{title = "Categories", path = "/categories/", mobile_only = true}
] ]
latest_posts_count = 3 latest_posts_count = 3
repository_url = "https://git.0x45.cz/em/em.0x45.cz" repository_url = "https://git.0x45.cz/em/em.0x45.cz"

View File

@@ -4,13 +4,15 @@ title = "About"
notoc = true notoc = true
+++ +++
This is my personal website mainly used for technical articles about problems and projects I am working on. <img src="profile-picture.jpg" alt="Profile Picture" class="profile-picture">
This is my personal website mainly used for technical articles about problems and projects I&nbsp;am working on.
It is generated to static HTML via [Zola](https://www.getzola.org/). It is generated to static HTML via [Zola](https://www.getzola.org/).
## Contact me ## Contact me
- [em@0x45.cz](mailto:em@0x45.cz) - [&#101;&#109;&#64;&#48;&#120;&#52;&#53;&#46;&#99;&#122;](&#109;&#97;&#105;&#108;&#116;&#111;&#58;&#101;&#109;&#64;&#48;&#120;&#52;&#53;&#46;&#99;&#122;)
- [@irungentoo](https://t.me/irungentoo) on Telegram - [@irungentoo](https://t.me/irungentoo) on Telegram
- PGP: [0x453A7AE1754BFED2](https://keys.openpgp.org/vks/v1/by-fingerprint/3B08B7B5F00CCB0370EE3E71453A7AE1754BFED2) - PGP: [0x453A7AE1754BFED2](https://keys.openpgp.org/vks/v1/by-fingerprint/3B08B7B5F00CCB0370EE3E71453A7AE1754BFED2)
@@ -18,9 +20,23 @@ It is generated to static HTML via [Zola](https://www.getzola.org/).
My primary employment is at [SUSE](https://www.suse.com) as a security QA engineer. I also work (or have worked) as a teacher at several Prague schools, mainly at [Gymnázium Jana Keplera](https://gjk.cz/). My primary employment is at [SUSE](https://www.suse.com) as a security QA engineer. I also work (or have worked) as a teacher at several Prague schools, mainly at [Gymnázium Jana Keplera](https://gjk.cz/).
## Education
Both of my degrees have been obtained from the Faculty of Education at Charles University in Prague, specializing in computer science and education.
My Bachelor's thesis explores static website generators and includes a reference implementation using Zola.
- [Bachelor's Thesis Text](https://dspace.cuni.cz/bitstream/handle/20.500.11956/152842/130279410.pdf)
- [Defense Results](https://dspace.cuni.cz/handle/20.500.11956/152842)
Master's thesis focuses on implementing a custom ESP32 development board expandable by custom extension boards, along with a set educational materials which can be applied when teaching embedded programing. Both source files and educational materials can be found at [capyboard.dev](https://capyboard.dev) or at [my GitHub profile](https://github.com/realcharmer).
- [Master's Thesis Text](https://dspace.cuni.cz/bitstream/handle/20.500.11956/196503/120496276.pdf)
- [Defense Results](https://dspace.cuni.cz/handle/20.500.11956/196503)
## Projects ## Projects
Some of my project are published directly at this website or in Git: Some of my projects are published directly at this website or in Git:
- [git.0x45.cz/em](https://git.0x45.cz/em) - [git.0x45.cz/em](https://git.0x45.cz/em)
- [git.microlab.space/em](https://git.microlab.space/em) - [git.microlab.space/em](https://git.microlab.space/em)

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

View File

@@ -0,0 +1,30 @@
+++
title = "Enabling USB-C Chargers on Miniware MDP-M905"
date = 2025-01-08
[taxonomies]
categories = ["Hardware"]
[extra]
author = "Emil Miler"
+++
The [MDP-M905](https://www.morningtools.com/article/116/533.html) is a small power supply by Miniware, known for their TS100 and TS80 series of smart soldering irons. The issue with the MDP-M905 is that it doesn't work with USB-C chargers, only USB-A. This article describes a way to enable this missing functionality through good old-fashioned hardware hacking.
<!-- more -->
The MDP-M905 has a USB-C connection for power, alongside a classic barrel jack. However, the USB-C port is not wired properly according to the USB-C specification. USB-C is more complex than older connectors; it requires an exchange of information between the connected devices before providing power.
![USB-C Pinout](usb-c-pinout.png)
The issue lies in the absence of a crucial component: two resistors on pins A5 and B5, which need to be pulled to ground through 5.1kΩ resistors. These pins, known as the "CC lines," are responsible for enabling USB-C to USB-C charging. Notice the two resistors R5 and R6 in the example above.
Fortunately, the fix is simple: attack the board with a soldering iron and a steady hand to add the missing resistors.
![Modifications](modifications.jpg)
I began by covering the PCB with Kapton tape to isolate the board from my modifications. Next, I added a GND line using a thicker wire and soldered the two resistors. The challenging part was connecting the resistors to the tiny pins using very thin wire. After some careful and precise soldering, the board is now able to power on with any USB-C-only charger.
![Working Device](working-device.jpg)
During my testing, the power limit was 18W at 12V. If you need more power, consider upgrading to the newer MDP-M906, which supports both QC (Quick Charge) and PD (Power Delivery).

Binary file not shown.

After

Width:  |  Height:  |  Size: 228 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 138 KiB

View File

@@ -0,0 +1,127 @@
+++
title = "Installing openQA on Kubernetes with Helm Charts"
date = 2025-06-04
[taxonomies]
categories = ["Linux"]
[extra]
author = "Emil Miler"
+++
Recently, I experimented with Kubernetes by installing [openQA](https://open.qa/) using Helm Charts. This article is a simple guide on how to do the same locally on your computer, with minimal effort.
<!-- more -->
You need a working cluster. There are several single-node management tools that let you run a Kubernetes cluster locally on your machine, such as [k3d](https://k3d.io/), [kind](https://kind.sigs.k8s.io/), or [minikube](https://minikube.sigs.k8s.io/). They provide a cluster entirely isolated within a container.
I will be using minikube.
## Installation of Tools
Install minikube -- or any other management tool -- along with *helm*. For example, on Void Linux:
```
xbps-install -S minikube kubernetes-helm
```
You also have to have a running container engine, such as [Podman](https://podman.io/) or [Docker](https://www.docker.com/). Configuring a container engine is outside the scope of this article.
## Preparing the Cluster
Since I am using minikube, I can simply start it like so:
```
minikube start
```
This might take a while during the initial setup. If you want, you can also run a web-based dashboard for better visualization and debugging.
```
minikube dashboard
```
The dashboard will open in your default browser.
## Downloading openQA Charts
The official charts are available in the [openQA source code repository](https://github.com/os-autoinst/openQA), which can be cloned with the following command:
```
git clone https://github.com/os-autoinst/openQA.git
```
If you have GitHub setup properly with SSH, you might use this instead:
```
git clone git@github.com:os-autoinst/openQA.git
```
The charts themselves are stored in `container/helm`. You can change your working directory to that location:
```
cd openQA/container/helm
```
### Chart Structure
The `charts` folder contains several different charts:
- `openqa` -- Parent chart.
- `webui` -- Web interface and API for openQA.
- `worker` -- openQA worker that performs the actual test execution.
We will be working with the parent chart `openqa`. Since it references the other charts, they will be handled automatically by the parent.
## Installing openQA
We then have to update dependencies and install openQA:
```
$ helm dependency update charts/openqa/
$ helm install openqa charts/openqa/
```
We can also list installed releases to confirm that openQA was deployed:
```
$ helm list
NAME NAMESPACE REVISION UPDATED STATUS CHART APP VERSION
openqa default 1 2025-06-19 12:39:08.150021755 +0200 CEST deployed openqa-0.1.0
```
The minikube dashboard also lists all running pods:
![Minikube Dashboard, Pods](minikube-dashboard-pods.png)
### Accessing openQA WebUI
Since openQA is confined to its own network within minikube, it is not directly accessible from the host system. Below are the services running inside minikube:
```
$ kubectl get services
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
db ClusterIP 10.103.192.20 <none> 5432/TCP 6m34s
db-hl ClusterIP None <none> 5432/TCP 6m34s
kubernetes ClusterIP 10.96.0.1 <none> 443/TCP 174m
openqa-webui ClusterIP 10.104.24.71 <none> 9526/TCP,9527/TCP,9528/TCP,9529/TCP 6m34s
```
First, we need to create a proxy within the minikube network:
```
minikube tunnel
```
The openQA WebUI is running at `10.104.24.71` on port `9526` and is now accessible directly from the browser.
![openQA Dashboard](openqa-dashboard.png)

Binary file not shown.

After

Width:  |  Height:  |  Size: 102 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 401 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 310 KiB

View File

@@ -0,0 +1,57 @@
+++
title = "Streaming camera output trough UDP"
date = 2024-11-14
[taxonomies]
categories = ["Linux"]
[extra]
author = "Emil Miler"
+++
I had an interesting assignment to set up a low-latency video stream from a camera to a TV screen on the third floor of a building for a short event. I came up with a simple solution: streaming it directly over UDP using OBS and capturing the stream with MPV.
<!-- more -->
## Intro
The event took place on a stage that needed to be streamed to two screens -- one in the lobby on the ground floor and another deep inside the third floor. The audio team provided a clean audio mix directly to my camera's audio mixer.
![Camera](camera.jpg)
The building, located in the old town of Prague, had a complicated floor plan, as you might imagine. Connecting to the ground floor screen was straightforward with a direct HDMI link, but reaching the third floor was more challenging without an HDMI-to-Ethernet extender, which I didn't have.
![x210](x210.jpg)
My solution was to lay a UTP cable from the TV on the third floor to my laptop with an HDMI capture card, configure direct routing, and stream from OBS to a client that would output the video and audio over HDMI to the TV.
## Server setup
First I set up the network:
```sh
ip a add 192.168.66.1/24 dev eth0
ip route add 192.168.66.2 dev eth0
```
The OBS part was a little tricky. I am sure this can be highly optimized.
![OBS Settings](obs-settings.png)
## Client setup
Again, static adress is needed, but no routes have to be defined this time.
```sh
ip a add 192.168.66.2/24 dev eth0
```
The stream can then by captured with MPV:
```
mpv --no-cache udp://@0.0.0.0:8081
```
![Client](client.jpg)
It does have some latency, but since the TV was hidden deep in the building, it was not a big issue. I might try using something like [UltraGrid](http://www.ultragrid.cz/) in the future.

Binary file not shown.

After

Width:  |  Height:  |  Size: 294 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 471 KiB

View File

@@ -23,6 +23,8 @@ The Act Runner reads the private key from a secret and uses it for SSH authentic
Zola is also installed by pulling a built release tar and extracting the binary. I have tried using the official Zola container, but it just would not work properly. Zola is also installed by pulling a built release tar and extracting the binary. I have tried using the official Zola container, but it just would not work properly.
Here is an example from this very website [deployment action](https://git.0x45.cz/em/em.0x45.cz/src/branch/master/.gitea/workflows/deploy.yaml):
```yaml ```yaml
name: Build name: Build
@@ -32,9 +34,9 @@ on:
- master - master
env: env:
ZOLA_VERSION: "0.18.0" ZOLA_VERSION: ${{ vars.ZOLA_VERSION }}
HOST: ${{ secrets.SSH_HOSTNAME }} HOST: ${{ vars.HOST }}
HOST_DIR: ${{ secrets.SSH_TARGET_DIR }} HOST_DIR: ${{ vars.HOST_DIR }}
SSH_USERNAME: ${{ secrets.SSH_USERNAME }} SSH_USERNAME: ${{ secrets.SSH_USERNAME }}
SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }} SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }}
@@ -64,6 +66,8 @@ jobs:
rsync -r --delete-after public/* "${SSH_USERNAME}@${HOST}:${HOST_DIR}" rsync -r --delete-after public/* "${SSH_USERNAME}@${HOST}:${HOST_DIR}"
``` ```
The example cinfiguration uses both secrets and vars from within Gitea. These have to be set trough the web UI under *Settings > Actions*.
## Webserver configuration ## Webserver configuration
The server needs a new user with write access to the website root directory. I still call it drone for the sake of not having to redo my server configuration. The server needs a new user with write access to the website root directory. I still call it drone for the sake of not having to redo my server configuration.
@@ -88,7 +92,13 @@ Public key has to be added to `~/.ssh/authorized_keys` of the "drone" user.
| | | | | |
|-------------------|------------------------| |-------------------|------------------------|
| `SSH_HOSTNAME` | Server hostname |
| `SSH_TARGET_DIR` | Website root directory |
| `SSH_USERNAME` | In our case "drone" | | `SSH_USERNAME` | In our case "drone" |
| `SSH_PRIVATE_KEY` | Plaintext private key | | `SSH_PRIVATE_KEY` | Plaintext private key |
## Vars
| | |
|-------------------|------------------------|
| `ZOLA_VERSION` | Version of Zola |
| `HOST` | Server hostname |
| `TARGET_DIR` | Website root directory |

View File

@@ -3,24 +3,24 @@
font-style: normal; font-style: normal;
font-weight: 400; font-weight: 400;
src: url("/fonts/spectral-regular.woff2") format("woff2"), src: url("/fonts/spectral-regular.woff2") format("woff2"),
url("/fonts/spectral-regular.woff") format("woff"), url("/fonts/spectral-regular.woff") format("woff"),
url("/fonts/spectral-regular.ttf") format("truetype"); url("/fonts/spectral-regular.ttf") format("truetype");
} }
@font-face { @font-face {
font-family: spectral; font-family: spectral;
font-style: normal; font-style: normal;
font-weight: 700; font-weight: 700;
src: url("/fonts/spectral-bold.woff2") format("woff2"), src: url("/fonts/spectral-bold.woff2") format("woff2"),
url("/fonts/spectral-bold.woff") format("woff"), url("/fonts/spectral-bold.woff") format("woff"),
url("/fonts/spectral-bold.ttf") format("truetype"); url("/fonts/spectral-bold.ttf") format("truetype");
} }
@font-face { @font-face {
font-family: spectral; font-family: spectral;
font-style: italic; font-style: italic;
font-weight: 400; font-weight: 400;
src: url("/fonts/spectral-italic.woff2") format("woff2"), src: url("/fonts/spectral-italic.woff2") format("woff2"),
url("/fonts/spectral-italic.woff") format("woff"), url("/fonts/spectral-italic.woff") format("woff"),
url("/fonts/spectral-italic.ttf") format("truetype"); url("/fonts/spectral-italic.ttf") format("truetype");
} }
::selection{ ::selection{
@@ -28,6 +28,8 @@
color: #000; color: #000;
} }
html { scroll-behavior: smooth }
.wrap { .wrap {
max-width: 55rem; max-width: 55rem;
margin: 0 auto; margin: 0 auto;
@@ -81,6 +83,9 @@ nav {
align-self: start; align-self: start;
top: 2em; top: 2em;
margin-bottom: 4rem; margin-bottom: 4rem;
max-height: calc(100vh - 2em);
box-sizing: border-box;
overflow-y: auto;
span.title { span.title {
display: block; display: block;
@@ -94,7 +99,8 @@ nav {
li a { li a {
color: #000; color: #000;
display: block; display: block;
padding: 0 .5em; line-height: 1;
padding: .25em .5em;
&:hover { &:hover {
color: #fff; color: #fff;
@@ -102,7 +108,35 @@ nav {
text-decoration: none; text-decoration: none;
} }
} }
li.mobile-only { display: none } }
ul.table-of-contents li {
a {
color: #e1140a;
position: relative;
&::before, &::after {
content: "";
background-color: #bbb;
position: absolute;
left: 0;
top: 0;
}
&::before {
height: 2px;
width: .5em;
}
&::after {
width: 2px;
height: .5em;
}
&:hover { color: #fff }
&:hover::before, &:hover::after { display: none }
}
li {
padding: 0 .5em;
&>a::before, &>a::after { display: none }
}
} }
} }
@@ -189,7 +223,6 @@ main {
} }
input.search { input.search {
display: none; // Hide for non-js browsers
box-sizing: border-box; box-sizing: border-box;
width: 100%; width: 100%;
border: 1px solid #ccc; border: 1px solid #ccc;
@@ -212,23 +245,36 @@ a {
&:hover { text-decoration: underline } &:hover { text-decoration: underline }
} }
.profile-picture {
float: right;
width: 15rem;
margin-left: 3em;
}
@media screen and (max-width: 50rem) { @media screen and (max-width: 50rem) {
.grid { grid-template-columns: auto } .grid { grid-template-columns: auto }
header { margin-bottom: 0 }
nav { nav {
position: initial; position: initial;
margin-bottom: 1rem;
ul { display: none } ul { display: none }
ul.main { ul.main {
display: flex; display: flex;
justify-content: center; justify-content: center;
flex-wrap: wrap; flex-wrap: wrap;
border: 1px solid #43494c;
padding: .25rem 0;
} }
ul li.mobile-only { display: list-item }
span.title { display: none } span.title { display: none }
} }
header, nav { margin-bottom: 3rem } .profile-picture {
width: 10rem;
margin-left: 1em;
}
} }
@media (prefers-color-scheme: dark) { @media (prefers-color-scheme: dark) {
@@ -242,14 +288,19 @@ a {
color: inherit; color: inherit;
} }
nav ul li a { nav ul li a, nav ul.table-of-contents li a {
color: inherit; color: inherit;
&:hover { &:hover {
color: #131516; color: #131516;
background-color: #cdcdcd; background-color: #cdcdcd;
} }
} }
nav ul.table-of-contents li a {
color: #ff6e67;
&::before, &::after {
background-color: #383838;
}
}
main { main {
.info, input.search { border-color: #383838 } .info, input.search { border-color: #383838 }

View File

@@ -1,11 +1,23 @@
function filter_name(str) function filter_title(query, articles)
{ {
if (str.length == 0) { const search = query.trim().toLowerCase();
articles.forEach(article => article.style.display = "block");
} else { if (search.length === 0) {
articles.forEach(article => article.style.display = article.dataset.title.indexOf(str.toLowerCase()) === -1 ? "none" : "block"); articles.forEach(article => article.style.display = 'block');
return;
} }
articles.forEach(article => {
const title = article.dataset.title || '';
article.style.display = title.includes(search) ? 'block' : 'none';
});
} }
let articles = Array.from(document.querySelectorAll("article")); window.addEventListener('DOMContentLoaded', () => {
document.querySelector("#search").style.display = "inline-block"; document.querySelector('.search-wrapper').innerHTML =
'<input class="search" type="search" placeholder="Search">';
const search = document.querySelector("input[type='search']");
const articles = Array.from(document.querySelectorAll('article'));
search.addEventListener('input', () => filter_title(search.value, articles));
});

View File

@@ -5,9 +5,9 @@
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}{{ config.title }}{% endblock %}</title> <title>{% block title %}{{ config.title }}{% endblock %}</title>
<link rel="alternate" type="application/atom+xml" title="Atom" href="{{ get_url(path="/atom.xml", trailing_slash=false) }}"> <link rel="alternate" type="application/atom+xml" title="Atom" href="{{ get_url(path="/atom.xml") }}">
<link rel="stylesheet" href="/style.css"> <link rel="stylesheet" href="{{ get_url(path="/style.css") }}">
<link rel="icon" href="favicon.svg"> <link rel="icon" href="{{ get_url(path="/favicon.svg") }}">
</head> </head>
<body> <body>
<header> <header>
@@ -17,7 +17,7 @@
{% if config.extra.repository_url %} {% if config.extra.repository_url %}
<a href="{{ config.extra.repository_url }}">Source</a>, <a href="{{ config.extra.repository_url }}">Source</a>,
{% endif %} {% endif %}
<a href="{{ get_url(path="/atom.xml", trailing_slash=false) }}">RSS/Atom</a> <a href="{{ get_url(path="/atom.xml") }}">RSS/Atom</a>
</div> </div>
</div> </div>
</header> </header>
@@ -26,13 +26,13 @@
<span class="title">Navigation</span> <span class="title">Navigation</span>
<ul class="main"> <ul class="main">
{% for item in config.extra.nav %} {% for item in config.extra.nav %}
<li <li><a href="{{ get_url(path=item.path, trailing_slash=true) }}">{{ item.title }}</a></li>
{% if item.mobile_only %}
class="mobile-only"
{% endif %}
><a href="{{ get_url(path=item.path, trailing_slash=true) }}">{{ item.title }}</a></li>
{% endfor %} {% endfor %}
</ul> </ul>
{% if page.toc and not page.extra.notoc %}
<span class="title">Table of Contents</span>
{{ macros::toc() }}
{% endif %}
<span class="title">Categories</span> <span class="title">Categories</span>
{{ macros::list_taxonomy(kind="categories") }} {{ macros::list_taxonomy(kind="categories") }}
</nav> </nav>

View File

@@ -48,24 +48,22 @@
{% endmacro %} {% endmacro %}
{% macro toc() %} {% macro toc() %}
{% if page.toc and not page.extra.notoc %} <ul class="table-of-contents">
<ul> {% for h1 in page.toc %}
{% for h1 in page.toc %} <li>
<li> <a href="{{ h1.permalink | safe }}">{{ h1.title }}</a>
<a href="{{ h1.permalink | safe }}">{{ h1.title }}</a> {% if h1.children %}
{% if h1.children %} <ul>
<ul> {% for h2 in h1.children %}
{% for h2 in h1.children %} <li>
<li> <a href="{{ h2.permalink | safe }}">{{ h2.title }}</a>
<a href="{{ h2.permalink | safe }}">{{ h2.title }}</a> </li>
</li> {% endfor %}
{% endfor %} </ul>
</ul> {% endif %}
{% endif %} </li>
</li> {% endfor %}
{% endfor %} </ul>
</ul>
{% endif %}
{% endmacro %} {% endmacro %}
{% macro list_taxonomy(kind, page=false, prepend="") %} {% macro list_taxonomy(kind, page=false, prepend="") %}

View File

@@ -6,7 +6,6 @@
{% block content %} {% block content %}
<h1>{{ page.title }}</h1> <h1>{{ page.title }}</h1>
{{ macros::toc() }}
{{ page.content | safe }} {{ page.content | safe }}
{{ macros::page_info(page=page) }} {{ macros::page_info(page=page) }}
{{ macros::page_updates(page=page) }} {{ macros::page_updates(page=page) }}

View File

@@ -6,7 +6,7 @@
{% block content %} {% block content %}
<h1>{{ section.title }}</h1> <h1>{{ section.title }}</h1>
<input id="search" class="search" type="text" placeholder="Search titles" oninput="filter_name(this.value)"> <div class="search-wrapper"></div>
{{ macros::list_posts() }} {{ macros::list_posts() }}
{% endblock content %} {% endblock content %}