upd
3
.vscode/extensions.json
vendored
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
{
|
||||||
|
"recommendations": []
|
||||||
|
}
|
|
@ -32,6 +32,7 @@ Broken links are to be expected.
|
||||||
|
|
||||||
As long as the move / redesign is not fully done, here the old site is still online: [old.aron.petau.net](https://old.aron.petau.net)
|
As long as the move / redesign is not fully done, here the old site is still online: [old.aron.petau.net](https://old.aron.petau.net)
|
||||||
{% end %}
|
{% end %}
|
||||||
|
|
||||||
Progress of the rebuild:
|
Progress of the rebuild:
|
||||||
<progress value="90" max="100"></progress>
|
<progress value="90" max="100"></progress>
|
||||||
|
|
||||||
|
@ -41,14 +42,13 @@ Further, there is an initial effort to bring translations to this website. That
|
||||||
Progress of the translation:
|
Progress of the translation:
|
||||||
<progress value="15" max="100"></progress>
|
<progress value="15" max="100"></progress>
|
||||||
|
|
||||||
|
|
||||||
{% alert(important=true) %}
|
{% alert(important=true) %}
|
||||||
Last updated: 2025-05-14
|
Last updated: 2025-05-14
|
||||||
{% end %}
|
{% end %}
|
||||||
|
|
||||||
{% crt() %}
|
{% crt() %}
|
||||||
|
|
||||||
```
|
```sh
|
||||||
➜ content git:(main) ✗ tree -L 2
|
➜ content git:(main) ✗ tree -L 2
|
||||||
.
|
.
|
||||||
├── _index.md
|
├── _index.md
|
||||||
|
|
|
@ -1,17 +1,17 @@
|
||||||
+++
|
+++
|
||||||
title = "Curriculum vitae"
|
title = "Vita"
|
||||||
description = "Aron writes about their past experience"
|
description = "Arons Vita und Erfahrung"
|
||||||
authors = ["Aron Petau"]
|
authors = ["Aron Petau"]
|
||||||
toc = true
|
toc = true
|
||||||
+++
|
+++
|
||||||
|
|
||||||
## Who am I?
|
## Wer bin ich?
|
||||||
|
|
||||||
Below you will find a chronological list of my education, my work experience and a rough overview of different softwares and machines I am familiar with. For a less formal self-description, please see the [About](/about) page.
|
Im Folgenden findest du eine chronologische Liste meiner Ausbildung, meiner Berufserfahrung sowie einen groben Überblick über verschiedene Softwares und Maschinen, mit denen ich vertraut bin. Eine weniger formelle Selbstbeschreibung findest du auf der [Über mich](/about)-Seite.
|
||||||
|
|
||||||
Contact me via [Email](mailto:aron@petau.net) for further questions.
|
Für weitere Fragen kannst du mich gerne per [E-Mail](mailto:aron@petau.net) kontaktieren.
|
||||||
|
|
||||||
### Education
|
### Ausbildung
|
||||||
|
|
||||||
{% timeline() %}
|
{% timeline() %}
|
||||||
[
|
[
|
||||||
|
@ -25,7 +25,7 @@ Contact me via [Email](mailto:aron@petau.net) for further questions.
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"title": "BSc. Cognitive Science",
|
"title": "BSc. Cognitive Science",
|
||||||
"body": "Within a diverse program, I focused on Philosophy, Artificial Intelligence, Machine Learning, Informatics, and Linguistics.",
|
"body": "Im Rahmen eines vielseitigen Studienprogramms habe ich mich auf Philosophie, Künstliche Intelligenz, Maschinelles Lernen, Informatik und Linguistik konzentriert.",
|
||||||
"from": "Oct ‘16",
|
"from": "Oct ‘16",
|
||||||
"to": "Feb ‘22",
|
"to": "Feb ‘22",
|
||||||
"icon": "fas fa-building",
|
"icon": "fas fa-building",
|
||||||
|
@ -34,15 +34,15 @@ Contact me via [Email](mailto:aron@petau.net) for further questions.
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"title": "RISE Internship",
|
"title": "RISE Internship",
|
||||||
"body": "In the context of a DAAD RISE Stipend, I researched the possibilities of a decentralized electricity grid in remote regions of the Himalayas. I conducted field research, evaluation, and product simulation.",
|
"body": "Im Rahmen eines DAAD RISE-Stipendiums forschte ich zu den Möglichkeiten eines dezentralen Stromnetzes in abgelegenen Regionen des Himalayas. Dabei führte ich Feldforschung, Auswertungen und Produktsimulationen durch.",
|
||||||
"from": "Sep ‘18",
|
"from": "Sep ‘18",
|
||||||
"to": "Jan ‘19",
|
"to": "Jan ‘19",
|
||||||
"icon": "fas fa-building",
|
"icon": "fas fa-building",
|
||||||
"location": "IIT Kharagpur, India"
|
"location": "IIT Kharagpur, Indien"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"title": "Erasmus Semester",
|
"title": "Erasmus Semester",
|
||||||
"body": "I took courses in the Philosophy department and Masters’ program for Cognitive Science. I also attended the Cognitive Science Summer School.",
|
"body": "Ich belegte Kurse im Fachbereich Philosophie sowie im Masterprogramm für Kognitionswissenschaft. Außerdem nahm ich an der Summer School für Kognitionswissenschaft teil.",
|
||||||
"from": "Feb ‘19",
|
"from": "Feb ‘19",
|
||||||
"to": "Jul ‘19",
|
"to": "Jul ‘19",
|
||||||
"icon": "fas fa-building",
|
"icon": "fas fa-building",
|
||||||
|
@ -51,7 +51,7 @@ Contact me via [Email](mailto:aron@petau.net) for further questions.
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"title": "M.A. Design & Computation",
|
"title": "M.A. Design & Computation",
|
||||||
"body": "I am currently in the fourth semester of transdisciplinary cooperation between UdK and TU Berlin with a focus on critical artistic engagement with technology.",
|
"body": "Derzeit schließe ich ein transdisziplinäres Programm zwischen der UdK und der TU Berlin ab, das sich auf eine kritische und künstlerische Auseinandersetzung mit Technologie konzentriert.",
|
||||||
"from": "Oct ‘22",
|
"from": "Oct ‘22",
|
||||||
"to": "now",
|
"to": "now",
|
||||||
"icon": "fas fa-building",
|
"icon": "fas fa-building",
|
||||||
|
|
|
@ -51,7 +51,7 @@ Contact me via [Email](mailto:aron@petau.net) for further questions.
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"title": "M.A. Design & Computation",
|
"title": "M.A. Design & Computation",
|
||||||
"body": "I am currently in the fourth semester of transdisciplinary cooperation between UdK and TU Berlin with a focus on critical artistic engagement with technology.",
|
"body": "I am currently completing a transdisciplinary program between UdK and TU Berlin, focusing on critical and artistic engagement with technology.",
|
||||||
"from": "Oct ‘22",
|
"from": "Oct ‘22",
|
||||||
"to": "now",
|
"to": "now",
|
||||||
"icon": "fas fa-building",
|
"icon": "fas fa-building",
|
||||||
|
|
|
@ -1,32 +1,70 @@
|
||||||
+++
|
+++
|
||||||
title = "Terms and Privacy Statement"
|
title = "Terms und Privacy "
|
||||||
date = 2025-05-01
|
date = 2025-05-01
|
||||||
authors = ["Aron Petau"]
|
authors = ["Aron Petau"]
|
||||||
+++
|
+++
|
||||||
My website address is: <https://aron.petau.net> .
|
|
||||||
|
|
||||||
## Location
|
Meine Website-Adresse ist: <https://aron.petau.net>
|
||||||
|
|
||||||
~~This page is hosted on [GitHub](https://github.com) through GitHub-pages.~~
|
## Standort
|
||||||
Not anymore! It is now proudly self-hosted from under my sofa, sometimes using solar energy. I consider you being able to read this already a win.
|
|
||||||
|
|
||||||
It is protected by [Cloudflare](https://www.cloudflare.com/en-gb/). Not so proud of that one, but you gotta be secure, no?
|
Diese Seite wird nicht mehr auf [GitHub Pages](https://github.com) gehostet — **sie wird jetzt stolz selbstgehostet** auf einem FriendlyElec CM3588 Board, das unter meinem Sofa in Deutschland läuft, manchmal sogar mit Solarstrom. Dass du das hier lesen kannst, ist für mich schon ein kleiner Erfolg.
|
||||||
I do not proxy anything and I try using my best knowledge and consciousness to minimize my and other peoples efforts of tracking.
|
|
||||||
|
|
||||||
I do not collect any data.
|
Die gesamte Seite läuft in einem schlanken, modularen **Docker**-Setup, das eine einfache Wartung, Migration und Anpassung ermöglicht, ohne Stabilität oder Kontrolle einzubüßen.
|
||||||
This is a static website, which means there is no database attached and nothing can be tracked by me.
|
|
||||||
I also do not collect any cookies, nor are there any third-party cookies involved.
|
|
||||||
|
|
||||||
[GitHub](https://github.com), the place where I host this website, does collect the IP address of any visitor.
|
Die Website wird mit [Caddy](https://caddyserver.com) betrieben, der HTTPS und Web-Auslieferung einfach und elegant handhabt.
|
||||||
I have no influence on this and neither the financial resources to avoid this free hosting firm.
|
|
||||||
|
|
||||||
## Embedded content from other websites
|
Die Seite wird durch [Cloudflare](https://www.cloudflare.com/en-gb/) geschützt. Damit bin ich nicht wirklich glücklich, aber Sicherheit ist wichtig – genauso wie deine Privatsphäre.
|
||||||
|
|
||||||
Articles on this site may include embedded content (e.g. videos, images, articles, etc.). Embedded content from other websites behaves in the exact same way as if the visitor has visited the other website.
|
Ich nutze keine weiteren Proxy-Dienste als die unbedingt notwendigen und bemühe mich mit meinem Wissen und Bewusstsein, Tracking für mich und andere so gut wie möglich zu minimieren.
|
||||||
These websites may collect data about you, use cookies, embed additional third-party tracking, and monitor your interaction with that embedded content, including tracking your interaction with the embedded content if you have an account and are logged in to that website.
|
|
||||||
|
|
||||||
If privacy is important enough for you to check out this page, you probably know how to use a VPN service and avoid this problem altogether.
|
Da die Seite in Deutschland gehostet wird, unterliegt sie der [DSGVO](https://dsgvo-gesetz.de/). Weil ich keine personenbezogenen Daten erhebe oder verarbeite, ist **kein Cookie-Banner oder Datenschutzhinweis gesetzlich erforderlich**.
|
||||||
|
|
||||||
I have a raspberry pi and the motivation, but not the knowledge to properly self-host. If you do have constructive feedback, please feel free to contact me.
|
### Keine Datenerhebung
|
||||||
|
|
||||||
Thank you for your attention.
|
Dies ist eine statische Website.
|
||||||
|
Es gibt **keine Datenbank**, **keine Benutzerkonten** und **keine Cookies** — weder von mir, noch von Drittanbietern.
|
||||||
|
Hier wird nichts getrackt.
|
||||||
|
|
||||||
|
### Kommentare via Mastodon
|
||||||
|
|
||||||
|
Diese Seite nutzt ein leichtgewichtiges, datenschutzfreundliches Kommentarsystem, das mit [Mastodon](https://joinmastodon.org) integriert ist. Alle Kommentare werden live per API von Mastodon-Instanzen abgerufen.
|
||||||
|
|
||||||
|
Das bedeutet:
|
||||||
|
|
||||||
|
- **Alle Kommentardaten verbleiben bei Mastodon**
|
||||||
|
- Ich speichere **nichts** – keine Kommentare, keine Nutzernamen, keine Metadaten
|
||||||
|
- Die Nutzung ist freiwillig und wird vollständig über Mastodon abgewickelt
|
||||||
|
|
||||||
|
### Eingebettete YouTube-Videos
|
||||||
|
|
||||||
|
Auf der Website sind YouTube-Videos eingebettet. Leider bedeutet das, dass YouTube dein Betrachtungsverhalten verfolgen kann, wenn du diese Videos ansiehst.
|
||||||
|
|
||||||
|
### IndieWeb Sharing
|
||||||
|
|
||||||
|
Es gibt verschiedene Möglichkeiten, Beiträge über das [IndieWeb](https://indieweb.org) zu teilen, etwa über Webmention oder ActivityPub. Diese Werkzeuge fördern Dezentralisierung und datenschutzbewusste Interaktion – ohne Tracking oder Überwachungskapitalismus.
|
||||||
|
|
||||||
|
### Quellcode-Transparenz
|
||||||
|
|
||||||
|
Der gesamte Quellcode dieser Website ist öffentlich zugänglich.
|
||||||
|
Du findest ihn unten rechts auf der Seite – schau gerne rein, prüfe oder fork ihn.
|
||||||
|
|
||||||
|
## Eingebettete Inhalte von anderen Websites
|
||||||
|
|
||||||
|
Manche Beiträge enthalten eingebettete Inhalte (z.B. Videos, Bilder oder Artikel).
|
||||||
|
Diese verhalten sich genau so, als würdest du die jeweilige andere Website direkt besuchen. Das heißt:
|
||||||
|
|
||||||
|
- Sie **können Daten über dich sammeln**
|
||||||
|
- Sie **können Cookies verwenden**
|
||||||
|
- Sie **können Drittanbieter-Tracking einbetten**
|
||||||
|
- Sie **können deine Interaktionen verfolgen**, insbesondere wenn du bei deren Diensten angemeldet bist
|
||||||
|
|
||||||
|
Wenn du es bis hierher in meine Datenschutzerklärung geschafft hast, kennst du dich wahrscheinlich mit VPNs aus und hast ein gutes Gespür für digitale Hygiene.
|
||||||
|
|
||||||
|
## Schlusswort
|
||||||
|
|
||||||
|
Ich hoste diese Website jetzt selbst auf einem kleinen, aber leistungsstarken CM3588 Board in einem Docker-Setup mit Caddy als Webserver. Das ist eine spannende Lernreise – manchmal chaotisch, aber immer mit viel Spaß.
|
||||||
|
|
||||||
|
Wenn du Ideen zur Verbesserung hast oder einfach über Home-Server quatschen willst, melde dich gern.
|
||||||
|
|
||||||
|
Danke, dass du vorbeischaust und dir Datenschutz wichtig ist.
|
||||||
|
|
|
@ -2,31 +2,70 @@
|
||||||
title = "Terms and Privacy Statement"
|
title = "Terms and Privacy Statement"
|
||||||
date = 2025-05-01
|
date = 2025-05-01
|
||||||
authors = ["Aron Petau"]
|
authors = ["Aron Petau"]
|
||||||
|
|
||||||
|
[extra]
|
||||||
|
show_copyright = true
|
||||||
|
show_shares = true
|
||||||
|
featured = true
|
||||||
+++
|
+++
|
||||||
My website address is: <https://aron.petau.net> .
|
|
||||||
|
My website address is: <https://aron.petau.net>
|
||||||
|
|
||||||
## Location
|
## Location
|
||||||
|
|
||||||
~~This page is hosted on [GitHub](https://github.com) through GitHub-pages.~~
|
This page is no longer hosted on [GitHub Pages](https://github.com)—**it is now proudly self-hosted** on a FriendlyElec CM3588 board, running under my sofa in Germany, sometimes even powered by solar energy. The fact that you're reading this already feels like a small victory.
|
||||||
Not anymore! It is now proudly self-hosted from under my sofa, sometimes using solar energy. I consider you being able to read this already a win.
|
|
||||||
|
|
||||||
It is protected by [Cloudflare](https://www.cloudflare.com/en-gb/). Not so proud of that one, but you gotta be secure, no?
|
The entire site runs in a lightweight and modular **Docker** setup, making it easy to maintain, migrate, and tweak without compromising on stability or control.
|
||||||
I do not proxy anything and I try using my best knowledge and consciousness to minimize my and other peoples efforts of tracking.
|
|
||||||
|
|
||||||
I do not collect any data.
|
The site is served using [Caddy](https://caddyserver.com), which handles HTTPS and web delivery with simplicity and elegance.
|
||||||
This is a static website, which means there is no database attached and nothing can be tracked by me.
|
|
||||||
I also do not collect any cookies, nor are there any third-party cookies involved.
|
|
||||||
|
|
||||||
[GitHub](https://github.com), the place where I host this website, does collect the IP address of any visitor.
|
It is protected by [Cloudflare](https://www.cloudflare.com/en-gb/). I'm not particularly thrilled about that part, but security matters—and so does your privacy.
|
||||||
I have no influence on this and neither the financial resources to avoid this free hosting firm.
|
|
||||||
|
|
||||||
## Embedded content from other websites
|
I do not use any proxying services beyond what's absolutely necessary, and I do my best to reduce tracking for both myself and others, using what knowledge and care I have.
|
||||||
|
|
||||||
Articles on this site may include embedded content (e.g. videos, images, articles, etc.). Embedded content from other websites behaves in the exact same way as if the visitor has visited the other website.
|
Because this site is hosted in Germany, it falls under the [GDPR](https://gdpr.eu/). Since I do not collect or process any personal data, **no cookie banner or privacy notice is legally required**.
|
||||||
These websites may collect data about you, use cookies, embed additional third-party tracking, and monitor your interaction with that embedded content, including tracking your interaction with the embedded content if you have an account and are logged in to that website.
|
|
||||||
|
|
||||||
If privacy is important enough for you to check out this page, you probably know how to use a VPN service and avoid this problem altogether.
|
### No Data Collection
|
||||||
|
|
||||||
I have a raspberry pi and the motivation, but not the knowledge to properly self-host. If you do have constructive feedback, please feel free to contact me.
|
This is a static website.
|
||||||
|
There is **no database**, **no user accounts**, and **no cookies**—not from me, and not from any third parties.
|
||||||
|
Nothing here tracks you.
|
||||||
|
|
||||||
Thank you for your attention.
|
### Comments via Mastodon
|
||||||
|
|
||||||
|
This site includes a lightweight, privacy-conscious commenting system that integrates with [Mastodon](https://joinmastodon.org). All comments are retrieved live via API calls from Mastodon instances.
|
||||||
|
|
||||||
|
This means:
|
||||||
|
|
||||||
|
- **All comment data remains on Mastodon**
|
||||||
|
- I store **nothing**—no comment content, no usernames, no metadata
|
||||||
|
- Interaction with the system is completely voluntary and handled on Mastodon’s side
|
||||||
|
|
||||||
|
### IndieWeb Sharing
|
||||||
|
|
||||||
|
You’ll find various options to share posts across the [IndieWeb](https://indieweb.org), using standards like Webmention and ActivityPub. These tools empower decentralization and privacy-conscious interaction—no tracking, no surveillance capitalism.
|
||||||
|
|
||||||
|
### Source Code Transparency
|
||||||
|
|
||||||
|
The entire codebase of this site is publicly available.
|
||||||
|
You can find it in the bottom right corner of the page—feel free to explore, audit, or fork.
|
||||||
|
|
||||||
|
## Embedded Content from Other Websites
|
||||||
|
|
||||||
|
Some posts may include embedded content (like videos, images, or articles).
|
||||||
|
These behave just like if you had visited the other website directly. That means:
|
||||||
|
|
||||||
|
- They **may collect data** about you
|
||||||
|
- They **may use cookies**
|
||||||
|
- They **might embed third-party tracking**
|
||||||
|
- They **can track your interaction**, especially if you're logged in to their service
|
||||||
|
|
||||||
|
If you’ve made it this far into my privacy policy, chances are you already know how to use a VPN and have some basic sense of digital hygiene.
|
||||||
|
|
||||||
|
## Final Words
|
||||||
|
|
||||||
|
I now self-host this website on a tiny but powerful CM3588 board with a Dockerized setup and Caddy as the web server. It’s a learning journey—both fun and slightly chaotic.
|
||||||
|
|
||||||
|
If you have ideas for improving the setup, or just want to chat about home servers, reach out anytime.
|
||||||
|
|
||||||
|
Thanks for stopping by and caring about privacy.
|
||||||
|
|
|
@ -1,23 +1,19 @@
|
||||||
---
|
+++
|
||||||
permalink: /rent-ulli/
|
title = "How to miet Ulli"
|
||||||
title: "How to miet Ulli"
|
date = 2025-05-01
|
||||||
excerpt: "Everything to know about the T4"
|
authors = ["Aron Petau"]
|
||||||
last_modified_at: 2022-04-30T10:23:16-04:00
|
|
||||||
toc: false
|
[extra]
|
||||||
layout: single
|
show_copyright = true
|
||||||
classes: wide
|
show_shares = true
|
||||||
author: "Aron Petau"
|
featured = true
|
||||||
header:
|
+++
|
||||||
overlay_image : /assets/images/ulli.jpg
|
|
||||||
overlay_filter: 0.5
|
|
||||||
---
|
|
||||||
|
|
||||||
This is a work in Progress. Informations on here are subject to change.
|
This is a work in Progress. Informations on here are subject to change.
|
||||||
{: .notice--danger}
|
{: .notice--danger}
|
||||||
|
|
||||||
<iframe width="100%" height="800" src="https://vrm.victronenergy.com/installation/167009/embed/f61b11f2"></iframe>
|
<iframe width="100%" height="800" src="https://vrm.victronenergy.com/installation/167009/embed/f61b11f2"></iframe>
|
||||||
|
|
||||||
|
|
||||||
## The general stuff
|
## The general stuff
|
||||||
|
|
||||||
The car is a 1991 VW T4, with a 2.0l **Benzin** (petrol) engine.
|
The car is a 1991 VW T4, with a 2.0l **Benzin** (petrol) engine.
|
||||||
|
@ -109,7 +105,8 @@ Then we can arrange a time for you to pick up the car.
|
||||||
The car costs 30€ per day, plus 0.10€ per km.
|
The car costs 30€ per day, plus 0.10€ per km.
|
||||||
This factors in my insurance and the taxes I have to pay.
|
This factors in my insurance and the taxes I have to pay.
|
||||||
|
|
||||||
Any damages to the car will be charged to you. A total damage would cost you somewhere around 10.000 Euro, so please be careful. Check your Haftpflichtversicherung (private liability insurance) to see whether it covers rented cars.
|
Any damages to the car will be charged to you. A total damage would cost you somewhere around 10.000 Euro, so please be careful.
|
||||||
|
Check your Haftpflichtversicherung (private liability insurance) to see whether it covers rented cars.
|
||||||
{: .notice--danger}
|
{: .notice--danger}
|
||||||
|
|
||||||
[Benzinrechner](https://benzinrechner.info/en){: .btn .btn--large}
|
[Benzinrechner](https://benzinrechner.info/en)
|
||||||
|
|
Before Width: | Height: | Size: 94 KiB |
|
@ -14,8 +14,8 @@ tags = [
|
||||||
"work",
|
"work",
|
||||||
"3D printing",
|
"3D printing",
|
||||||
]
|
]
|
||||||
|
|
||||||
[extra]
|
[extra]
|
||||||
banner = "eins zwo vier logo.png"
|
|
||||||
show_copyright = true
|
show_copyright = true
|
||||||
show_shares = true
|
show_shares = true
|
||||||
featured = true
|
featured = true
|
||||||
|
|
|
@ -1,23 +1,48 @@
|
||||||
+++
|
+++
|
||||||
title = "einszwovier: making of"
|
title = "zola - a switch to rust"
|
||||||
date = 2025-05-16
|
date = 2025-05-16
|
||||||
authors = ["Aron Petau", "Friedrich Weber"]
|
authors = ["Aron Petau", "Friedrich Weber"]
|
||||||
description = "The story of our new Makerspace: studio einszwovier"
|
description = "revamping my website, futureproofing"
|
||||||
|
draft = true
|
||||||
|
|
||||||
[taxonomies]
|
[taxonomies]
|
||||||
tags = [
|
tags = [
|
||||||
"making",
|
"rust",
|
||||||
"education",
|
"programming",
|
||||||
"democratic",
|
"static site generator",
|
||||||
"engineering",
|
"blogging",
|
||||||
|
"hosting",
|
||||||
"experiment",
|
"experiment",
|
||||||
"work",
|
"private",
|
||||||
"3D printing",
|
|
||||||
]
|
]
|
||||||
[extra]
|
[extra]
|
||||||
banner = "eins zwo vier logo.png"
|
|
||||||
show_copyright = true
|
show_copyright = true
|
||||||
show_shares = true
|
show_shares = true
|
||||||
featured = true
|
featured = false
|
||||||
draft = true
|
|
||||||
+++
|
+++
|
||||||
|
|
||||||
|
For years, Jekyll was my go-to for building static websites. It was familiar, widely supported, and part of the broader Ruby ecosystem. But over time, my frustrations grew—slow builds, complicated plugin setups, and a dependency stack that never felt quite right. Recently, I made the leap to **Zola**, a Rust-powered static site generator, and I don't see myself going back.
|
||||||
|
|
||||||
|
## Rust Feels Right
|
||||||
|
|
||||||
|
I’ve always admired Rust for its speed, safety, and modern tooling. Using a static site generator built with Rust just made sense. **Zola is fast**—blazing fast. Even during local development, rebuilds are near-instant, and that alone makes the writing process smoother and more enjoyable.
|
||||||
|
|
||||||
|
Plus, using something written in Rust means fewer external dependencies, no bundler hell, and zero Ruby setup headaches. I can just download the binary, run it, and get going. It respects my time.
|
||||||
|
|
||||||
|
## Zola Is Thoughtfully Designed
|
||||||
|
|
||||||
|
Beyond performance, Zola is simply well-designed. Its template syntax (thanks to Tera) is more powerful and readable than Liquid. The built-in shortcodes, pagination, and asset pipelines all feel cohesive and purposeful. There’s very little “configuration over convention” fatigue that Jekyll often gave me.
|
||||||
|
|
||||||
|
And even though both systems are Markdown-based, migrating wasn't just a matter of copy-pasting files. I had to rethink frontmatter, adjust templates, and wrangle image paths and shortcodes. The structure and behavior are different enough that it felt like a real rebuild—not just a port.
|
||||||
|
|
||||||
|
## Duckquill Made me switch
|
||||||
|
|
||||||
|
The real catalyst, though? **Duckquill**, a stunning Zola theme built by [Daudix](https://github.com/daudix). It struck the perfect balance between minimalism and elegance—exactly the aesthetic I wanted but could never quite achieve with Jekyll. Duckquill didn’t just make Zola usable for me; it made it *irresistible*.
|
||||||
|
|
||||||
|
What really sets **Duckquill** apart—beyond its clean typography and smart layout—is how well it supports a vision of digital autonomy. The theme comes with **Mastodon-powered comments**, allowing for lightweight, federated interaction without relying on big centralized platforms. This fits perfectly with my goal of reclaiming control through **self-hosting**. Whether it's running my own site, owning my content, or interacting through the fediverse, Duckquill reinforces those values rather than working against them. It’s a rare example of design and infrastructure aligning with personal principles.
|
||||||
|
|
||||||
|
## Final Thoughts
|
||||||
|
|
||||||
|
Switching from Jekyll to Zola wasn’t effortless, but it was absolutely worth it. I now have a faster, more reliable, and better-looking site that’s easier to maintain and feels like it fits my tooling philosophy.
|
||||||
|
|
||||||
|
If you're feeling the weight of your current setup, maybe it's time to try Zola—and give Duckquill a spin while you're at it.
|
||||||
|
|
BIN
content/project/2025-05-16-einszwovier-opening/bench.jpeg
Normal file
After Width: | Height: | Size: 190 KiB |
BIN
content/project/2025-05-16-einszwovier-opening/cad.jpeg
Normal file
After Width: | Height: | Size: 71 KiB |
|
@ -21,3 +21,37 @@ show_shares = true
|
||||||
featured = true
|
featured = true
|
||||||
draft = true
|
draft = true
|
||||||
+++
|
+++
|
||||||
|
|
||||||
|
## Die Entstehung von studio einszwovier
|
||||||
|
|
||||||
|
## August 2024
|
||||||
|
|
||||||
|
Wir begannen mit dem Aufbau und der Planung der Raumgestaltung sowie der Ausstattung. Dabei hatten wir die Möglichkeit, die Werkbank selbst aus Holz zu bauen – so wurde sie zu etwas Eigenem.
|
||||||
|
|
||||||
|
## Dezember 2024 – Ein Raum für Ideen wird Realität
|
||||||
|
|
||||||
|
Nach monatelanger Planung, Organisation und Vorfreude war es im Dezember 2024 endlich so weit: Unser Maker Space **„studio einszwovier“** öffnete offiziell seine Türen.
|
||||||
|
Mitten im Schulalltag entstand eine innovative Lernumgebung – eine, die Kreativität, Technologie und Bildungsgerechtigkeit miteinander verbindet.
|
||||||
|
|
||||||
|
## Vom Konzept zur Wirklichkeit
|
||||||
|
|
||||||
|
Die Idee war klar: Ein Raum, in dem „Making“ greifbar wird – durch selbstbestimmtes und spielerisches Arbeiten mit analogen und digitalen Werkzeugen. Lernende sollen ihren Lernprozess mitgestalten, ihre individuellen Stärken entdecken und die motivierende Kraft des Selbermachens erleben.
|
||||||
|
|
||||||
|
Dazu wurde der Raum mit modernen Werkzeugen ausgestattet: **3D-Drucker**, **Lasercutter**, **Mikrocontroller** sowie Equipment für **Holzarbeiten** und **Textildruck** ermöglichen praktisches, projektbasiertes Lernen.
|
||||||
|
|
||||||
|
## Ein Ort für freies und entdeckendes Lernen
|
||||||
|
|
||||||
|
Geleitet von Aron und Friedrich – beide Masterstudenten im Studiengang *Design + Computation* in Berlin – bietet das „studio einszwovier“ Zugang zu Werkzeugen, Materialien und Wissen.
|
||||||
|
Es ist ein Raum für offenes, exploratives Lernen, das nicht nur digitale Technologien, sondern auch Kreativität, Problemlösung und Eigeninitiative in den Mittelpunkt stellt.
|
||||||
|
Die Schüler*innen sind eingeladen, sowohl an thematisch geführten Kursen als auch an offenen Tüftelzeiten teilzunehmen.
|
||||||
|
|
||||||
|
## Offene Türen für kreative Köpfe
|
||||||
|
|
||||||
|
Das „studio einszwovier“ ist **montags bis mittwochs von 11:00 bis 15:00 Uhr** geöffnet.
|
||||||
|
Eine spezielle **Open Lab Time** findet **dienstags von 13:30 bis 15:00 Uhr** statt.
|
||||||
|
Alle sind herzlich eingeladen, vorbeizukommen, Ideen zu teilen und loszulegen.
|
||||||
|
|
||||||
|
## Ein Raum für die Zukunft
|
||||||
|
|
||||||
|
Mit dem studio einszwovier haben wir einen Ort geschaffen, an dem das Lernen durch eigenes Tun im Mittelpunkt steht – und damit sowohl praktische als auch digitale Kompetenzen für die Zukunft gefördert werden.
|
||||||
|
Ein Ort, an dem aus Ideen greifbare Ergebnisse entstehen und an dem die Lernkultur unserer Schule auf nachhaltige Weise wächst.
|
||||||
|
|
|
@ -3,6 +3,7 @@ title = "einszwovier: making of"
|
||||||
date = 2025-05-16
|
date = 2025-05-16
|
||||||
authors = ["Aron Petau", "Friedrich Weber"]
|
authors = ["Aron Petau", "Friedrich Weber"]
|
||||||
description = "The story of our new Makerspace: studio einszwovier"
|
description = "The story of our new Makerspace: studio einszwovier"
|
||||||
|
draft = false
|
||||||
|
|
||||||
[taxonomies]
|
[taxonomies]
|
||||||
tags = [
|
tags = [
|
||||||
|
@ -18,8 +19,69 @@ tags = [
|
||||||
banner = "eins zwo vier logo.png"
|
banner = "eins zwo vier logo.png"
|
||||||
show_copyright = true
|
show_copyright = true
|
||||||
show_shares = true
|
show_shares = true
|
||||||
featured = true
|
featured = false
|
||||||
draft = true
|
|
||||||
+++
|
+++
|
||||||
|
|
||||||
|
## The Making of studio einszwovier
|
||||||
|
|
||||||
|
## August 2024
|
||||||
|
|
||||||
|
We started constructing and planning the layout and equipment for the room. We had the chance to build the wooden workbench ourselves, making it our own.
|
||||||
|
|
||||||
|
## December 2024 – A Space for Ideas Becomes Reality
|
||||||
|
|
||||||
|
After months of planning, organizing, and anticipation, it finally happened in December 2024: our Maker Space **“studio einszwovier”** officially opened its doors.
|
||||||
|
In the midst of everyday school life, an innovative learning environment came to life—one that combines creativity, technology, and educational equity.
|
||||||
|
|
||||||
|
## From Concept to Reality
|
||||||
|
|
||||||
|
The idea was clear: a space where “making” becomes tangible—through self-directed and playful work with analog and digital tools. Learners should be able to shape their learning process, discover their individual strengths, and experience the empowering motivation of doing things themselves.
|
||||||
|
|
||||||
|
To support that, the room was equipped with state-of-the-art tools: **3D printers**, **laser cutters**, **microcontrollers**, and equipment for **woodworking** and **textile printing** enable hands-on, project-based learning.
|
||||||
|
|
||||||
|
## A Place for Free and Explorative Learning
|
||||||
|
|
||||||
|
Led by Aron and Friedrich — both graduate students in *Design + Computation* in Berlin—the “studio einszwovier” provides access to tools, materials, and knowledge.
|
||||||
|
It’s a place for open-ended, explorative learning that emphasizes not just digital technologies, but also creativity, problem-solving, and initiative.
|
||||||
|
The students are invited to join both courses with a predefined focus and open "tüftling".
|
||||||
|
|
||||||
|
## Open Doors for Creative Minds
|
||||||
|
|
||||||
|
“studio einszwovier” is open **Tuesdays to Thursdays from 11:00 AM to :00 PM**.
|
||||||
|
A dedicated **open lab time** is available **Wednesdays from 1:30 PM to 3:00 PM**.
|
||||||
|
Everyone is welcome to drop in, share ideas, and get started.
|
||||||
|
|
||||||
|
## A Space for the Future
|
||||||
|
|
||||||
|
With studio einszwovier, we’ve created a space where learning through hands-on experience takes center stage—promoting both practical and digital skills for the future.
|
||||||
|
It’s a place where ideas become tangible outcomes and where the learning culture of our school grows in a lasting and meaningful way.
|
||||||
|
|
||||||
|
{% gallery() %}
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"file": "eins zwo vier logo.png",
|
||||||
|
"title": "",
|
||||||
|
"alt": "our new logo"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"file": "lasercutter.jpeg",
|
||||||
|
"alt": "",
|
||||||
|
"title": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"file": "printing-lamps.jpeg",
|
||||||
|
"alt": "",
|
||||||
|
"title": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"file": "workbench.jpeg",
|
||||||
|
"alt": "",
|
||||||
|
"title": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"file": "cad.jpeg",
|
||||||
|
"alt": "",
|
||||||
|
"title": ""
|
||||||
|
}
|
||||||
|
]
|
||||||
|
{% end %}
|
||||||
|
|
BIN
content/project/2025-05-16-einszwovier-opening/lasercutter.jpeg
Normal file
After Width: | Height: | Size: 221 KiB |
After Width: | Height: | Size: 202 KiB |
BIN
content/project/2025-05-16-einszwovier-opening/workbench.jpeg
Normal file
After Width: | Height: | Size: 73 KiB |
|
@ -3,8 +3,8 @@ title = "Übersetzung: Aron's Blog"
|
||||||
sort_by = "date"
|
sort_by = "date"
|
||||||
template = "article_list.html"
|
template = "article_list.html"
|
||||||
page_template = "article.html"
|
page_template = "article.html"
|
||||||
paginate_by = 8
|
paginate_by = 10
|
||||||
+++
|
+++
|
||||||
|
|
||||||
Find all my projects here.
|
Hier ist eine Übersicht meiner Projekte.
|
||||||
They are sorted by date, you can also filter by tags.
|
Sie sind sortiert nach Datum, aber du kannst auch Themen durch Tags filtern.
|
||||||
|
|
BIN
public/404.gif
Before Width: | Height: | Size: 4 KiB |
BIN
public/404.png
Before Width: | Height: | Size: 340 B |
Before Width: | Height: | Size: 28 KiB |
1
public/auto-render.min.js
vendored
|
@ -1 +0,0 @@
|
||||||
!function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t(require("katex")):"function"==typeof define&&define.amd?define(["katex"],t):"object"==typeof exports?exports.renderMathInElement=t(require("katex")):e.renderMathInElement=t(e.katex)}("undefined"!=typeof self?self:this,(function(e){return function(){"use strict";var t={771:function(t){t.exports=e}},n={};function r(e){var o=n[e];if(void 0!==o)return o.exports;var i=n[e]={exports:{}};return t[e](i,i.exports,r),i.exports}r.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return r.d(t,{a:t}),t},r.d=function(e,t){for(var n in t)r.o(t,n)&&!r.o(e,n)&&Object.defineProperty(e,n,{enumerable:!0,get:t[n]})},r.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)};var o={};return function(){r.d(o,{default:function(){return d}});var e=r(771),t=r.n(e);const n=function(e,t,n){let r=n,o=0;const i=e.length;for(;r<t.length;){const n=t[r];if(o<=0&&t.slice(r,r+i)===e)return r;"\\"===n?r++:"{"===n?o++:"}"===n&&o--,r++}return-1},i=/^\\begin{/;var a=function(e,t){let r;const o=[],a=new RegExp("("+t.map((e=>e.left.replace(/[-/\\^$*+?.()|[\]{}]/g,"\\$&"))).join("|")+")");for(;r=e.search(a),-1!==r;){r>0&&(o.push({type:"text",data:e.slice(0,r)}),e=e.slice(r));const a=t.findIndex((t=>e.startsWith(t.left)));if(r=n(t[a].right,e,t[a].left.length),-1===r)break;const l=e.slice(0,r+t[a].right.length),s=i.test(l)?l:e.slice(t[a].left.length,r);o.push({type:"math",data:s,rawData:l,display:t[a].display}),e=e.slice(r+t[a].right.length)}return""!==e&&o.push({type:"text",data:e}),o};const l=function(e,n){const r=a(e,n.delimiters);if(1===r.length&&"text"===r[0].type)return null;const o=document.createDocumentFragment();for(let e=0;e<r.length;e++)if("text"===r[e].type)o.appendChild(document.createTextNode(r[e].data));else{const i=document.createElement("span");let a=r[e].data;n.displayMode=r[e].display;try{n.preProcess&&(a=n.preProcess(a)),t().render(a,i,n)}catch(i){if(!(i instanceof t().ParseError))throw i;n.errorCallback("KaTeX auto-render: Failed to parse `"+r[e].data+"` with ",i),o.appendChild(document.createTextNode(r[e].rawData));continue}o.appendChild(i)}return o},s=function(e,t){for(let n=0;n<e.childNodes.length;n++){const r=e.childNodes[n];if(3===r.nodeType){let o=r.textContent,i=r.nextSibling,a=0;for(;i&&i.nodeType===Node.TEXT_NODE;)o+=i.textContent,i=i.nextSibling,a++;const s=l(o,t);if(s){for(let e=0;e<a;e++)r.nextSibling.remove();n+=s.childNodes.length-1,e.replaceChild(s,r)}else n+=a}else if(1===r.nodeType){const e=" "+r.className+" ";-1===t.ignoredTags.indexOf(r.nodeName.toLowerCase())&&t.ignoredClasses.every((t=>-1===e.indexOf(" "+t+" ")))&&s(r,t)}}};var d=function(e,t){if(!e)throw new Error("No element provided to render");const n={};for(const e in t)t.hasOwnProperty(e)&&(n[e]=t[e]);n.delimiters=n.delimiters||[{left:"$$",right:"$$",display:!0},{left:"\\(",right:"\\)",display:!1},{left:"\\begin{equation}",right:"\\end{equation}",display:!0},{left:"\\begin{align}",right:"\\end{align}",display:!0},{left:"\\begin{alignat}",right:"\\end{alignat}",display:!0},{left:"\\begin{gather}",right:"\\end{gather}",display:!0},{left:"\\begin{CD}",right:"\\end{CD}",display:!0},{left:"\\[",right:"\\]",display:!0}],n.ignoredTags=n.ignoredTags||["script","noscript","style","textarea","pre","code","option"],n.ignoredClasses=n.ignoredClasses||[],n.errorCallback=n.errorCallback||console.error,n.macros=n.macros||{},s(e,n)}}(),o=o.default}()}));
|
|
BIN
public/card.png
Before Width: | Height: | Size: 1.2 MiB |
|
@ -1,27 +0,0 @@
|
||||||
const closable = document.querySelectorAll("details.closable");
|
|
||||||
|
|
||||||
closable.forEach((detail) => {
|
|
||||||
detail.addEventListener("toggle", () => {
|
|
||||||
if (detail.open) setTargetDetail(detail);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
function setTargetDetail(targetDetail) {
|
|
||||||
closable.forEach((detail) => {
|
|
||||||
if (detail !== targetDetail) {
|
|
||||||
detail.open = false;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
document.addEventListener("click", function (event) {
|
|
||||||
const isClickInsideDetail = [...closable].some((detail) =>
|
|
||||||
detail.contains(event.target)
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!isClickInsideDetail) {
|
|
||||||
closable.forEach((detail) => {
|
|
||||||
detail.open = false;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
|
@ -1,406 +0,0 @@
|
||||||
// Taken from https://carlschwan.eu/2020/12/29/adding-comments-to-your-static-blog-with-mastodon/
|
|
||||||
// Attachment, card, and spoiler code taken from https://github.com/cassidyjames/cassidyjames.github.io/blob/99782788a7e3ba3cc52d6803010873abd1b02b9e/_includes/comments.html#L251-L296
|
|
||||||
|
|
||||||
let blogPostAuthorText = document.getElementById("blog-post-author-text").textContent;
|
|
||||||
let boostsFromText = document.getElementById("boosts-from-text").textContent;
|
|
||||||
let dateLocale = document.getElementById("date-locale").textContent;
|
|
||||||
let favesFromText = document.getElementById("faves-from-text").textContent;
|
|
||||||
let host = document.getElementById("host").textContent;
|
|
||||||
let id = document.getElementById("id").textContent;
|
|
||||||
let lazyAsyncImage = document.getElementById("lazy-async-image").textContent;
|
|
||||||
let loadingText = document.getElementById("loading-text").textContent;
|
|
||||||
let noCommentsText = document.getElementById("no-comments-text").textContent;
|
|
||||||
let relAttributes = document.getElementById("rel-attributes").textContent;
|
|
||||||
let reloadText = document.getElementById("reload-text").textContent;
|
|
||||||
let sensitiveText = document.getElementById("sensitive-text").textContent;
|
|
||||||
let user = document.getElementById("user").textContent;
|
|
||||||
let viewCommentText = document.getElementById("view-comment-text").textContent;
|
|
||||||
let viewProfileText = document.getElementById("view-profile-text").textContent;
|
|
||||||
|
|
||||||
document.getElementById("load-comments").addEventListener("click", loadComments);
|
|
||||||
|
|
||||||
function escapeHtml(unsafe) {
|
|
||||||
return unsafe
|
|
||||||
.replace(/&/g, "&")
|
|
||||||
.replace(/</g, "<")
|
|
||||||
.replace(/>/g, ">")
|
|
||||||
.replace(/"/g, """)
|
|
||||||
.replace(/'/g, "'");
|
|
||||||
}
|
|
||||||
function emojify(input, emojis) {
|
|
||||||
let output = input;
|
|
||||||
|
|
||||||
emojis.forEach((emoji) => {
|
|
||||||
let picture = document.createElement("picture");
|
|
||||||
|
|
||||||
let source = document.createElement("source");
|
|
||||||
source.setAttribute("srcset", escapeHtml(emoji.url));
|
|
||||||
source.setAttribute("media", "(prefers-reduced-motion: no-preference)");
|
|
||||||
|
|
||||||
let img = document.createElement("img");
|
|
||||||
img.className = "emoji";
|
|
||||||
img.setAttribute("src", escapeHtml(emoji.static_url));
|
|
||||||
img.setAttribute("alt", `:${emoji.shortcode}:`);
|
|
||||||
img.setAttribute("title", `:${emoji.shortcode}:`);
|
|
||||||
if (lazyAsyncImage == "true") {
|
|
||||||
img.setAttribute("decoding", "async");
|
|
||||||
img.setAttribute("loading", "lazy");
|
|
||||||
}
|
|
||||||
|
|
||||||
picture.appendChild(source);
|
|
||||||
picture.appendChild(img);
|
|
||||||
|
|
||||||
output = output.replace(`:${emoji.shortcode}:`, picture.outerHTML);
|
|
||||||
});
|
|
||||||
|
|
||||||
return output;
|
|
||||||
}
|
|
||||||
|
|
||||||
function loadComments() {
|
|
||||||
let commentsWrapper = document.getElementById("comments-wrapper");
|
|
||||||
commentsWrapper.innerHTML = "";
|
|
||||||
|
|
||||||
let loadCommentsButton = document.getElementById("load-comments");
|
|
||||||
loadCommentsButton.innerHTML = loadingText;
|
|
||||||
loadCommentsButton.disabled = true;
|
|
||||||
|
|
||||||
fetch(`https://${host}/api/v1/statuses/${id}/context`)
|
|
||||||
.then(function (response) {
|
|
||||||
return response.json();
|
|
||||||
})
|
|
||||||
.then(function (data) {
|
|
||||||
let descendants = data["descendants"];
|
|
||||||
if (
|
|
||||||
descendants &&
|
|
||||||
Array.isArray(descendants) &&
|
|
||||||
descendants.length > 0
|
|
||||||
) {
|
|
||||||
commentsWrapper.innerHTML = "";
|
|
||||||
|
|
||||||
descendants.forEach(function (status) {
|
|
||||||
console.log(descendants);
|
|
||||||
if (status.account.display_name.length > 0) {
|
|
||||||
status.account.display_name = escapeHtml(
|
|
||||||
status.account.display_name
|
|
||||||
);
|
|
||||||
status.account.display_name = emojify(
|
|
||||||
status.account.display_name,
|
|
||||||
status.account.emojis
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
status.account.display_name = status.account.username;
|
|
||||||
}
|
|
||||||
|
|
||||||
let instance = "";
|
|
||||||
if (status.account.acct.includes("@")) {
|
|
||||||
instance = status.account.acct.split("@")[1];
|
|
||||||
} else {
|
|
||||||
instance = host;
|
|
||||||
}
|
|
||||||
|
|
||||||
const isReply = status.in_reply_to_id !== id;
|
|
||||||
|
|
||||||
let op = false;
|
|
||||||
if (status.account.acct == user) {
|
|
||||||
op = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
status.content = emojify(status.content, status.emojis);
|
|
||||||
|
|
||||||
let comment = document.createElement("article");
|
|
||||||
comment.id = `comment-${status.id}`;
|
|
||||||
comment.className = isReply ? "comment comment-reply" : "comment";
|
|
||||||
comment.setAttribute("itemprop", "comment");
|
|
||||||
comment.setAttribute("itemtype", "http://schema.org/Comment");
|
|
||||||
|
|
||||||
let avatarSource = document.createElement("source");
|
|
||||||
avatarSource.setAttribute(
|
|
||||||
"srcset",
|
|
||||||
escapeHtml(status.account.avatar)
|
|
||||||
);
|
|
||||||
avatarSource.setAttribute(
|
|
||||||
"media",
|
|
||||||
"(prefers-reduced-motion: no-preference)"
|
|
||||||
);
|
|
||||||
|
|
||||||
let avatarImg = document.createElement("img");
|
|
||||||
avatarImg.className = "avatar";
|
|
||||||
avatarImg.setAttribute(
|
|
||||||
"src",
|
|
||||||
escapeHtml(status.account.avatar_static)
|
|
||||||
);
|
|
||||||
avatarImg.setAttribute(
|
|
||||||
"alt",
|
|
||||||
`@${status.account.username}@${instance} avatar`
|
|
||||||
);
|
|
||||||
if (lazyAsyncImage == "true") {
|
|
||||||
avatarImg.setAttribute("decoding", "async");
|
|
||||||
avatarImg.setAttribute("loading", "lazy");
|
|
||||||
}
|
|
||||||
|
|
||||||
let avatarPicture = document.createElement("picture");
|
|
||||||
avatarPicture.appendChild(avatarSource);
|
|
||||||
avatarPicture.appendChild(avatarImg);
|
|
||||||
|
|
||||||
let avatar = document.createElement("a");
|
|
||||||
avatar.className = "avatar-link";
|
|
||||||
avatar.setAttribute("href", status.account.url);
|
|
||||||
avatar.setAttribute("rel", relAttributes);
|
|
||||||
avatar.setAttribute(
|
|
||||||
"title",
|
|
||||||
`${viewProfileText} @${status.account.username}@${instance}`
|
|
||||||
);
|
|
||||||
avatar.appendChild(avatarPicture);
|
|
||||||
comment.appendChild(avatar);
|
|
||||||
|
|
||||||
let instanceBadge = document.createElement("a");
|
|
||||||
instanceBadge.className = "instance";
|
|
||||||
instanceBadge.setAttribute("href", status.account.url);
|
|
||||||
instanceBadge.setAttribute(
|
|
||||||
"title",
|
|
||||||
`@${status.account.username}@${instance}`
|
|
||||||
);
|
|
||||||
instanceBadge.setAttribute("rel", relAttributes);
|
|
||||||
instanceBadge.textContent = instance;
|
|
||||||
|
|
||||||
let display = document.createElement("span");
|
|
||||||
display.className = "display";
|
|
||||||
display.setAttribute("itemprop", "author");
|
|
||||||
display.setAttribute("itemtype", "http://schema.org/Person");
|
|
||||||
display.innerHTML = status.account.display_name;
|
|
||||||
|
|
||||||
let header = document.createElement("header");
|
|
||||||
header.className = "author";
|
|
||||||
header.appendChild(display);
|
|
||||||
header.appendChild(instanceBadge);
|
|
||||||
comment.appendChild(header);
|
|
||||||
|
|
||||||
let permalink = document.createElement("a");
|
|
||||||
permalink.setAttribute("href", status.url);
|
|
||||||
permalink.setAttribute("itemprop", "url");
|
|
||||||
permalink.setAttribute("title", `${viewCommentText} ${instance}`);
|
|
||||||
permalink.setAttribute("rel", relAttributes);
|
|
||||||
permalink.textContent = new Date(
|
|
||||||
status.created_at
|
|
||||||
).toLocaleString(dateLocale, {
|
|
||||||
dateStyle: "long",
|
|
||||||
timeStyle: "short",
|
|
||||||
});
|
|
||||||
|
|
||||||
let timestamp = document.createElement("time");
|
|
||||||
timestamp.setAttribute("datetime", status.created_at);
|
|
||||||
timestamp.appendChild(permalink);
|
|
||||||
permalink.classList.add("external");
|
|
||||||
comment.appendChild(timestamp);
|
|
||||||
|
|
||||||
let main = document.createElement("main");
|
|
||||||
main.setAttribute("itemprop", "text");
|
|
||||||
|
|
||||||
if (status.sensitive == true || status.spoiler_text != "") {
|
|
||||||
let summary = document.createElement("summary");
|
|
||||||
if (status.spoiler_text == "") {
|
|
||||||
status.spoiler_text == sensitiveText;
|
|
||||||
}
|
|
||||||
summary.innerHTML = status.spoiler_text;
|
|
||||||
|
|
||||||
let spoiler = document.createElement("details");
|
|
||||||
spoiler.appendChild(summary);
|
|
||||||
spoiler.innerHTML += status.content;
|
|
||||||
|
|
||||||
main.appendChild(spoiler);
|
|
||||||
} else {
|
|
||||||
main.innerHTML = status.content;
|
|
||||||
}
|
|
||||||
comment.appendChild(main);
|
|
||||||
|
|
||||||
let attachments = status.media_attachments;
|
|
||||||
let SUPPORTED_MEDIA = ["image", "video", "gifv", "audio"];
|
|
||||||
let media = document.createElement("div");
|
|
||||||
media.className = "attachments";
|
|
||||||
if (
|
|
||||||
attachments &&
|
|
||||||
Array.isArray(attachments) &&
|
|
||||||
attachments.length > 0
|
|
||||||
) {
|
|
||||||
attachments.forEach((attachment) => {
|
|
||||||
if (SUPPORTED_MEDIA.includes(attachment.type)) {
|
|
||||||
|
|
||||||
let mediaElement;
|
|
||||||
switch (attachment.type) {
|
|
||||||
case "image":
|
|
||||||
mediaElement = document.createElement("img");
|
|
||||||
mediaElement.setAttribute("src", attachment.preview_url);
|
|
||||||
|
|
||||||
if (attachment.description != null) {
|
|
||||||
mediaElement.setAttribute("alt", attachment.description);
|
|
||||||
mediaElement.setAttribute("title", attachment.description);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (lazyAsyncImage == "true") {
|
|
||||||
mediaElement.setAttribute("decoding", "async");
|
|
||||||
mediaElement.setAttribute("loading", "lazy");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (status.sensitive == true) {
|
|
||||||
mediaElement.classList.add("spoiler");
|
|
||||||
}
|
|
||||||
|
|
||||||
media.appendChild(mediaElement);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "video":
|
|
||||||
mediaElement = document.createElement("video");
|
|
||||||
mediaElement.setAttribute("src", attachment.url);
|
|
||||||
mediaElement.setAttribute("controls", "");
|
|
||||||
|
|
||||||
if (attachment.description != null) {
|
|
||||||
mediaElement.setAttribute("aria-title", attachment.description);
|
|
||||||
mediaElement.setAttribute("title", attachment.description);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (status.sensitive == true) {
|
|
||||||
mediaElement.classList.add("spoiler");
|
|
||||||
}
|
|
||||||
|
|
||||||
media.appendChild(mediaElement);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "gifv":
|
|
||||||
mediaElement = document.createElement("video");
|
|
||||||
mediaElement.setAttribute("src", attachment.url);
|
|
||||||
mediaElement.setAttribute("autoplay", "");
|
|
||||||
mediaElement.setAttribute("playsinline", "");
|
|
||||||
mediaElement.setAttribute("loop", "");
|
|
||||||
|
|
||||||
if (attachment.description != null) {
|
|
||||||
mediaElement.setAttribute("aria-title", attachment.description);
|
|
||||||
mediaElement.setAttribute("title", attachment.description);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (status.sensitive == true) {
|
|
||||||
mediaElement.classList.add("spoiler");
|
|
||||||
}
|
|
||||||
|
|
||||||
media.appendChild(mediaElement);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "audio":
|
|
||||||
mediaElement = document.createElement("audio");
|
|
||||||
mediaElement.setAttribute("src", attachment.url);
|
|
||||||
mediaElement.setAttribute("controls", "");
|
|
||||||
|
|
||||||
if (attachment.description != null) {
|
|
||||||
mediaElement.setAttribute("aria-title", attachment.description);
|
|
||||||
mediaElement.setAttribute("title", attachment.description);
|
|
||||||
}
|
|
||||||
|
|
||||||
media.appendChild(mediaElement);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
let mediaLink = document.createElement("a");
|
|
||||||
mediaLink.setAttribute("href", attachment.url);
|
|
||||||
mediaLink.setAttribute("rel", relAttributes);
|
|
||||||
mediaLink.appendChild(mediaElement);
|
|
||||||
|
|
||||||
media.appendChild(mediaLink);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
comment.appendChild(media);
|
|
||||||
}
|
|
||||||
|
|
||||||
let interactions = document.createElement("footer");
|
|
||||||
|
|
||||||
let boosts = document.createElement("a");
|
|
||||||
boosts.className = "boosts";
|
|
||||||
boosts.setAttribute("href", `${status.url}/reblogs`);
|
|
||||||
boosts.setAttribute("title", `${boostsFromText}`.replace("$INSTANCE", instance));
|
|
||||||
|
|
||||||
let boostsIcon = document.createElement("i");
|
|
||||||
boostsIcon.className = "icon";
|
|
||||||
boosts.appendChild(boostsIcon);
|
|
||||||
boosts.insertAdjacentHTML('beforeend', ` ${status.reblogs_count}`);
|
|
||||||
interactions.appendChild(boosts);
|
|
||||||
|
|
||||||
let faves = document.createElement("a");
|
|
||||||
faves.className = "faves";
|
|
||||||
faves.setAttribute("href", `${status.url}/favourites`);
|
|
||||||
faves.setAttribute("title", `${favesFromText}`.replace("$INSTANCE", instance));
|
|
||||||
|
|
||||||
let favesIcon = document.createElement("i");
|
|
||||||
favesIcon.className = "icon";
|
|
||||||
faves.appendChild(favesIcon);
|
|
||||||
faves.insertAdjacentHTML('beforeend', ` ${status.favourites_count}`);
|
|
||||||
interactions.appendChild(faves);
|
|
||||||
comment.appendChild(interactions);
|
|
||||||
|
|
||||||
if (status.card != null) {
|
|
||||||
let cardFigure = document.createElement("figure");
|
|
||||||
|
|
||||||
if (status.card.image != null) {
|
|
||||||
let cardImg = document.createElement("img");
|
|
||||||
cardImg.setAttribute("src", status.card.image);
|
|
||||||
cardImg.classList.add("no-hover");
|
|
||||||
cardFigure.appendChild(cardImg);
|
|
||||||
}
|
|
||||||
|
|
||||||
let cardCaption = document.createElement("figcaption");
|
|
||||||
|
|
||||||
let cardTitle = document.createElement("strong");
|
|
||||||
cardTitle.innerHTML = status.card.title;
|
|
||||||
cardCaption.appendChild(cardTitle);
|
|
||||||
|
|
||||||
if (status.card.description != null && status.card.description.length > 0) {
|
|
||||||
let cardDescription = document.createElement("p");
|
|
||||||
cardDescription.innerHTML = status.card.description;
|
|
||||||
cardCaption.appendChild(cardDescription);
|
|
||||||
}
|
|
||||||
|
|
||||||
cardFigure.appendChild(cardCaption);
|
|
||||||
|
|
||||||
let card = document.createElement("a");
|
|
||||||
card.className = "card";
|
|
||||||
card.setAttribute("href", status.card.url);
|
|
||||||
card.setAttribute("rel", relAttributes);
|
|
||||||
card.appendChild(cardFigure);
|
|
||||||
|
|
||||||
comment.appendChild(card);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (op === true) {
|
|
||||||
comment.classList.add("op");
|
|
||||||
|
|
||||||
avatar.classList.add("op");
|
|
||||||
avatar.setAttribute(
|
|
||||||
"title",
|
|
||||||
`${blogPostAuthorText}: ` + avatar.getAttribute("title")
|
|
||||||
);
|
|
||||||
|
|
||||||
instanceBadge.classList.add("op");
|
|
||||||
instanceBadge.setAttribute(
|
|
||||||
"title",
|
|
||||||
`${blogPostAuthorText}: ` + instanceBadge.getAttribute("title")
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
commentsWrapper.innerHTML += comment.outerHTML;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
else {
|
|
||||||
var statusText = document.createElement("p");
|
|
||||||
statusText.innerHTML = noCommentsText;
|
|
||||||
statusText.setAttribute("id", "comments-status");
|
|
||||||
commentsWrapper.appendChild(statusText);
|
|
||||||
}
|
|
||||||
|
|
||||||
loadCommentsButton.innerHTML = reloadText;
|
|
||||||
})
|
|
||||||
.catch(function (error) {
|
|
||||||
console.error('Error loading comments:', error);
|
|
||||||
})
|
|
||||||
.finally(function () {
|
|
||||||
loadCommentsButton.disabled = false;
|
|
||||||
});
|
|
||||||
}
|
|
|
@ -1,57 +0,0 @@
|
||||||
// Based on https://www.roboleary.net/2022/01/13/copy-code-to-clipboard-blog.html
|
|
||||||
document.addEventListener("DOMContentLoaded", function () {
|
|
||||||
let blocks = document.querySelectorAll("pre[class^='language-']");
|
|
||||||
|
|
||||||
blocks.forEach((block) => {
|
|
||||||
if (navigator.clipboard) {
|
|
||||||
// Code block header title
|
|
||||||
let title = document.createElement("span");
|
|
||||||
let lang = block.getAttribute("data-lang");
|
|
||||||
title.innerHTML = lang;
|
|
||||||
|
|
||||||
// Copy button icon
|
|
||||||
let icon = document.createElement("i");
|
|
||||||
icon.classList.add("icon");
|
|
||||||
|
|
||||||
// Copy button
|
|
||||||
let button = document.createElement("button");
|
|
||||||
let copyCodeText = document.getElementById("copy-code-text").textContent;
|
|
||||||
button.setAttribute("title", copyCodeText)
|
|
||||||
button.appendChild(icon);
|
|
||||||
|
|
||||||
// Code block header
|
|
||||||
let header = document.createElement("div");
|
|
||||||
header.classList.add("header");
|
|
||||||
header.appendChild(title);
|
|
||||||
header.appendChild(button);
|
|
||||||
|
|
||||||
// Container that holds header and the code block itself
|
|
||||||
let container = document.createElement("div");
|
|
||||||
container.classList.add("pre-container");
|
|
||||||
container.appendChild(header);
|
|
||||||
|
|
||||||
// Move code block into the container
|
|
||||||
block.parentNode.insertBefore(container, block);
|
|
||||||
container.appendChild(block);
|
|
||||||
|
|
||||||
button.addEventListener("click", async () => {
|
|
||||||
await copyCode(block, header, button); // Pass the button here
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
async function copyCode(block, header, button) {
|
|
||||||
let code = block.querySelector("code");
|
|
||||||
let text = code.innerText;
|
|
||||||
|
|
||||||
await navigator.clipboard.writeText(text);
|
|
||||||
|
|
||||||
header.classList.add("active");
|
|
||||||
button.setAttribute("disabled", true);
|
|
||||||
|
|
||||||
header.addEventListener("animationend", () => {
|
|
||||||
header.classList.remove("active");
|
|
||||||
button.removeAttribute("disabled");
|
|
||||||
}, { once: true });
|
|
||||||
}
|
|
||||||
});
|
|
271
public/count.js
|
@ -1,271 +0,0 @@
|
||||||
// GoatCounter: https://www.goatcounter.com
|
|
||||||
// This file is released under the ISC license: https://opensource.org/licenses/ISC
|
|
||||||
;(function() {
|
|
||||||
'use strict';
|
|
||||||
|
|
||||||
if (window.goatcounter && window.goatcounter.vars) // Compatibility with very old version; do not use.
|
|
||||||
window.goatcounter = window.goatcounter.vars
|
|
||||||
else
|
|
||||||
window.goatcounter = window.goatcounter || {}
|
|
||||||
|
|
||||||
// Load settings from data-goatcounter-settings.
|
|
||||||
var s = document.querySelector('script[data-goatcounter]')
|
|
||||||
if (s && s.dataset.goatcounterSettings) {
|
|
||||||
try { var set = JSON.parse(s.dataset.goatcounterSettings) }
|
|
||||||
catch (err) { console.error('invalid JSON in data-goatcounter-settings: ' + err) }
|
|
||||||
for (var k in set)
|
|
||||||
if (['no_onload', 'no_events', 'allow_local', 'allow_frame', 'path', 'title', 'referrer', 'event'].indexOf(k) > -1)
|
|
||||||
window.goatcounter[k] = set[k]
|
|
||||||
}
|
|
||||||
|
|
||||||
var enc = encodeURIComponent
|
|
||||||
|
|
||||||
// Get all data we're going to send off to the counter endpoint.
|
|
||||||
var get_data = function(vars) {
|
|
||||||
var data = {
|
|
||||||
p: (vars.path === undefined ? goatcounter.path : vars.path),
|
|
||||||
r: (vars.referrer === undefined ? goatcounter.referrer : vars.referrer),
|
|
||||||
t: (vars.title === undefined ? goatcounter.title : vars.title),
|
|
||||||
e: !!(vars.event || goatcounter.event),
|
|
||||||
s: [window.screen.width, window.screen.height, (window.devicePixelRatio || 1)],
|
|
||||||
b: is_bot(),
|
|
||||||
q: location.search,
|
|
||||||
}
|
|
||||||
|
|
||||||
var rcb, pcb, tcb // Save callbacks to apply later.
|
|
||||||
if (typeof(data.r) === 'function') rcb = data.r
|
|
||||||
if (typeof(data.t) === 'function') tcb = data.t
|
|
||||||
if (typeof(data.p) === 'function') pcb = data.p
|
|
||||||
|
|
||||||
if (is_empty(data.r)) data.r = document.referrer
|
|
||||||
if (is_empty(data.t)) data.t = document.title
|
|
||||||
if (is_empty(data.p)) data.p = get_path()
|
|
||||||
|
|
||||||
if (rcb) data.r = rcb(data.r)
|
|
||||||
if (tcb) data.t = tcb(data.t)
|
|
||||||
if (pcb) data.p = pcb(data.p)
|
|
||||||
return data
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if a value is "empty" for the purpose of get_data().
|
|
||||||
var is_empty = function(v) { return v === null || v === undefined || typeof(v) === 'function' }
|
|
||||||
|
|
||||||
// See if this looks like a bot; there is some additional filtering on the
|
|
||||||
// backend, but these properties can't be fetched from there.
|
|
||||||
var is_bot = function() {
|
|
||||||
// Headless browsers are probably a bot.
|
|
||||||
var w = window, d = document
|
|
||||||
if (w.callPhantom || w._phantom || w.phantom)
|
|
||||||
return 150
|
|
||||||
if (w.__nightmare)
|
|
||||||
return 151
|
|
||||||
if (d.__selenium_unwrapped || d.__webdriver_evaluate || d.__driver_evaluate)
|
|
||||||
return 152
|
|
||||||
if (navigator.webdriver)
|
|
||||||
return 153
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
// Object to urlencoded string, starting with a ?.
|
|
||||||
var urlencode = function(obj) {
|
|
||||||
var p = []
|
|
||||||
for (var k in obj)
|
|
||||||
if (obj[k] !== '' && obj[k] !== null && obj[k] !== undefined && obj[k] !== false)
|
|
||||||
p.push(enc(k) + '=' + enc(obj[k]))
|
|
||||||
return '?' + p.join('&')
|
|
||||||
}
|
|
||||||
|
|
||||||
// Show a warning in the console.
|
|
||||||
var warn = function(msg) {
|
|
||||||
if (console && 'warn' in console)
|
|
||||||
console.warn('goatcounter: ' + msg)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get the endpoint to send requests to.
|
|
||||||
var get_endpoint = function() {
|
|
||||||
var s = document.querySelector('script[data-goatcounter]')
|
|
||||||
if (s && s.dataset.goatcounter)
|
|
||||||
return s.dataset.goatcounter
|
|
||||||
return (goatcounter.endpoint || window.counter) // counter is for compat; don't use.
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get current path.
|
|
||||||
var get_path = function() {
|
|
||||||
var loc = location,
|
|
||||||
c = document.querySelector('link[rel="canonical"][href]')
|
|
||||||
if (c) { // May be relative or point to different domain.
|
|
||||||
var a = document.createElement('a')
|
|
||||||
a.href = c.href
|
|
||||||
if (a.hostname.replace(/^www\./, '') === location.hostname.replace(/^www\./, ''))
|
|
||||||
loc = a
|
|
||||||
}
|
|
||||||
return (loc.pathname + loc.search) || '/'
|
|
||||||
}
|
|
||||||
|
|
||||||
// Run function after DOM is loaded.
|
|
||||||
var on_load = function(f) {
|
|
||||||
if (document.body === null)
|
|
||||||
document.addEventListener('DOMContentLoaded', function() { f() }, false)
|
|
||||||
else
|
|
||||||
f()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Filter some requests that we (probably) don't want to count.
|
|
||||||
goatcounter.filter = function() {
|
|
||||||
if ('visibilityState' in document && document.visibilityState === 'prerender')
|
|
||||||
return 'visibilityState'
|
|
||||||
if (!goatcounter.allow_frame && location !== parent.location)
|
|
||||||
return 'frame'
|
|
||||||
if (!goatcounter.allow_local && location.hostname.match(/(localhost$|^127\.|^10\.|^172\.(1[6-9]|2[0-9]|3[0-1])\.|^192\.168\.|^0\.0\.0\.0$)/))
|
|
||||||
return 'localhost'
|
|
||||||
if (!goatcounter.allow_local && location.protocol === 'file:')
|
|
||||||
return 'localfile'
|
|
||||||
if (localStorage && localStorage.getItem('skipgc') === 't')
|
|
||||||
return 'disabled with #toggle-goatcounter'
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get URL to send to GoatCounter.
|
|
||||||
window.goatcounter.url = function(vars) {
|
|
||||||
var data = get_data(vars || {})
|
|
||||||
if (data.p === null) // null from user callback.
|
|
||||||
return
|
|
||||||
data.rnd = Math.random().toString(36).substr(2, 5) // Browsers don't always listen to Cache-Control.
|
|
||||||
|
|
||||||
var endpoint = get_endpoint()
|
|
||||||
if (!endpoint)
|
|
||||||
return warn('no endpoint found')
|
|
||||||
|
|
||||||
return endpoint + urlencode(data)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Count a hit.
|
|
||||||
window.goatcounter.count = function(vars) {
|
|
||||||
var f = goatcounter.filter()
|
|
||||||
if (f)
|
|
||||||
return warn('not counting because of: ' + f)
|
|
||||||
var url = goatcounter.url(vars)
|
|
||||||
if (!url)
|
|
||||||
return warn('not counting because path callback returned null')
|
|
||||||
|
|
||||||
if (!navigator.sendBeacon(url)) {
|
|
||||||
// This mostly fails due to being blocked by CSP; try again with an
|
|
||||||
// image-based fallback.
|
|
||||||
var img = document.createElement('img')
|
|
||||||
img.src = url
|
|
||||||
img.style.position = 'absolute' // Affect layout less.
|
|
||||||
img.style.bottom = '0px'
|
|
||||||
img.style.width = '1px'
|
|
||||||
img.style.height = '1px'
|
|
||||||
img.loading = 'eager'
|
|
||||||
img.setAttribute('alt', '')
|
|
||||||
img.setAttribute('aria-hidden', 'true')
|
|
||||||
|
|
||||||
var rm = function() { if (img && img.parentNode) img.parentNode.removeChild(img) }
|
|
||||||
img.addEventListener('load', rm, false)
|
|
||||||
document.body.appendChild(img)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get a query parameter.
|
|
||||||
window.goatcounter.get_query = function(name) {
|
|
||||||
var s = location.search.substr(1).split('&')
|
|
||||||
for (var i = 0; i < s.length; i++)
|
|
||||||
if (s[i].toLowerCase().indexOf(name.toLowerCase() + '=') === 0)
|
|
||||||
return s[i].substr(name.length + 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Track click events.
|
|
||||||
window.goatcounter.bind_events = function() {
|
|
||||||
if (!document.querySelectorAll) // Just in case someone uses an ancient browser.
|
|
||||||
return
|
|
||||||
|
|
||||||
var send = function(elem) {
|
|
||||||
return function() {
|
|
||||||
goatcounter.count({
|
|
||||||
event: true,
|
|
||||||
path: (elem.dataset.goatcounterClick || elem.name || elem.id || ''),
|
|
||||||
title: (elem.dataset.goatcounterTitle || elem.title || (elem.innerHTML || '').substr(0, 200) || ''),
|
|
||||||
referrer: (elem.dataset.goatcounterReferrer || elem.dataset.goatcounterReferral || ''),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Array.prototype.slice.call(document.querySelectorAll("*[data-goatcounter-click]")).forEach(function(elem) {
|
|
||||||
if (elem.dataset.goatcounterBound)
|
|
||||||
return
|
|
||||||
var f = send(elem)
|
|
||||||
elem.addEventListener('click', f, false)
|
|
||||||
elem.addEventListener('auxclick', f, false) // Middle click.
|
|
||||||
elem.dataset.goatcounterBound = 'true'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add a "visitor counter" frame or image.
|
|
||||||
window.goatcounter.visit_count = function(opt) {
|
|
||||||
on_load(function() {
|
|
||||||
opt = opt || {}
|
|
||||||
opt.type = opt.type || 'html'
|
|
||||||
opt.append = opt.append || 'body'
|
|
||||||
opt.path = opt.path || get_path()
|
|
||||||
opt.attr = opt.attr || {width: '200', height: (opt.no_branding ? '60' : '80')}
|
|
||||||
|
|
||||||
opt.attr['src'] = get_endpoint() + 'er/' + enc(opt.path) + '.' + enc(opt.type) + '?'
|
|
||||||
if (opt.no_branding) opt.attr['src'] += '&no_branding=1'
|
|
||||||
if (opt.style) opt.attr['src'] += '&style=' + enc(opt.style)
|
|
||||||
if (opt.start) opt.attr['src'] += '&start=' + enc(opt.start)
|
|
||||||
if (opt.end) opt.attr['src'] += '&end=' + enc(opt.end)
|
|
||||||
|
|
||||||
var tag = {png: 'img', svg: 'img', html: 'iframe'}[opt.type]
|
|
||||||
if (!tag)
|
|
||||||
return warn('visit_count: unknown type: ' + opt.type)
|
|
||||||
|
|
||||||
if (opt.type === 'html') {
|
|
||||||
opt.attr['frameborder'] = '0'
|
|
||||||
opt.attr['scrolling'] = 'no'
|
|
||||||
}
|
|
||||||
|
|
||||||
var d = document.createElement(tag)
|
|
||||||
for (var k in opt.attr)
|
|
||||||
d.setAttribute(k, opt.attr[k])
|
|
||||||
|
|
||||||
var p = document.querySelector(opt.append)
|
|
||||||
if (!p)
|
|
||||||
return warn('visit_count: append not found: ' + opt.append)
|
|
||||||
p.appendChild(d)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Make it easy to skip your own views.
|
|
||||||
if (location.hash === '#toggle-goatcounter') {
|
|
||||||
if (localStorage.getItem('skipgc') === 't') {
|
|
||||||
localStorage.removeItem('skipgc', 't')
|
|
||||||
alert('GoatCounter tracking is now ENABLED in this browser.')
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
localStorage.setItem('skipgc', 't')
|
|
||||||
alert('GoatCounter tracking is now DISABLED in this browser until ' + location + ' is loaded again.')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!goatcounter.no_onload)
|
|
||||||
on_load(function() {
|
|
||||||
// 1. Page is visible, count request.
|
|
||||||
// 2. Page is not yet visible; wait until it switches to 'visible' and count.
|
|
||||||
// See #487
|
|
||||||
if (!('visibilityState' in document) || document.visibilityState === 'visible')
|
|
||||||
goatcounter.count()
|
|
||||||
else {
|
|
||||||
var f = function(e) {
|
|
||||||
if (document.visibilityState !== 'visible')
|
|
||||||
return
|
|
||||||
document.removeEventListener('visibilitychange', f)
|
|
||||||
goatcounter.count()
|
|
||||||
}
|
|
||||||
document.addEventListener('visibilitychange', f)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!goatcounter.no_events)
|
|
||||||
goatcounter.bind_events()
|
|
||||||
})
|
|
||||||
})();
|
|
|
@ -1,48 +0,0 @@
|
||||||
#image-gallery {
|
|
||||||
margin: 2rem 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.gallery {
|
|
||||||
column-count: 3;
|
|
||||||
column-gap: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.gallery-item {
|
|
||||||
break-inside: avoid;
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
text-align: center;
|
|
||||||
list-style: none; /* ← important! */
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
.gallery-item img {
|
|
||||||
width: 100%;
|
|
||||||
height: auto;
|
|
||||||
border-radius: 12px;
|
|
||||||
box-shadow: 0 4px 8px rgba(0,0,0,0.1);
|
|
||||||
transition: transform 0.2s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.gallery-item img:hover {
|
|
||||||
transform: scale(1.6);
|
|
||||||
}
|
|
||||||
|
|
||||||
.caption {
|
|
||||||
font-size: 0.9rem;
|
|
||||||
margin-top: 0.5rem;
|
|
||||||
margin-bottom: 0; /* ← just in case */
|
|
||||||
color: var(--fg-color);
|
|
||||||
list-style: none; /* ← extra-safe */
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
.gallery {
|
|
||||||
column-count: 2;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 480px) {
|
|
||||||
.gallery {
|
|
||||||
column-count: 1;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,28 +0,0 @@
|
||||||
.mermaid {
|
|
||||||
text-align: center;
|
|
||||||
margin-top: 1.5em;
|
|
||||||
margin-bottom: 1.5em;
|
|
||||||
padding: 1em;
|
|
||||||
border-radius: 0.5em;
|
|
||||||
background-color: var(--code-bg);
|
|
||||||
font-family: var(--code-font, monospace);
|
|
||||||
font-size: 0.9rem;
|
|
||||||
overflow-x: auto;
|
|
||||||
max-width: 100%;
|
|
||||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.08);
|
|
||||||
}
|
|
||||||
|
|
||||||
.mermaid strong {
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mermaid svg {
|
|
||||||
max-width: 100%;
|
|
||||||
height: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (prefers-color-scheme: dark) {
|
|
||||||
.mermaid {
|
|
||||||
background-color: var(--code-bg-dark, #2d2d2d);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,72 +0,0 @@
|
||||||
/* Basic Layout for Skills List */
|
|
||||||
#skills-content {
|
|
||||||
margin: 2rem 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
#skills-content .category {
|
|
||||||
margin-bottom: 3rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
#skills-content .category h3 {
|
|
||||||
font-size: 1.5rem;
|
|
||||||
font-weight: regular;
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Skills container for single-line display */
|
|
||||||
#skills-content .skills-list {
|
|
||||||
list-style: none;
|
|
||||||
padding: 0;
|
|
||||||
margin: 0;
|
|
||||||
display: flex; /* Use flexbox for single-line display */
|
|
||||||
flex-wrap: wrap; /* Allow skills to wrap if needed */
|
|
||||||
gap: 1rem; /* Spacing between skills */
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Skill List Item */
|
|
||||||
#skills-content .skills-list .skill {
|
|
||||||
display: inline-flex; /* Ensures each skill is aligned inline */
|
|
||||||
align-items: center;
|
|
||||||
position: relative;
|
|
||||||
cursor: pointer; /* Show pointer cursor on hover */
|
|
||||||
text-decoration: none; /* Remove underline from links */
|
|
||||||
color: inherit; /* Inherit color from parent */
|
|
||||||
transition: background-color 0.3s, color 0.3s;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/* Skill Name */
|
|
||||||
#skills-content .skills-list .skill span {
|
|
||||||
display: inline-block;
|
|
||||||
font-size: 1.2rem;
|
|
||||||
margin-left: 0.5rem;
|
|
||||||
line-height: 1.4;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Skill Icon */
|
|
||||||
#skills-content .skills-list .skill .skill-icon {
|
|
||||||
margin-right: 0.5rem;
|
|
||||||
color: var(--accent-color);
|
|
||||||
font-size: 1.4rem;
|
|
||||||
vertical-align: middle;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Hover Effects for Skills */
|
|
||||||
#skills-content .skills-list .skill:hover::before {
|
|
||||||
background: var(--accent-color-dark);
|
|
||||||
}
|
|
||||||
|
|
||||||
#skills-content .skills-list .skill:hover {
|
|
||||||
background-color: var(--accent-color-alpha);
|
|
||||||
color: var(--fg-color); /* Change text color on hover */
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Skill Category Hover Effect */
|
|
||||||
#skills-content .skills-list .skill:hover span {
|
|
||||||
color: var(--fg-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Optional: Customize Icon Color on Hover */
|
|
||||||
#skills-content .skills-list .skill:hover .skill-icon {
|
|
||||||
color: var(--fg-color);
|
|
||||||
}
|
|
|
@ -1,91 +0,0 @@
|
||||||
/* Basic Layout */
|
|
||||||
#timeline-content {
|
|
||||||
position: relative;
|
|
||||||
margin: 2rem 0;
|
|
||||||
padding-left: 120px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#timeline-content ul.timeline {
|
|
||||||
list-style: none;
|
|
||||||
padding: 0;
|
|
||||||
margin: 0;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
#timeline-content ul.timeline::before {
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
left: -30px;
|
|
||||||
top: 0;
|
|
||||||
bottom: 0;
|
|
||||||
width: 2px;
|
|
||||||
background: var(--accent-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Event List Item */
|
|
||||||
#timeline-content li.event {
|
|
||||||
position: relative;
|
|
||||||
margin-bottom: 3rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Event Circle */
|
|
||||||
#timeline-content li.event::before {
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
left: -39px;
|
|
||||||
top: 5px;
|
|
||||||
width: 16px;
|
|
||||||
height: 16px;
|
|
||||||
border-radius: 50%;
|
|
||||||
background: var(--bg-color);
|
|
||||||
border: 2px solid white;
|
|
||||||
z-index: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* From + To Label — styled exactly like old date label */
|
|
||||||
#timeline-content li.event::after {
|
|
||||||
content: attr(data-from) "\A" attr(data-to);
|
|
||||||
white-space: pre; /* ensures newline works */
|
|
||||||
position: absolute;
|
|
||||||
left: -160px;
|
|
||||||
width: 100px;
|
|
||||||
text-align: right;
|
|
||||||
color: var(--fg-color);
|
|
||||||
font-weight: bold;
|
|
||||||
font-size: 0.9rem;
|
|
||||||
line-height: 1.3;
|
|
||||||
top: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Event Heading */
|
|
||||||
#timeline-content li.event h3 {
|
|
||||||
margin: 0 0 0.5rem 0;
|
|
||||||
display: inline-block;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Event Description */
|
|
||||||
#timeline-content li.event p {
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Timeline Icon */
|
|
||||||
#timeline-content .timeline-icon {
|
|
||||||
margin-right: 0.5rem;
|
|
||||||
color: var(--accent-color);
|
|
||||||
font-size: 1.2rem;
|
|
||||||
vertical-align: middle;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Hover Effects */
|
|
||||||
#timeline-content li.event:hover::before {
|
|
||||||
background: var(--accent-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
#timeline-content li.event:hover {
|
|
||||||
background-color: var(--accent-color-alpha);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* From + To Label Hover Effect */
|
|
||||||
#timeline-content li.event:hover::after {
|
|
||||||
color: var(--accent-color);
|
|
||||||
}
|
|
|
@ -1,30 +0,0 @@
|
||||||
[
|
|
||||||
{
|
|
||||||
"grouping": "Languages, Operating Systems & Tools",
|
|
||||||
"skills": [
|
|
||||||
{ "name": "Cplusplus", "icon": "fa-solid fa-c" },
|
|
||||||
{ "name": "Rust", "icon": "fab fa-rust" },
|
|
||||||
{ "name": "Java", "icon": "fab fa-java" },
|
|
||||||
{ "name": "Python", "icon": "fab fa-python" },
|
|
||||||
{ "name": "git", "icon": "fas fa-code-branch" },
|
|
||||||
{ "name": "linux", "icon": "fab fa-linux" },
|
|
||||||
{ "name": "bash", "icon": "fas fa-terminal" },
|
|
||||||
{ "name": "javascript", "icon": "fab fa-js" }
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"grouping": "Platform Development & Administration",
|
|
||||||
"skills": [
|
|
||||||
{ "name": "NGINX", "icon": "fas fa-server" },
|
|
||||||
{ "name": "MySQL", "icon": "fas fa-database" },
|
|
||||||
{ "name": "Slurm", "icon": "fas fa-project-diagram" }
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"grouping": "Containers & Cloud",
|
|
||||||
"skills": [
|
|
||||||
{ "name": "Docker", "icon": "fab fa-docker" },
|
|
||||||
{ "name": "Aliyun", "icon": "fas fa-cloud" }
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
Before Width: | Height: | Size: 58 KiB |
Before Width: | Height: | Size: 64 KiB |
Before Width: | Height: | Size: 65 KiB |
Before Width: | Height: | Size: 60 KiB |
Before Width: | Height: | Size: 57 KiB |
Before Width: | Height: | Size: 59 KiB |
Before Width: | Height: | Size: 32 KiB |
Before Width: | Height: | Size: 3.1 MiB |
Before Width: | Height: | Size: 1.1 MiB |
Before Width: | Height: | Size: 1.2 MiB |
Before Width: | Height: | Size: 4 MiB |
Before Width: | Height: | Size: 3.2 MiB |
Before Width: | Height: | Size: 2.6 MiB |
Before Width: | Height: | Size: 4.4 MiB |
Before Width: | Height: | Size: 2.7 MiB |
Before Width: | Height: | Size: 3.9 MiB |
Before Width: | Height: | Size: 3.4 MiB |
Before Width: | Height: | Size: 2.8 MiB |
Before Width: | Height: | Size: 3.8 MiB |
Before Width: | Height: | Size: 2.8 MiB |
Before Width: | Height: | Size: 182 KiB |
Before Width: | Height: | Size: 13 MiB |
Before Width: | Height: | Size: 12 MiB |
Before Width: | Height: | Size: 12 MiB |
Before Width: | Height: | Size: 9 MiB |
Before Width: | Height: | Size: 9.9 MiB |
Before Width: | Height: | Size: 1.1 MiB |
Before Width: | Height: | Size: 1.8 MiB |
Before Width: | Height: | Size: 2 MiB |
Before Width: | Height: | Size: 1.2 MiB |
Before Width: | Height: | Size: 2.1 MiB |
Before Width: | Height: | Size: 1.4 MiB |
Before Width: | Height: | Size: 1.1 MiB |
Before Width: | Height: | Size: 1.4 MiB |
Before Width: | Height: | Size: 1.3 MiB |
Before Width: | Height: | Size: 721 KiB |
Before Width: | Height: | Size: 1.3 MiB |
Before Width: | Height: | Size: 2.4 MiB |
Before Width: | Height: | Size: 2.3 MiB |
Before Width: | Height: | Size: 1.8 MiB |
Before Width: | Height: | Size: 1.2 MiB |
Before Width: | Height: | Size: 1.6 MiB |
Before Width: | Height: | Size: 2 MiB |
Before Width: | Height: | Size: 975 KiB |
Before Width: | Height: | Size: 519 KiB |
Before Width: | Height: | Size: 1 MiB |
Before Width: | Height: | Size: 812 KiB |
Before Width: | Height: | Size: 955 KiB |
Before Width: | Height: | Size: 1.9 MiB |
Before Width: | Height: | Size: 1.8 MiB |
Before Width: | Height: | Size: 1.3 MiB |
Before Width: | Height: | Size: 1.1 MiB |
Before Width: | Height: | Size: 1.4 MiB |
Before Width: | Height: | Size: 50 KiB |
Before Width: | Height: | Size: 99 KiB |
Before Width: | Height: | Size: 97 KiB |
Before Width: | Height: | Size: 64 KiB |
Before Width: | Height: | Size: 32 KiB |
Before Width: | Height: | Size: 923 KiB |
Before Width: | Height: | Size: 55 KiB |
Before Width: | Height: | Size: 52 KiB |
Before Width: | Height: | Size: 69 KiB |
Before Width: | Height: | Size: 1.4 MiB |
Before Width: | Height: | Size: 1.1 MiB |
Before Width: | Height: | Size: 680 KiB |
Before Width: | Height: | Size: 944 KiB |
Before Width: | Height: | Size: 853 KiB |
Before Width: | Height: | Size: 839 KiB |