Compare commits
22 Commits
29402e477c
...
master
Author | SHA1 | Date | |
---|---|---|---|
8c9da1d33f | |||
c311b67b04 | |||
93c77ebd48 | |||
ad3ba54726 | |||
3085297dbe | |||
9c15c4d4d4 | |||
e3dcd4c6b0 | |||
0f6cc9b1fc | |||
e7d373f886 | |||
b851a127c7 | |||
a2b355477f | |||
6b245f540d | |||
85f0e72676 | |||
ee77ff7c84 | |||
f0571e9e38 | |||
c19dbf3e44 | |||
2f262dc7d3 | |||
e439db1a06 | |||
26940964b8 | |||
adff904a1a | |||
56915e99bf | |||
4557625816 |
38
.gitea/workflows/deploy.yaml
Normal 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}"
|
@@ -21,7 +21,6 @@ nav = [
|
||||
{title = "Index", path = "/"},
|
||||
{title = "About", path = "/about/"},
|
||||
{title = "Search", path = "/posts/"},
|
||||
{title = "Categories", path = "/categories/", mobile_only = true}
|
||||
]
|
||||
latest_posts_count = 3
|
||||
repository_url = "https://git.0x45.cz/em/em.0x45.cz"
|
||||
|
@@ -4,13 +4,15 @@ title = "About"
|
||||
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 am working on.
|
||||
|
||||
It is generated to static HTML via [Zola](https://www.getzola.org/).
|
||||
|
||||
## Contact me
|
||||
|
||||
- [em@0x45.cz](mailto:em@0x45.cz)
|
||||
- [em@0x45.cz](mailto:em@0x45.cz)
|
||||
- [@irungentoo](https://t.me/irungentoo) on Telegram
|
||||
- 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/).
|
||||
|
||||
## 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
|
||||
|
||||
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.microlab.space/em](https://git.microlab.space/em)
|
BIN
content/about/profile-picture.jpg
Normal file
After Width: | Height: | Size: 19 KiB |
@@ -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.
|
||||
|
||||

|
||||
|
||||
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.
|
||||
|
||||

|
||||
|
||||
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.
|
||||
|
||||

|
||||
|
||||
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).
|
After Width: | Height: | Size: 228 KiB |
After Width: | Height: | Size: 32 KiB |
After Width: | Height: | Size: 138 KiB |
@@ -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:
|
||||
|
||||

|
||||
|
||||
|
||||
### 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.
|
||||
|
||||

|
After Width: | Height: | Size: 102 KiB |
After Width: | Height: | Size: 37 KiB |
BIN
content/posts/streaming-camera-output-trough-udp/camera.jpg
Normal file
After Width: | Height: | Size: 401 KiB |
BIN
content/posts/streaming-camera-output-trough-udp/client.jpg
Normal file
After Width: | Height: | Size: 310 KiB |
57
content/posts/streaming-camera-output-trough-udp/index.md
Normal 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.
|
||||
|
||||

|
||||
|
||||
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.
|
||||
|
||||

|
||||
|
||||
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.
|
||||
|
||||

|
||||
|
||||
## 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
|
||||
```
|
||||
|
||||

|
||||
|
||||
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.
|
After Width: | Height: | Size: 294 KiB |
BIN
content/posts/streaming-camera-output-trough-udp/x210.jpg
Normal file
After Width: | Height: | Size: 471 KiB |
@@ -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.
|
||||
|
||||
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
|
||||
name: Build
|
||||
|
||||
@@ -32,9 +34,9 @@ on:
|
||||
- master
|
||||
|
||||
env:
|
||||
ZOLA_VERSION: "0.18.0"
|
||||
HOST: ${{ secrets.SSH_HOSTNAME }}
|
||||
HOST_DIR: ${{ secrets.SSH_TARGET_DIR }}
|
||||
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 }}
|
||||
|
||||
@@ -64,6 +66,8 @@ jobs:
|
||||
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
|
||||
|
||||
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_PRIVATE_KEY` | Plaintext private key |
|
||||
|
||||
## Vars
|
||||
|
||||
| | |
|
||||
|-------------------|------------------------|
|
||||
| `ZOLA_VERSION` | Version of Zola |
|
||||
| `HOST` | Server hostname |
|
||||
| `TARGET_DIR` | Website root directory |
|
||||
|
@@ -3,24 +3,24 @@
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
src: url("/fonts/spectral-regular.woff2") format("woff2"),
|
||||
url("/fonts/spectral-regular.woff") format("woff"),
|
||||
url("/fonts/spectral-regular.ttf") format("truetype");
|
||||
url("/fonts/spectral-regular.woff") format("woff"),
|
||||
url("/fonts/spectral-regular.ttf") format("truetype");
|
||||
}
|
||||
@font-face {
|
||||
font-family: spectral;
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
src: url("/fonts/spectral-bold.woff2") format("woff2"),
|
||||
url("/fonts/spectral-bold.woff") format("woff"),
|
||||
url("/fonts/spectral-bold.ttf") format("truetype");
|
||||
url("/fonts/spectral-bold.woff") format("woff"),
|
||||
url("/fonts/spectral-bold.ttf") format("truetype");
|
||||
}
|
||||
@font-face {
|
||||
font-family: spectral;
|
||||
font-style: italic;
|
||||
font-weight: 400;
|
||||
src: url("/fonts/spectral-italic.woff2") format("woff2"),
|
||||
url("/fonts/spectral-italic.woff") format("woff"),
|
||||
url("/fonts/spectral-italic.ttf") format("truetype");
|
||||
url("/fonts/spectral-italic.woff") format("woff"),
|
||||
url("/fonts/spectral-italic.ttf") format("truetype");
|
||||
}
|
||||
|
||||
::selection{
|
||||
@@ -28,6 +28,8 @@
|
||||
color: #000;
|
||||
}
|
||||
|
||||
html { scroll-behavior: smooth }
|
||||
|
||||
.wrap {
|
||||
max-width: 55rem;
|
||||
margin: 0 auto;
|
||||
@@ -81,6 +83,9 @@ nav {
|
||||
align-self: start;
|
||||
top: 2em;
|
||||
margin-bottom: 4rem;
|
||||
max-height: calc(100vh - 2em);
|
||||
box-sizing: border-box;
|
||||
overflow-y: auto;
|
||||
|
||||
span.title {
|
||||
display: block;
|
||||
@@ -94,7 +99,8 @@ nav {
|
||||
li a {
|
||||
color: #000;
|
||||
display: block;
|
||||
padding: 0 .5em;
|
||||
line-height: 1;
|
||||
padding: .25em .5em;
|
||||
|
||||
&:hover {
|
||||
color: #fff;
|
||||
@@ -102,7 +108,35 @@ nav {
|
||||
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 {
|
||||
display: none; // Hide for non-js browsers
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
border: 1px solid #ccc;
|
||||
@@ -212,23 +245,36 @@ a {
|
||||
&:hover { text-decoration: underline }
|
||||
}
|
||||
|
||||
.profile-picture {
|
||||
float: right;
|
||||
width: 15rem;
|
||||
margin-left: 3em;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 50rem) {
|
||||
|
||||
.grid { grid-template-columns: auto }
|
||||
|
||||
header { margin-bottom: 0 }
|
||||
|
||||
nav {
|
||||
position: initial;
|
||||
margin-bottom: 1rem;
|
||||
ul { display: none }
|
||||
ul.main {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
border: 1px solid #43494c;
|
||||
padding: .25rem 0;
|
||||
}
|
||||
ul li.mobile-only { display: list-item }
|
||||
span.title { display: none }
|
||||
}
|
||||
|
||||
header, nav { margin-bottom: 3rem }
|
||||
.profile-picture {
|
||||
width: 10rem;
|
||||
margin-left: 1em;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
@@ -242,14 +288,19 @@ a {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
nav ul li a {
|
||||
nav ul li a, nav ul.table-of-contents li a {
|
||||
color: inherit;
|
||||
|
||||
&:hover {
|
||||
color: #131516;
|
||||
background-color: #cdcdcd;
|
||||
}
|
||||
}
|
||||
nav ul.table-of-contents li a {
|
||||
color: #ff6e67;
|
||||
&::before, &::after {
|
||||
background-color: #383838;
|
||||
}
|
||||
}
|
||||
|
||||
main {
|
||||
.info, input.search { border-color: #383838 }
|
||||
|
@@ -1,11 +1,23 @@
|
||||
function filter_name(str)
|
||||
function filter_title(query, articles)
|
||||
{
|
||||
if (str.length == 0) {
|
||||
articles.forEach(article => article.style.display = "block");
|
||||
} else {
|
||||
articles.forEach(article => article.style.display = article.dataset.title.indexOf(str.toLowerCase()) === -1 ? "none" : "block");
|
||||
const search = query.trim().toLowerCase();
|
||||
|
||||
if (search.length === 0) {
|
||||
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"));
|
||||
document.querySelector("#search").style.display = "inline-block";
|
||||
window.addEventListener('DOMContentLoaded', () => {
|
||||
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));
|
||||
});
|
||||
|
@@ -5,9 +5,9 @@
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<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="stylesheet" href="/style.css">
|
||||
<link rel="icon" href="favicon.svg">
|
||||
<link rel="alternate" type="application/atom+xml" title="Atom" href="{{ get_url(path="/atom.xml") }}">
|
||||
<link rel="stylesheet" href="{{ get_url(path="/style.css") }}">
|
||||
<link rel="icon" href="{{ get_url(path="/favicon.svg") }}">
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
@@ -17,7 +17,7 @@
|
||||
{% if config.extra.repository_url %}
|
||||
<a href="{{ config.extra.repository_url }}">Source</a>,
|
||||
{% 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>
|
||||
</header>
|
||||
@@ -26,13 +26,13 @@
|
||||
<span class="title">Navigation</span>
|
||||
<ul class="main">
|
||||
{% for item in config.extra.nav %}
|
||||
<li
|
||||
{% if item.mobile_only %}
|
||||
class="mobile-only"
|
||||
{% endif %}
|
||||
><a href="{{ get_url(path=item.path, trailing_slash=true) }}">{{ item.title }}</a></li>
|
||||
<li><a href="{{ get_url(path=item.path, trailing_slash=true) }}">{{ item.title }}</a></li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% if page.toc and not page.extra.notoc %}
|
||||
<span class="title">Table of Contents</span>
|
||||
{{ macros::toc() }}
|
||||
{% endif %}
|
||||
<span class="title">Categories</span>
|
||||
{{ macros::list_taxonomy(kind="categories") }}
|
||||
</nav>
|
||||
|
@@ -48,24 +48,22 @@
|
||||
{% endmacro %}
|
||||
|
||||
{% macro toc() %}
|
||||
{% if page.toc and not page.extra.notoc %}
|
||||
<ul>
|
||||
{% for h1 in page.toc %}
|
||||
<li>
|
||||
<a href="{{ h1.permalink | safe }}">{{ h1.title }}</a>
|
||||
{% if h1.children %}
|
||||
<ul>
|
||||
{% for h2 in h1.children %}
|
||||
<li>
|
||||
<a href="{{ h2.permalink | safe }}">{{ h2.title }}</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
<ul class="table-of-contents">
|
||||
{% for h1 in page.toc %}
|
||||
<li>
|
||||
<a href="{{ h1.permalink | safe }}">{{ h1.title }}</a>
|
||||
{% if h1.children %}
|
||||
<ul>
|
||||
{% for h2 in h1.children %}
|
||||
<li>
|
||||
<a href="{{ h2.permalink | safe }}">{{ h2.title }}</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro list_taxonomy(kind, page=false, prepend="") %}
|
||||
|
@@ -6,7 +6,6 @@
|
||||
|
||||
{% block content %}
|
||||
<h1>{{ page.title }}</h1>
|
||||
{{ macros::toc() }}
|
||||
{{ page.content | safe }}
|
||||
{{ macros::page_info(page=page) }}
|
||||
{{ macros::page_updates(page=page) }}
|
||||
|
@@ -6,7 +6,7 @@
|
||||
|
||||
{% block content %}
|
||||
<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() }}
|
||||
{% endblock content %}
|
||||
|
||||
|