Compare commits

..

4 commits

18 changed files with 835 additions and 95 deletions

View file

@ -4,15 +4,15 @@
/* From https://css.glass */ /* From https://css.glass */
.glass { .glass {
background: rgba(18, 47, 101, 0.25); background: rgba(18, 47, 101, 0.6);
border-radius: 16px; border-radius: 16px;
box-shadow: 0 4px 30px rgba(0, 0, 0, 0.1); box-shadow: 0 4px 30px rgba(0, 0, 0, 0.1);
backdrop-filter: blur(8px); backdrop-filter: blur(8px);
animation: backdropShift 10s linear infinite; animation: backdropShift 10s linear infinite;
-webkit-backdrop-filter: blur(8px); -webkit-backdrop-filter: blur(8px);
border: 1px solid rgba(18, 47, 101, 0.35); border: 1px solid rgba(18, 47, 101, 0.45);
padding: 2rem; padding: 2rem;
filter: saturate(0.7) brightness(0.8); filter: saturate(0.8) brightness(0.9);
z-index: 2; z-index: 2;
} }
@ -153,4 +153,16 @@ header button[id^="search-button"]:hover {
/* Remove the default smooth scrolling behavior */ /* Remove the default smooth scrolling behavior */
html { html {
scroll-behavior: smooth; scroll-behavior: smooth;
}
/* Video feature styling */
video.mb-6 {
width: 100%;
height: auto;
object-fit: cover;
}
article video {
max-width: 100%;
height: auto;
} }

View file

@ -5,11 +5,10 @@ date = 2025-02-27T07:07:07-05:00
draft = false draft = false
categories = ['thoughts'] categories = ['thoughts']
tags = ['hugo', 'web'] tags = ['hugo', 'web']
disableToc = true
showFeature = false
+++ +++
First Post!!!
<!--more-->
## Social anxiety or introvert, chicken or egg? ## Social anxiety or introvert, chicken or egg?
I have been an introvert for as long as I can remember, and while I don't want this to turn into a self-help post, suffice it to say that I have never made a personal website primarily because it makes the hairs on the back of my neck stand up. Let today be the day that I face my fears and finally launch this site. I have been an introvert for as long as I can remember, and while I don't want this to turn into a self-help post, suffice it to say that I have never made a personal website primarily because it makes the hairs on the back of my neck stand up. Let today be the day that I face my fears and finally launch this site.

View file

@ -7,9 +7,6 @@ categories = ['references']
tags = ['hugo', 'markdown'] tags = ['hugo', 'markdown']
+++ +++
This cheatsheet is intended as a quick reference and showcase of the markdown syntax used in Hugo and Congo.
<!--more-->
{{< lead >}} {{< lead >}}
This cheatsheet is intended as a quick reference and showcase of the markdown syntax used in Hugo and Congo. This cheatsheet is intended as a quick reference and showcase of the markdown syntax used in Hugo and Congo.
{{< /lead >}} {{< /lead >}}

View file

@ -0,0 +1,281 @@
+++
title = 'Hosting [matrix] Synapse with Docker Compose'
description = 'Including configuration and initialization into docker compose files to make setup faster.'
date = 2025-12-08T14:10:00-05:00
draft = false
categories = ['guides']
tags = ['Matrix', 'Synapse', 'Element', 'Docker', 'Compose', 'Portainer', 'HAProxy']
disableToc = false
showFeature = false
+++
# A workaround for docker container intialization requirements when using docker compose.
<!--more-->
<div style="width:100%; max-width:640px; aspect-ratio:16/9; position:relative; overflow:hidden;">
<iframe src="/matrix-synapse-docker-compose/matrix-synapse-thumbnail.html" style="border:none; width:200%; height:200%; transform:scale(0.5); transform-origin:0 0;"></iframe>
</div>
Synapse is a great implementation of the [matrix] federated chat platform, but the [official documentation](https://matrix-org.github.io/synapse/latest/setup/installation.html) expects you to run several manual initialization steps before your server is ready to use. This gets tedious when you're just want to use a docker compose file to plug in your info and get going, especially if you are using tools like Portainer to administrate your containers.
This guide shows you how to bundle the entire initialization process into your docker-compose.yml so your [matrix] server goes from zero to fully operational with a single `docker compose up`. This compose file also includes element, which is a web-based client frontend for the [matrix] server. Note that I use HAProxy to terminate SSL within my network, so you may want to use traefic / let's encrypt or some other proxy between your matrix server and the web if you are not in the same boat. Even if you don't use HAProxy I think this can be a useful guide!
## The Problem
The standard [matrix] Synapse setup requires you to:
1. Generate the initial homeserver configuration
2. Create a registration shared secret
3. Wait for the server to start
4. Manually run register_new_matrix_user to create your admin account
5. Configure email settings
6. Set up your public base URL
If you're deploying through a UI like Portainer or want to version control your entire setup, having to SSH into your server and run commands defeats the purpose. We can do better.
## The Solution
We're going to use a custom entrypoint script that handles all the initialization automatically. All of our personal information will be stored in environment variables for easy access. These can be directly in the compose file, they can be in a separate .env file, or configured within the Portainer GUI. The script will check if this is the first run, generate the configuration if needed, inject our custom settings, start the server, and create the admin user once Synapse is ready.
Here's a minimally censored docker-compose configuration to make it easier to understand:
```yaml
version: '3'
services:
synapse:
image: docker.io/matrixdotorg/synapse:latest
restart: unless-stopped
environment:
SYNAPSE_SERVER_NAME: matrix.karsttech.com
SYNAPSE_REPORT_STATS: no
SYNAPSE_ADMIN_USER: admin
SYNAPSE_ADMIN_PASSWORD: [ADMIN_PASSWORD]
TZ: America/New_York
# SMTP Configuration
SMTP_HOST: smtp.protonmail.ch
SMTP_PORT: 587
SMTP_USER: automation@karsttech.com
SMTP_PASS: [SMTP_PASSWORD]
SMTP_FROM: "Matrix <automation@karsttech.com>"
volumes:
- data:/data
- media:/data/media
ports:
- 9447:8008/tcp # Reverse Proxy Should point http to this port
entrypoint:
- sh
- -c
- |
if [ ! -f /data/homeserver.yaml ]; then
/start.py generate;
REG_SECRET=$$(cat /dev/urandom | tr -dc 'a-zA-Z0-9' | fold -w 32 | head -n 1)
echo "registration_shared_secret: \"$$REG_SECRET\"" >> /data/homeserver.yaml
# Add public_baseurl
echo "public_baseurl: https://matrix.karsttech.com" >> /data/homeserver.yaml
echo "" >> /data/homeserver.yaml
echo "email:" >> /data/homeserver.yaml
echo " smtp_host: $$SMTP_HOST" >> /data/homeserver.yaml
echo " smtp_port: $$SMTP_PORT" >> /data/homeserver.yaml
echo " smtp_user: $$SMTP_USER" >> /data/homeserver.yaml
echo " smtp_pass: $$SMTP_PASS" >> /data/homeserver.yaml
echo " require_transport_security: true" >> /data/homeserver.yaml
echo " notif_from: \"$$SMTP_FROM\"" >> /data/homeserver.yaml
echo " enable_notifs: true" >> /data/homeserver.yaml
fi;
/start.py run &
SYNAPSE_PID=$$!
if [ ! -f /data/.admin_created ]; then
echo 'Waiting for Synapse to start...'
sleep 5
for i in 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15; do
if curl -s http://localhost:8008/_matrix/client/versions > /dev/null 2>&1; then
echo 'Synapse is ready, creating admin user...'
if register_new_matrix_user -u $$SYNAPSE_ADMIN_USER -p $$SYNAPSE_ADMIN_PASSWORD -a -c /data/homeserver.yaml http://localhost:8008; then
touch /data/.admin_created
echo 'Admin user created successfully!'
else
echo 'ERROR: Failed to create admin user!'
fi
break
fi
echo "Waiting... attempt $$i/15"
sleep 2
done
else
echo 'Admin user already created (skipping)'
fi
wait $$SYNAPSE_PID
element:
image: vectorim/element-web:latest
restart: unless-stopped
user: root
environment:
ELEMENT_SERVER_NAME: matrix.karsttech.com
ELEMENT_BASE_URL: https://matrix.karsttech.com
ports:
- 9449:8080
entrypoint: |
sh -c '
cat > /tmp/config.json <<EOF
{
"default_server_config": {
"m.homeserver": {
"base_url": "$$ELEMENT_BASE_URL",
"server_name": "$$ELEMENT_SERVER_NAME"
}
},
"brand": "Element",
"disable_guests": false,
"disable_3pid_login": false,
"default_theme": "light",
"room_directory": {
"servers": ["$$ELEMENT_SERVER_NAME", "matrix.org"]
},
"enable_presence_by_default": true,
"setting_defaults": {
"breadcrumbs": true
},
"show_labs_settings": false
}
EOF
cp /tmp/config.json /app/config.json
exec nginx -g "daemon off;"
'
volumes:
media:
data:
```
## How It Works
The magic happens in the custom entrypoint script. Let's break down what's happening:
### First Run Detection
```sh
if [ ! -f /data/homeserver.yaml ]; then
```
We check if the homeserver configuration exists. If it doesn't, we know this is a fresh installation and we need to generate everything.
### Configuration Generation
```sh
/start.py generate;
REG_SECRET=$$(cat /dev/urandom | tr -dc 'a-zA-Z0-9' | fold -w 32 | head -n 1)
echo "registration_shared_secret: \"$$REG_SECRET\"" >> /data/homeserver.yaml
```
We run the standard Synapse configuration generator, then immediately append our registration secret. The secret is randomly generated from `/dev/urandom` to ensure it's unique and secure.
### Configuration Injection
Rather than manually editing the generated `homeserver.yaml` later, we append our custom configuration directly:
```sh
echo "public_baseurl: https://matrix.karsttech.com" >> /data/homeserver.yaml
echo "" >> /data/homeserver.yaml
echo "email:" >> /data/homeserver.yaml
echo " smtp_host: $$SMTP_HOST" >> /data/homeserver.yaml
# ... more email config
```
This pulls values from environment variables, making the setup portable and easy to version control.
### Background Server Start
```
/start.py run &
SYNAPSE_PID=$$!
```
We start Synapse in the background and capture its process ID. This lets us continue with initialization while the server warms up.
### Admin User Creation
```sh
if [ ! -f /data/.admin_created ]; then
echo 'Waiting for Synapse to start...'
sleep 5
for i in 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15; do
if curl -s http://localhost:8008/_matrix/client/versions > /dev/null 2>&1; then
```
We use a marker file (`.admin_created`) to track whether we've already created the admin user. Then we poll the Synapse API endpoint until it responds, indicating the server is ready to accept user registration.
Once ready, we create the admin user and touch the marker file so we don't try to create it again on subsequent restarts.
### Process Management
```
wait $SYNAPSE_PID
```
Finally, we wait on the Synapse process. This ensures the container doesn't exit and that Docker properly handles signals when you stop the container.
## Element Web Client
The Element service is simpler but uses the same pattern - we generate its configuration from environment variables on startup:
```sh
cat > /tmp/config.json <<EOF
{
"default_server_config": {
"m.homeserver": {
"base_url": "$$ELEMENT_BASE_URL",
"server_name": "$$ELEMENT_SERVER_NAME"
}
},
...
}
EOF
cp /tmp/config.json /app/config.json
```
This lets you change Element's configuration by updating environment variables rather than mounting configuration files.
## Federation and .well-known
For [matrix] federation to work properly, you need to serve the `.well-known` endpoints. If you're using HAProxy like I am, here's the configuration:
```
acl matrix_wellknown_client_path_acl var(txn.txnpath) -m str -i /.well-known/matrix/client
acl matrix_wellknown_server_path_acl var(txn.txnpath) -m str -i /.well-known/matrix/server
http-request return status 200 content-type application/json string '{"m.homeserver":{"base_url":"https://matrix.karsttech.com"}}' if matrix_wellknown_client_path_acl aclcrt_karsttech_ssl_offloading_frontend
http-request return status 200 content-type application/json string '{"m.server":"matrix.karsttech.com:443"}' if matrix_wellknown_server_path_acl aclcrt_karsttech_ssl_offloading_frontend
```
These rules tell other [matrix] servers where to find your homeserver and ensure clients can auto-discover your server configuration.
## Deployment
To deploy this setup:
1. Replace `matrix.karsttech.com` with your domain
2. Set your admin credentials in `SYNAPSE_ADMIN_USER` and `SYNAPSE_ADMIN_PASSWORD`
3. Configure your SMTP settings for email notifications
4. Run `docker compose up -d`
That's it. The first startup will take a bit longer as it generates configuration and creates the admin user, but subsequent restarts will be fast since everything is already initialized.
## A Quick Warning
If you are new to [matrix] I highly recommend that the first thing you do when you set up a new account is to go to **Settings** -> **Encryption** and save / write down your Recovery Key! Without this if you clear your browser cache or move to a new computer you will [lose access](https://element.io/blog/resetting-the-server-side-key-backup/) to your encrypted messages and will need to reestablish trust with your fellow [matrix] users.
## A Small Invitation
As a special way to reach out to any readers who also use [matrix] chat, the first 10 people to use [THIS LINK](https://element.karsttech.com/#/register?token=MMqg3UbvlCuhZ.tH) will be able to create an account at *matrix.karsttech.com*
Happy self-hosting!

Binary file not shown.

After

Width:  |  Height:  |  Size: 216 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 231 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 465 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 140 KiB

View file

@ -0,0 +1,63 @@
+++
title = 'Modifying A Buggy Browser Extension'
description = '1Password has been misbehaving lately, lets fix it!'
date = 2026-01-06T16:20:00-05:00
draft = false
categories = ['guides']
tags = ['Firefox', 'Browser Extensions', '1Password']
disableToc = false
+++
# The 1Password browser extension has been misbehaving lately... lets fix it!
<!--more-->
The problem that I have been having is a recent bug introduced into 1Password where it will not autofill on local http websites. The reason for this is a problem importing window.crypto.randomUUID() within a non-secure context and is outlined [here](https://www.1password.community/discussions/1password/re-enable-auto-fill-for-localhost-local-ip-network-device-websites/165052).
This guide will walk you through how to fix this issue, and also how to modify other Firefox extensions.
## Step 1: Figure out where your Firefox profile is stored
Open a new firefox tab and type **about:support** into the url bar. Then find the **Profile Directory** section and click the **Open Directory** button.
![Firefox about:support](firefox-profile-dir.png)
## Step 2: Extract the extension
Within the Firefox profile directory, go into the **extensions** folder and you will find one .xpi file for each extension you are using in Firefox. One of these extensions will be the one you want.
![Firefox Profile Extensions](extensions-folder.png)
I have found the easiest way to figure out which .xpi file is correct is to open them up in an archive manager (you can rename the file to *.zip first if you want), then check inside the **credits.html** file.
![Extension Archive](extension-archive.png)
The extension name is near the top under &lt;meta content=[EXTENSION_NAME]/&gt;
![Credits HTML](credits-html.png)
## Step 3: Edit the extension
To solve our particular problem, extract this whole .xpi file into a new folder. We will want to edit the /[Folder Name]/inline/injected.js file.
Open a new terminal window here (in the folder with injected.js) and run:
```sh
sed -i 's/QDe=yte==="client"?window\.crypto\.randomUUID()\.slice(0,8):void 0/QDe=yte==="client"\&\&window.crypto.randomUUID?window.crypto.randomUUID().slice(0,8):void 0/g' injected.js
```
This uses sed to find and replace a string. We are replacing the existing call to window.crypto.randomUUID() to a ternary operator which will check if window.crypto.randomUUID exists and if so use it, if not fall back to void 0 (undefined) which will cause the existing 1Password code to fall back to a counter-based ID instead.
Note that this sed statement will not work if the 1Password extension significantly changes, so you may need to modify it. Hopefully if the extension changes it means the maintainers have fixed this bug and we don't need to do this in the first place!
## Step 4: Add the modified extension into Firefox
In a Firefox tab go to **about:debugging#/runtime/this-firefox**
Click the **Load Temporary Add-on** button.
![Temporary Addon](load-temporary-addon.png)
Load the addon by pointing to your **/[Folder Name]/manifest.json** file.
1Password should now offer to autofill on http websites as normal.
All done!!!

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

View file

@ -1,57 +0,0 @@
---
title: "TOC Disable Example"
description: "Example showing how to disable the table of contents in markdown files"
date: 2025-01-27
draft: false
categories: ['examples']
tags: ['toc', 'markdown', 'hugo']
disableToc: true # This disables the table of contents for this page
---
{{< lead >}}
This page demonstrates how to disable the table of contents in markdown files.
{{< /lead >}}
## How to Disable TOC
To disable the table of contents in any markdown file (including `index.md` files), simply add the `disableToc: true` parameter to the front matter:
```yaml
---
title: "Your Page Title"
description: "Your page description"
disableToc: true # This disables the TOC
---
```
## Usage Examples
### For Individual Pages
Add `disableToc: true` to the front matter of any markdown file to disable the TOC for that specific page.
### For Index Pages
For `index.md` files that serve as list pages, you can also use the `cascade` parameter to apply the setting to all pages in that section:
```yaml
---
title: "Section Title"
cascade:
disableToc: true # Disables TOC for all pages in this section
---
```
## How It Works
When `disableToc: true` is set:
1. The table of contents will not be displayed on the page
2. The TOC JavaScript will not be loaded (improving performance)
3. The TOC highlighting functionality will be disabled
## Note
This feature works for both:
- Individual article pages (using the `single.html` template)
- List/index pages (using the `list.html` template)
The TOC will still be generated by Hugo, but it won't be displayed or have JavaScript functionality when disabled.

View file

@ -7,9 +7,6 @@ categories = ['references']
tags = ['LSI', 'TrueNAS Scale', 'Broadcom', 'Update', 'SAS3Flash', 'HBA'] tags = ['LSI', 'TrueNAS Scale', 'Broadcom', 'Update', 'SAS3Flash', 'HBA']
+++ +++
Navigating Broadcom and LSI HBA updates on TrueNAS Scale.
<!--more-->
{{< lead >}} {{< lead >}}
Navigating Broadcom and LSI HBA updates on TrueNAS Scale. Navigating Broadcom and LSI HBA updates on TrueNAS Scale.
{{< /lead >}} {{< /lead >}}

View file

@ -1,7 +1,11 @@
<article class="mt-6 flex max-w-prose flex-row"> <article class="mt-6 flex max-w-prose flex-row">
{{- $images := $.Resources.ByType "image" }} {{- $images := $.Resources.ByType "image" }}
{{- $videos := $.Resources.ByType "video" }}
{{- $thumbnail := $images.GetMatch (.Params.thumbnail | default "*thumb*") }} {{- $thumbnail := $images.GetMatch (.Params.thumbnail | default "*thumb*") }}
{{- $feature := $images.GetMatch (.Params.feature | default "*feature*") | default $thumbnail }} {{- $feature := $images.GetMatch (.Params.feature | default "*feature*") | default $thumbnail }}
{{- if not $feature }}
{{- $feature = $videos.GetMatch (.Params.feature | default "*feature*") }}
{{- end }}
{{- with $feature }} {{- with $feature }}
<div class="flex-none pe-4 sm:pe-6 "> <div class="flex-none pe-4 sm:pe-6 ">
<a <a
@ -12,21 +16,37 @@
{{ end }}" {{ end }}"
aria-label="{{ $.Title | emojify }}" aria-label="{{ $.Title | emojify }}"
> >
<img {{ if eq .ResourceType "video" }}
alt="{{ $.Params.featureAlt | default $.Params.thumbnailAlt | default "" }}" <video
{{ if eq .MediaType.SubType "svg" }} class="w-24 rounded-md sm:w-40 max-h-[7.5rem]"
class="w-24 max-w-[6rem] max-h-[4.5rem] rounded-md sm:max-h-[7.5rem] sm:w-40 autoplay
sm:max-w-[10rem]" src="{{ .RelPermalink }}" loop
{{ else }} muted
class="w-24 rounded-md sm:w-40" srcset=" playsinline
{{- (.Fill "160x120 smart").RelPermalink }} {{ if $.Site.Params.enableImageLazyLoading | default true }}
160w, {{- (.Fill "320x240 smart").RelPermalink }} 2x" loading="lazy"
src="{{ (.Fill "160x120 smart").RelPermalink }}" width="160" height="120" {{ end }}
{{ end }} aria-label="{{ $.Params.featureAlt | default $.Params.thumbnailAlt | default "" }}"
{{ if $.Site.Params.enableImageLazyLoading | default true }} >
loading="lazy" <source src="{{ .RelPermalink }}" type="{{ .MediaType }}">
{{ end }} </video>
/> {{ else }}
<img
alt="{{ $.Params.featureAlt | default $.Params.thumbnailAlt | default "" }}"
{{ if eq .MediaType.SubType "svg" }}
class="w-24 max-w-[6rem] max-h-[4.5rem] rounded-md sm:max-h-[7.5rem] sm:w-40
sm:max-w-[10rem]" src="{{ .RelPermalink }}"
{{ else }}
class="w-24 rounded-md sm:w-40" srcset="
{{- (.Fill "160x120 smart").RelPermalink }}
160w, {{- (.Fill "320x240 smart").RelPermalink }} 2x"
src="{{ (.Fill "160x120 smart").RelPermalink }}" width="160" height="120"
{{ end }}
{{ if $.Site.Params.enableImageLazyLoading | default true }}
loading="lazy"
{{ end }}
/>
{{ end }}
</a> </a>
</div> </div>
{{- end }} {{- end }}
@ -70,7 +90,15 @@
</div> </div>
{{ if .Params.showSummary | default (.Site.Params.list.showSummary | default false) }} {{ if .Params.showSummary | default (.Site.Params.list.showSummary | default false) }}
<div class="prose py-1 dark:prose-invert"> <div class="prose py-1 dark:prose-invert">
{{ $summary := .Summary | emojify }} {{ $summary := "" }}
{{ $hasManualSummary := findRE "<!--\\s*more\\s*-->" .RawContent }}
{{ if $hasManualSummary }}
{{ $summary = .Summary | emojify }}
{{ else if .Description }}
{{ $summary = .Description | emojify }}
{{ else }}
{{ $summary = .Summary | emojify }}
{{ end }}
{{ $summary := replaceRE "<h[1-6][^>]*>(.*?)</h[1-6]>" "$1" $summary }} {{ $summary := replaceRE "<h[1-6][^>]*>(.*?)</h[1-6]>" "$1" $summary }}
{{ $summary := replaceRE "\\s*<br\\s*/?>" " " $summary }} {{ $summary := replaceRE "\\s*<br\\s*/?>" " " $summary }}
{{ $summary := replaceRE "\\s+" " " $summary }} {{ $summary := replaceRE "\\s+" " " $summary }}

View file

@ -0,0 +1,23 @@
{{ $video := .video }}
{{ $class := .class }}
{{ $lazy := .lazy }}
{{ $alt := .alt }}
{{ $autoplay := .autoplay | default true }}
{{ $loop := .loop | default true }}
{{ $muted := .muted | default true }}
{{ with $video }}
<video
{{ with $class }}class="{{ . }}"{{ end }}
{{ if $autoplay }}autoplay{{ end }}
{{ if $loop }}loop{{ end }}
{{ if $muted }}muted{{ end }}
playsinline
{{ with $lazy }}loading="lazy"{{ end }}
{{ with $alt }}aria-label="{{ . }}"{{ end }}
>
<source src="{{ .RelPermalink }}" type="{{ .MediaType }}">
{{ with $alt }}{{ . }}{{ end }}
</video>
{{ end }}

View file

@ -1,8 +1,12 @@
{{ define "main" }} {{ define "main" }}
{{ partial "background-images.html" . }} {{ partial "background-images.html" . }}
{{- $images := .Resources.ByType "image" }} {{- $images := .Resources.ByType "image" }}
{{- $videos := .Resources.ByType "video" }}
{{- $cover := $images.GetMatch (.Params.cover | default "*cover*") }} {{- $cover := $images.GetMatch (.Params.cover | default "*cover*") }}
{{- $feature := $images.GetMatch (.Params.feature | default "*feature*") | default $cover }} {{- $feature := $images.GetMatch (.Params.feature | default "*feature*") | default $cover }}
{{- if not $feature }}
{{- $feature = $videos.GetMatch (.Params.feature | default "*feature*") }}
{{- end }}
<article class="prose max-w-full dark:prose-invert glass"> <article class="prose max-w-full dark:prose-invert glass">
<header class="max-w-prose"> <header class="max-w-prose">
{{ if .Params.showBreadcrumbs | default (.Site.Params.article.showBreadcrumbs | default false) }} {{ if .Params.showBreadcrumbs | default (.Site.Params.article.showBreadcrumbs | default false) }}
@ -22,16 +26,22 @@
{{ partial "article-meta.html" (dict "context" . "scope" "single") }} {{ partial "article-meta.html" (dict "context" . "scope" "single") }}
</div> </div>
{{ end }} {{ end }}
{{ with $feature }} {{ if .Params.showFeature | default true }}
<div class="prose"> {{ with $feature }}
{{ $altText := $.Params.featureAlt | default $.Params.coverAlt | default "" }} <div class="prose">
{{ $class := "mb-6 rounded-md" }} {{ $altText := $.Params.featureAlt | default $.Params.coverAlt | default "" }}
{{ $webp := $.Page.Site.Params.enableImageWebp | default true }} {{ $class := "mb-6 rounded-md" }}
{{ partial "picture.html" (dict "img" . "alt" $altText "class" $class "lazy" false "webp" $webp) }} {{ if eq .ResourceType "video" }}
{{ with $.Params.coverCaption }} {{ partial "video.html" (dict "video" . "alt" $altText "class" $class "lazy" false "autoplay" true "loop" true "muted" true) }}
<figcaption class="-mt-3 mb-6 text-center">{{ . | markdownify }}</figcaption> {{ else }}
{{ end }} {{ $webp := $.Page.Site.Params.enableImageWebp | default true }}
</div> {{ partial "picture.html" (dict "img" . "alt" $altText "class" $class "lazy" false "webp" $webp) }}
{{ end }}
{{ with $.Params.coverCaption }}
<figcaption class="-mt-3 mb-6 text-center">{{ . | markdownify }}</figcaption>
{{ end }}
</div>
{{ end }}
{{ end }} {{ end }}
</header> </header>
<section class="prose mt-0 flex max-w-full flex-col dark:prose-invert lg:flex-row"> <section class="prose mt-0 flex max-w-full flex-col dark:prose-invert lg:flex-row">

View file

@ -0,0 +1,387 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=1280, initial-scale=1.0">
<title>Matrix Synapse Docker Guide</title>
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@700;800&family=Archivo+Black&display=swap" rel="stylesheet">
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html {
margin: 0;
padding: 0;
overflow: hidden;
}
body {
background: #0a0a0a;
margin: 0;
padding: 0;
overflow: hidden;
}
#scaler {
width: 1280px;
height: 720px;
transform-origin: 0 0;
}
.thumbnail {
width: 1280px;
height: 720px;
background: linear-gradient(135deg, #0d1117 0%, #1a1f2e 50%, #0d1117 100%);
position: relative;
overflow: hidden;
border-radius: 8px;
box-shadow: 0 30px 90px rgba(0, 0, 0, 0.8);
}
/* Animated grid background */
.grid-bg {
position: absolute;
inset: 0;
background-image:
linear-gradient(rgba(0, 255, 159, 0.03) 1px, transparent 1px),
linear-gradient(90deg, rgba(0, 255, 159, 0.03) 1px, transparent 1px);
background-size: 40px 40px;
animation: gridPulse 3s ease-in-out infinite;
}
@keyframes gridPulse {
0%, 100% { opacity: 0.6; }
50% { opacity: 1; }
}
/* Glowing orbs */
.orb {
position: absolute;
border-radius: 50%;
filter: blur(80px);
opacity: 0.4;
animation: float 8s ease-in-out infinite;
}
.orb-1 {
width: 400px;
height: 400px;
background: radial-gradient(circle, #00ff9f 0%, transparent 70%);
top: -100px;
right: -100px;
animation-delay: 0s;
}
.orb-2 {
width: 300px;
height: 300px;
background: radial-gradient(circle, #00bfff 0%, transparent 70%);
bottom: -80px;
left: -80px;
animation-delay: 2s;
}
.orb-3 {
width: 250px;
height: 250px;
background: radial-gradient(circle, #7c3aed 0%, transparent 70%);
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
animation-delay: 4s;
}
@keyframes float {
0%, 100% { transform: translate(0, 0) scale(1); }
33% { transform: translate(30px, -30px) scale(1.1); }
66% { transform: translate(-30px, 30px) scale(0.9); }
}
/* Content container */
.content {
position: relative;
z-index: 10;
height: 100%;
display: flex;
flex-direction: column;
padding: 60px 80px;
}
/* Matrix logo section */
.logo-section {
display: flex;
align-items: center;
gap: 24px;
margin-bottom: 40px;
}
.matrix-logo {
width: 90px;
height: 90px;
background: linear-gradient(135deg, #00ff9f 0%, #00bfff 100%);
border-radius: 20px;
display: flex;
align-items: center;
justify-content: center;
font-family: 'JetBrains Mono', monospace;
font-size: 42px;
font-weight: 800;
color: #0a0a0a;
box-shadow: 0 8px 32px rgba(0, 255, 159, 0.3);
position: relative;
overflow: hidden;
}
.matrix-logo::before {
content: '';
position: absolute;
inset: -2px;
background: linear-gradient(45deg, transparent 30%, rgba(255, 255, 255, 0.3) 50%, transparent 70%);
animation: shine 3s infinite;
}
@keyframes shine {
0% { transform: translateX(-100%) rotate(45deg); }
100% { transform: translateX(200%) rotate(45deg); }
}
.matrix-logo-text {
font-family: 'Archivo Black', sans-serif;
font-size: 36px;
color: #ffffff;
letter-spacing: -1px;
text-transform: uppercase;
}
/* Main title */
.main-title {
font-family: 'Archivo Black', sans-serif;
font-size: 82px;
line-height: 0.95;
color: #ffffff;
margin-bottom: 30px;
letter-spacing: -3px;
text-transform: uppercase;
}
.title-line-1 {
display: block;
background: linear-gradient(90deg, #ffffff 0%, #00ff9f 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.title-line-2 {
display: block;
color: #ffffff;
margin-left: 120px;
}
/* Tech badges */
.tech-badges {
display: flex;
gap: 16px;
margin-bottom: auto;
}
.badge {
padding: 12px 24px;
background: rgba(0, 63, 39, 0.9);
border: 2px solid rgba(0, 255, 159, 0.3);
border-radius: 8px;
font-family: 'JetBrains Mono', monospace;
font-size: 18px;
font-weight: 700;
color: #00ff9f;
text-transform: uppercase;
letter-spacing: 1px;
backdrop-filter: blur(10px);
box-shadow: 0 4px 16px rgba(0, 255, 159, 0.1);
}
/* Docker compose visual */
.docker-visual {
position: absolute;
right: 80px;
bottom: 80px;
width: 420px;
}
.terminal-window {
background: rgba(13, 17, 23, 0.95);
border: 1px solid rgba(0, 255, 159, 0.2);
border-radius: 12px;
padding: 20px;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.6);
backdrop-filter: blur(20px);
}
.terminal-header {
display: flex;
gap: 8px;
margin-bottom: 16px;
}
.terminal-dot {
width: 12px;
height: 12px;
border-radius: 50%;
}
.dot-1 { background: #ff5f56; }
.dot-2 { background: #ffbd2e; }
.dot-3 { background: #27c93f; }
.terminal-content {
font-family: 'JetBrains Mono', monospace;
font-size: 16px;
line-height: 1.6;
}
.terminal-line {
display: flex;
margin-bottom: 8px;
}
.terminal-prompt {
color: #00ff9f;
margin-right: 8px;
}
.terminal-command {
color: #00bfff;
}
.terminal-comment {
color: #6e7681;
}
.terminal-yaml {
color: #ffffff;
}
.yaml-key {
color: #ff7b72;
}
.yaml-value {
color: #a5d6ff;
}
.cursor {
display: inline-block;
width: 10px;
height: 20px;
background: #00ff9f;
animation: blink 1s step-end infinite;
}
@keyframes blink {
50% { opacity: 0; }
}
/* Decorative elements */
.deco-line {
position: absolute;
height: 2px;
background: linear-gradient(90deg, transparent, #00ff9f, transparent);
opacity: 0.3;
}
.deco-line-1 {
width: 300px;
top: 180px;
left: 80px;
}
.deco-line-2 {
width: 200px;
bottom: 200px;
right: 520px;
}
</style>
</head>
<body>
<div id="scaler">
<div class="thumbnail">
<div class="grid-bg"></div>
<div class="orb orb-1"></div>
<div class="orb orb-2"></div>
<div class="orb orb-3"></div>
<div class="deco-line deco-line-1"></div>
<div class="deco-line deco-line-2"></div>
<div class="content">
<div class="logo-section">
<div class="matrix-logo">[m]</div>
<div class="matrix-logo-text">Synapse</div>
</div>
<h1 class="main-title">
<span class="title-line-1">Self-Host</span>
<span class="title-line-2">Your Server</span>
</h1>
<div class="tech-badges">
<div class="badge">Docker</div>
<div class="badge">Compose</div>
<div class="badge">Matrix</div>
</div>
</div>
<div class="docker-visual">
<div class="terminal-window">
<div class="terminal-header">
<div class="terminal-dot dot-1"></div>
<div class="terminal-dot dot-2"></div>
<div class="terminal-dot dot-3"></div>
</div>
<div class="terminal-content">
<div class="terminal-line">
<span class="terminal-prompt">$</span>
<span class="terminal-command">docker compose up -d</span>
</div>
<div class="terminal-line">
<span class="terminal-comment"># Starting services...</span>
</div>
<div class="terminal-line">
<span class="terminal-yaml"><span class="yaml-key">synapse:</span></span>
</div>
<div class="terminal-line">
<span class="terminal-yaml"> <span class="yaml-key">image:</span> <span class="yaml-value">matrixdotorg/synapse</span></span>
</div>
<div class="terminal-line">
<span class="terminal-yaml"> <span class="yaml-key">ports:</span></span>
</div>
<div class="terminal-line">
<span class="terminal-yaml"> - <span class="yaml-value">8008:8008</span></span>
</div>
<div class="terminal-line">
<span class="terminal-yaml"> <span class="yaml-key">volumes:</span></span>
</div>
<div class="terminal-line">
<span class="terminal-yaml"> - <span class="yaml-value">./data:/data</span><span class="cursor"></span></span>
</div>
</div>
</div>
</div>
</div>
</div>
<script>
function scaleContent() {
const scaler = document.getElementById('scaler');
const scaleX = window.innerWidth / 1280;
const scaleY = window.innerHeight / 720;
const scale = Math.min(scaleX, scaleY);
scaler.style.transform = `scale(${scale})`;
}
scaleContent();
window.addEventListener('resize', scaleContent);
</script>
</body>
</html>