Initialize project with general functionality
24
.gitignore
vendored
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
# build output
|
||||||
|
dist/
|
||||||
|
# generated types
|
||||||
|
.astro/
|
||||||
|
|
||||||
|
# dependencies
|
||||||
|
node_modules/
|
||||||
|
|
||||||
|
# logs
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
|
||||||
|
|
||||||
|
# environment variables
|
||||||
|
.env
|
||||||
|
.env.production
|
||||||
|
|
||||||
|
# macOS-specific files
|
||||||
|
.DS_Store
|
||||||
|
|
||||||
|
# jetbrains setting folder
|
||||||
|
.idea/
|
||||||
11
.prettierrc
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"plugins": ["prettier-plugin-astro"],
|
||||||
|
"overrides": [
|
||||||
|
{
|
||||||
|
"files": "*.astro",
|
||||||
|
"options": {
|
||||||
|
"parser": "astro"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
4
.vscode/extensions.json
vendored
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"recommendations": ["astro-build.astro-vscode"],
|
||||||
|
"unwantedRecommendations": []
|
||||||
|
}
|
||||||
11
.vscode/launch.json
vendored
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"version": "0.2.0",
|
||||||
|
"configurations": [
|
||||||
|
{
|
||||||
|
"command": "./node_modules/.bin/astro dev",
|
||||||
|
"name": "Development server",
|
||||||
|
"request": "launch",
|
||||||
|
"type": "node-terminal"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
47
README.md
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
# Astro Starter Kit: Minimal
|
||||||
|
|
||||||
|
```sh
|
||||||
|
npm create astro@latest -- --template minimal
|
||||||
|
```
|
||||||
|
|
||||||
|
[](https://stackblitz.com/github/withastro/astro/tree/latest/examples/minimal)
|
||||||
|
[](https://codesandbox.io/p/sandbox/github/withastro/astro/tree/latest/examples/minimal)
|
||||||
|
[](https://codespaces.new/withastro/astro?devcontainer_path=.devcontainer/minimal/devcontainer.json)
|
||||||
|
|
||||||
|
> 🧑🚀 **Seasoned astronaut?** Delete this file. Have fun!
|
||||||
|
|
||||||
|
## 🚀 Project Structure
|
||||||
|
|
||||||
|
Inside of your Astro project, you'll see the following folders and files:
|
||||||
|
|
||||||
|
```text
|
||||||
|
/
|
||||||
|
├── public/
|
||||||
|
├── src/
|
||||||
|
│ └── pages/
|
||||||
|
│ └── index.astro
|
||||||
|
└── package.json
|
||||||
|
```
|
||||||
|
|
||||||
|
Astro looks for `.astro` or `.md` files in the `src/pages/` directory. Each page is exposed as a route based on its file name.
|
||||||
|
|
||||||
|
There's nothing special about `src/components/`, but that's where we like to put any Astro/React/Vue/Svelte/Preact components.
|
||||||
|
|
||||||
|
Any static assets, like images, can be placed in the `public/` directory.
|
||||||
|
|
||||||
|
## 🧞 Commands
|
||||||
|
|
||||||
|
All commands are run from the root of the project, from a terminal:
|
||||||
|
|
||||||
|
| Command | Action |
|
||||||
|
| :------------------------ | :----------------------------------------------- |
|
||||||
|
| `npm install` | Installs dependencies |
|
||||||
|
| `npm run dev` | Starts local dev server at `localhost:4321` |
|
||||||
|
| `npm run build` | Build your production site to `./dist/` |
|
||||||
|
| `npm run preview` | Preview your build locally, before deploying |
|
||||||
|
| `npm run astro ...` | Run CLI commands like `astro add`, `astro check` |
|
||||||
|
| `npm run astro -- --help` | Get help using the Astro CLI |
|
||||||
|
|
||||||
|
## 👀 Want to learn more?
|
||||||
|
|
||||||
|
Feel free to check [our documentation](https://docs.astro.build) or jump into our [Discord server](https://astro.build/chat).
|
||||||
20
astro.config.mjs
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import { defineConfig } from "astro/config";
|
||||||
|
import tailwindcss from "@tailwindcss/vite";
|
||||||
|
import { remarkModifiedTime } from "./remark-modified-time.mjs";
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
prefetch: {
|
||||||
|
prefetchAll: true,
|
||||||
|
},
|
||||||
|
|
||||||
|
vite: {
|
||||||
|
plugins: [tailwindcss()],
|
||||||
|
},
|
||||||
|
|
||||||
|
markdown: {
|
||||||
|
shikiConfig: {
|
||||||
|
theme: "github-dark",
|
||||||
|
},
|
||||||
|
remarkPlugins: [remarkModifiedTime],
|
||||||
|
},
|
||||||
|
});
|
||||||
27
flake.lock
generated
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
{
|
||||||
|
"nodes": {
|
||||||
|
"nixpkgs": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1745391562,
|
||||||
|
"narHash": "sha256-sPwcCYuiEopaafePqlG826tBhctuJsLx/mhKKM5Fmjo=",
|
||||||
|
"owner": "NixOS",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"rev": "8a2f738d9d1f1d986b5a4cd2fd2061a7127237d7",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "NixOS",
|
||||||
|
"ref": "nixos-unstable",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"root": {
|
||||||
|
"inputs": {
|
||||||
|
"nixpkgs": "nixpkgs"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"root": "root",
|
||||||
|
"version": 7
|
||||||
|
}
|
||||||
41
flake.nix
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
{
|
||||||
|
inputs = {
|
||||||
|
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
|
||||||
|
};
|
||||||
|
|
||||||
|
outputs =
|
||||||
|
{ self, nixpkgs, ... }:
|
||||||
|
let
|
||||||
|
allSystems = [
|
||||||
|
"x86_64-linux"
|
||||||
|
"aarch64-linux"
|
||||||
|
"x86_64-darwin"
|
||||||
|
"aarch64-darwin"
|
||||||
|
];
|
||||||
|
|
||||||
|
forAllSystems = nixpkgs.lib.genAttrs allSystems;
|
||||||
|
in
|
||||||
|
{
|
||||||
|
devShells = forAllSystems (
|
||||||
|
system:
|
||||||
|
let
|
||||||
|
pkgs = nixpkgs.legacyPackages.${system};
|
||||||
|
in
|
||||||
|
{
|
||||||
|
default = pkgs.mkShell {
|
||||||
|
packages = with pkgs; [
|
||||||
|
nodejs
|
||||||
|
];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
formatter = forAllSystems (
|
||||||
|
system:
|
||||||
|
let
|
||||||
|
pkgs = nixpkgs.legacyPackages.${system};
|
||||||
|
in
|
||||||
|
pkgs.nixfmt-rfc-style
|
||||||
|
);
|
||||||
|
};
|
||||||
|
}
|
||||||
5486
package-lock.json
generated
Normal file
22
package.json
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"name": "thiloho-github-io",
|
||||||
|
"type": "module",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "astro dev",
|
||||||
|
"build": "astro build",
|
||||||
|
"preview": "astro preview",
|
||||||
|
"astro": "astro",
|
||||||
|
"format": "prettier . --write"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@tailwindcss/vite": "4.1.4",
|
||||||
|
"astro": "5.7.5",
|
||||||
|
"tailwindcss": "4.1.4"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@tailwindcss/typography": "0.5.16",
|
||||||
|
"prettier": "3.5.3",
|
||||||
|
"prettier-plugin-astro": "0.14.1"
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
public/apple-touch-icon.png
Normal file
|
After Width: | Height: | Size: 4.6 KiB |
BIN
public/favicon-96x96.png
Normal file
|
After Width: | Height: | Size: 2.7 KiB |
BIN
public/favicon.ico
Normal file
|
After Width: | Height: | Size: 15 KiB |
10
public/favicon.svg
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:svgjs="http://svgjs.dev/svgjs" width="174.8" height="131.4"><svg viewBox="0 0 174.8 131.4" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<rect x="0" y="0" width="174.8" height="131.4" fill="#e5e5e5" stroke="none" rx="20" ry="20"></rect>
|
||||||
|
<g transform="translate(20, 30)">
|
||||||
|
<g id="SvgjsG1082" stroke-linecap="round" fill-rule="evenodd" font-size="9pt" stroke="#525252" stroke-width="0.125mm" fill="#a1a1a1" style="stroke:#525252;stroke-width:0.125mm;fill:#a1a1a1">
|
||||||
|
<path d="M 92.2 71.4 L 63.1 71.4 L 63.1 67.2 L 64.4 67.2 Q 66.7 67.2 68.55 66.7 Q 70.4 66.2 71.5 64.65 A 5.045 5.045 0 0 0 72.17 63.27 Q 72.587 61.979 72.6 60.121 A 17.219 17.219 0 0 0 72.6 60 L 72.6 11 A 12.54 12.54 0 0 0 72.509 9.435 Q 72.285 7.659 71.509 6.627 A 3.715 3.715 0 0 0 71.45 6.55 Q 70.3 5.1 68.45 4.65 A 15.785 15.785 0 0 0 66.001 4.263 A 19.693 19.693 0 0 0 64.4 4.2 L 63.1 4.2 L 63.1 0 L 92.2 0 L 92.2 4.2 L 90.9 4.2 A 17.148 17.148 0 0 0 88.267 4.395 A 14.51 14.51 0 0 0 86.8 4.7 A 5.523 5.523 0 0 0 84.882 5.604 A 5.171 5.171 0 0 0 83.8 6.7 Q 82.771 8.104 82.705 10.996 A 17.684 17.684 0 0 0 82.7 11.4 L 82.7 31.6 L 115.2 31.6 L 115.2 11.4 A 14.799 14.799 0 0 0 115.113 9.734 Q 114.92 8.032 114.296 6.995 A 3.873 3.873 0 0 0 114.1 6.7 Q 113 5.2 111.15 4.7 A 14.115 14.115 0 0 0 108.926 4.296 A 18.631 18.631 0 0 0 107 4.2 L 105.7 4.2 L 105.7 0 L 134.8 0 L 134.8 4.2 L 133.5 4.2 A 17.148 17.148 0 0 0 130.867 4.395 A 14.51 14.51 0 0 0 129.4 4.7 A 5.523 5.523 0 0 0 127.482 5.604 A 5.171 5.171 0 0 0 126.4 6.7 Q 125.371 8.104 125.305 10.996 A 17.684 17.684 0 0 0 125.3 11.4 L 125.3 60.5 A 11.673 11.673 0 0 0 125.401 62.094 Q 125.512 62.894 125.743 63.543 A 4.544 4.544 0 0 0 126.45 64.85 Q 127.6 66.3 129.45 66.75 A 15.785 15.785 0 0 0 131.899 67.137 A 19.693 19.693 0 0 0 133.5 67.2 L 134.8 67.2 L 134.8 71.4 L 105.7 71.4 L 105.7 67.2 L 107 67.2 Q 109.3 67.2 111.15 66.7 Q 113 66.2 114.1 64.65 A 5.045 5.045 0 0 0 114.77 63.27 Q 115.187 61.979 115.2 60.121 A 17.219 17.219 0 0 0 115.2 60 L 115.2 36.6 L 82.7 36.6 L 82.7 60 A 14.094 14.094 0 0 0 82.787 61.621 Q 82.998 63.435 83.718 64.53 A 4.105 4.105 0 0 0 83.8 64.65 Q 84.9 66.2 86.8 66.7 A 15.233 15.233 0 0 0 89.554 67.151 A 18.189 18.189 0 0 0 90.9 67.2 L 92.2 67.2 L 92.2 71.4 Z M 44.1 71.4 L 13 71.4 L 13 67.2 L 15.3 67.2 A 18.863 18.863 0 0 0 17.664 67.058 A 15.182 15.182 0 0 0 19.35 66.75 Q 21.2 66.3 22.35 64.85 Q 23.179 63.805 23.41 62.007 A 11.832 11.832 0 0 0 23.5 60.5 L 23.5 5 L 13.9 5 Q 11.343 5 9.698 5.828 A 4.991 4.991 0 0 0 7.8 7.45 Q 6.1 9.9 5.7 13.2 L 5.2 17.5 L 0 17.5 L 0.5 0 L 56.8 0 L 57.3 17.5 L 52.1 17.5 L 51.6 13.2 A 13.62 13.62 0 0 0 50.739 9.756 A 11.607 11.607 0 0 0 49.5 7.45 Q 48.152 5.507 45.043 5.105 A 13.611 13.611 0 0 0 43.3 5 L 33.6 5 L 33.6 60 A 14.094 14.094 0 0 0 33.687 61.621 Q 33.898 63.435 34.618 64.53 A 4.105 4.105 0 0 0 34.7 64.65 Q 35.8 66.2 37.7 66.7 A 15.233 15.233 0 0 0 40.454 67.151 A 18.189 18.189 0 0 0 41.8 67.2 L 44.1 67.2 L 44.1 71.4 Z" vector-effect="non-scaling-stroke"></path>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg><style>@media (prefers-color-scheme: light) { :root { filter: none; } }
|
||||||
|
@media (prefers-color-scheme: dark) { :root { filter: none; } }
|
||||||
|
</style></svg>
|
||||||
|
After Width: | Height: | Size: 3.2 KiB |
2
public/robots.txt
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
User-agent: *
|
||||||
|
Allow: /
|
||||||
21
public/site.webmanifest
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"name": "Thilo Hohlt",
|
||||||
|
"short_name": "THohlt",
|
||||||
|
"icons": [
|
||||||
|
{
|
||||||
|
"src": "/web-app-manifest-192x192.png",
|
||||||
|
"sizes": "192x192",
|
||||||
|
"type": "image/png",
|
||||||
|
"purpose": "maskable"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "/web-app-manifest-512x512.png",
|
||||||
|
"sizes": "512x512",
|
||||||
|
"type": "image/png",
|
||||||
|
"purpose": "maskable"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"theme_color": "#ffffff",
|
||||||
|
"background_color": "#ffffff",
|
||||||
|
"display": "standalone"
|
||||||
|
}
|
||||||
BIN
public/web-app-manifest-192x192.png
Normal file
|
After Width: | Height: | Size: 4.6 KiB |
BIN
public/web-app-manifest-512x512.png
Normal file
|
After Width: | Height: | Size: 15 KiB |
9
remark-modified-time.mjs
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import { execSync } from "child_process";
|
||||||
|
|
||||||
|
export const remarkModifiedTime = () => {
|
||||||
|
return (tree, file) => {
|
||||||
|
const filepath = file.history[0];
|
||||||
|
const result = execSync(`git log -1 --pretty="format:%cI" "${filepath}"`);
|
||||||
|
file.data.astro.frontmatter.lastModified = result.toString();
|
||||||
|
};
|
||||||
|
};
|
||||||
17
src/components/Date.astro
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
---
|
||||||
|
interface Props {
|
||||||
|
date: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { date } = Astro.props;
|
||||||
|
---
|
||||||
|
|
||||||
|
<time datetime={date.toISOString()}>
|
||||||
|
{
|
||||||
|
date.toLocaleString("en-us", {
|
||||||
|
year: "numeric",
|
||||||
|
month: "long",
|
||||||
|
day: "numeric",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</time>
|
||||||
9
src/components/Footer.astro
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
<footer
|
||||||
|
class="flex flex-col items-center p-4 bg-neutral-100 dark:bg-neutral-900 prose prose-neutral dark:prose-invert max-w-none prose-a:text-blue-800 prose-a:dark:text-blue-300 prose-a:hover:no-underline"
|
||||||
|
>
|
||||||
|
<p class="mb-2">© 2025 Thilo Hohlt</p>
|
||||||
|
<div class="flex gap-4">
|
||||||
|
<a href="/legal-disclosure">Legal Disclosure</a>
|
||||||
|
<a href="https://github.com/thiloho">GitHub</a>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
49
src/components/Head.astro
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
---
|
||||||
|
import { ClientRouter } from "astro:transitions";
|
||||||
|
import "../styles/global.css";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
title: string;
|
||||||
|
metaDescription: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { title, metaDescription } = Astro.props;
|
||||||
|
---
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<link rel="icon" type="image/png" href="/favicon-96x96.png" sizes="96x96" />
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||||
|
<link rel="shortcut icon" href="/favicon.ico" />
|
||||||
|
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
|
||||||
|
<meta name="apple-mobile-web-app-title" content="THohlt" />
|
||||||
|
<link rel="manifest" href="/site.webmanifest" />
|
||||||
|
<meta name="viewport" content="width=device-width" />
|
||||||
|
<meta name="generator" content={Astro.generator} />
|
||||||
|
<title>{title}</title>
|
||||||
|
<meta name="description" content={metaDescription} />
|
||||||
|
<ClientRouter />
|
||||||
|
<script is:inline>
|
||||||
|
const setTheme = () => {
|
||||||
|
let theme = "light";
|
||||||
|
|
||||||
|
if (localStorage.getItem("theme")) {
|
||||||
|
theme = localStorage.getItem("theme");
|
||||||
|
} else if (window.matchMedia("(prefers-color-scheme: dark)").matches) {
|
||||||
|
theme = "dark";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (theme === "light") {
|
||||||
|
document.documentElement.classList.remove("dark");
|
||||||
|
} else {
|
||||||
|
document.documentElement.classList.add("dark");
|
||||||
|
}
|
||||||
|
|
||||||
|
window.localStorage.setItem("theme", theme);
|
||||||
|
};
|
||||||
|
|
||||||
|
setTheme();
|
||||||
|
|
||||||
|
document.addEventListener("astro:after-swap", setTheme);
|
||||||
|
</script>
|
||||||
|
</head>
|
||||||
33
src/components/Header.astro
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
---
|
||||||
|
import Date from "./Date.astro";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
title: string;
|
||||||
|
pubDate?: Date;
|
||||||
|
modDate?: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { title, pubDate, modDate } = Astro.props;
|
||||||
|
---
|
||||||
|
|
||||||
|
<header class="bg-white dark:bg-neutral-800">
|
||||||
|
<div
|
||||||
|
class="prose prose-neutral dark:prose-invert mx-auto px-4 py-8 border-b border-neutral-200 dark:border-neutral-700 prose-h1:font-bold"
|
||||||
|
>
|
||||||
|
{
|
||||||
|
pubDate ? (
|
||||||
|
<hgroup>
|
||||||
|
<h1 class="mb-2">{title}</h1>
|
||||||
|
<p>
|
||||||
|
Published: <Date date={pubDate} />
|
||||||
|
<br />
|
||||||
|
Last modified:{" "}
|
||||||
|
{modDate ? <Date date={modDate} /> : <span>No changes yet</span>}
|
||||||
|
</p>
|
||||||
|
</hgroup>
|
||||||
|
) : (
|
||||||
|
<h1>{title}</h1>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
77
src/components/Nav.astro
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
---
|
||||||
|
import Logo from "../img/TH.svg";
|
||||||
|
|
||||||
|
const routes = ["blog"];
|
||||||
|
---
|
||||||
|
|
||||||
|
<nav class="max-w-none bg-neutral-100 dark:bg-neutral-900 sticky top-0 z-10">
|
||||||
|
<div
|
||||||
|
class="dark:text-neutral-300 flex items-center justify-between max-w-screen-lg mx-auto ps-4 pe-2"
|
||||||
|
>
|
||||||
|
<a href="/" title="Home">
|
||||||
|
<Logo width={42} height={42} />
|
||||||
|
</a>
|
||||||
|
<div class="flex">
|
||||||
|
{
|
||||||
|
routes.map((route) => (
|
||||||
|
<a
|
||||||
|
class="inline-block p-2 border-b-2 border-transparent hover:bg-neutral-200 hover:border-neutral-400 hover:dark:bg-neutral-700 hover:dark:border-neutral-600"
|
||||||
|
href={`/${route}`}
|
||||||
|
>
|
||||||
|
{route
|
||||||
|
.split(" ")
|
||||||
|
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
|
||||||
|
.join(" ")}
|
||||||
|
</a>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
<button
|
||||||
|
class="theme-toggle p-2 cursor-pointer border-b-2 border-transparent hover:bg-neutral-200 hover:border-neutral-400 hover:dark:bg-neutral-700 hover:dark:border-neutral-600"
|
||||||
|
title="Toggle dark mode"
|
||||||
|
>
|
||||||
|
<!-- Moon -->
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
fill="currentColor"
|
||||||
|
class="size-5 dark:hidden"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fill-rule="evenodd"
|
||||||
|
d="M7.455 2.004a.75.75 0 0 1 .26.77 7 7 0 0 0 9.958 7.967.75.75 0 0 1 1.067.853A8.5 8.5 0 1 1 6.647 1.921a.75.75 0 0 1 .808.083Z"
|
||||||
|
clip-rule="evenodd"></path>
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
<!-- Sun -->
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
fill="currentColor"
|
||||||
|
class="size-5 hidden dark:block"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M10 2a.75.75 0 0 1 .75.75v1.5a.75.75 0 0 1-1.5 0v-1.5A.75.75 0 0 1 10 2ZM10 15a.75.75 0 0 1 .75.75v1.5a.75.75 0 0 1-1.5 0v-1.5A.75.75 0 0 1 10 15ZM10 7a3 3 0 1 0 0 6 3 3 0 0 0 0-6ZM15.657 5.404a.75.75 0 1 0-1.06-1.06l-1.061 1.06a.75.75 0 0 0 1.06 1.06l1.06-1.06ZM6.464 14.596a.75.75 0 1 0-1.06-1.06l-1.06 1.06a.75.75 0 0 0 1.06 1.06l1.06-1.06ZM18 10a.75.75 0 0 1-.75.75h-1.5a.75.75 0 0 1 0-1.5h1.5A.75.75 0 0 1 18 10ZM5 10a.75.75 0 0 1-.75.75h-1.5a.75.75 0 0 1 0-1.5h1.5A.75.75 0 0 1 5 10ZM14.596 15.657a.75.75 0 0 0 1.06-1.06l-1.06-1.061a.75.75 0 1 0-1.06 1.06l1.06 1.06ZM5.404 6.464a.75.75 0 0 0 1.06-1.06l-1.06-1.06a.75.75 0 1 0-1.061 1.06l1.06 1.06Z"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const setToggleListener = () => {
|
||||||
|
const toggleBtn = document.querySelector(".theme-toggle");
|
||||||
|
|
||||||
|
toggleBtn?.addEventListener("click", () => {
|
||||||
|
const element = document.documentElement;
|
||||||
|
element.classList.toggle("dark");
|
||||||
|
|
||||||
|
const isDark = element.classList.contains("dark");
|
||||||
|
localStorage.setItem("theme", isDark ? "dark" : "light");
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
setToggleListener();
|
||||||
|
|
||||||
|
document.addEventListener("astro:after-swap", setToggleListener);
|
||||||
|
</script>
|
||||||
23
src/content.config.ts
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import { defineCollection, z } from "astro:content";
|
||||||
|
import { glob } from "astro/loaders";
|
||||||
|
|
||||||
|
const index = defineCollection({
|
||||||
|
loader: glob({ pattern: "**/*.md", base: "./src/content/index" }),
|
||||||
|
});
|
||||||
|
|
||||||
|
const blog = defineCollection({
|
||||||
|
loader: glob({ pattern: "**/*.md", base: "./src/content/blog" }),
|
||||||
|
schema: z.object({
|
||||||
|
id: z.number().positive(),
|
||||||
|
title: z.string(),
|
||||||
|
description: z.string(),
|
||||||
|
pubDate: z.coerce.date(),
|
||||||
|
modDate: z.coerce.date().optional(),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const legal = defineCollection({
|
||||||
|
loader: glob({ pattern: "**/*.md", base: "./src/content/legal" }),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const collections = { index, blog, legal };
|
||||||
|
After Width: | Height: | Size: 184 KiB |
136
src/content/blog/nixos-with-ext4-and-luks/index.md
Normal file
@@ -0,0 +1,136 @@
|
|||||||
|
---
|
||||||
|
id: 1
|
||||||
|
title: "Steps to install NixOS on a system with ext4 and LUKS"
|
||||||
|
description: "A guide to installing NixOS with full disk encryption using LUKS and LVM, showing the complete process from disk partitioning to system configuration"
|
||||||
|
pubDate: "2025-01-04"
|
||||||
|
---
|
||||||
|
|
||||||
|
## Disk layout
|
||||||
|
|
||||||
|
```sh
|
||||||
|
NAME MAJ:MIN RM SIZE RO TYPE MOUNTPOINT
|
||||||
|
sda 8:0 0 233.8G 0 disk
|
||||||
|
├─sda1 8:1 0 500M 0 part /boot # Unencrypted EFI partition
|
||||||
|
└─sda2 8:2 0 233.3G 0 part # Encrypted partition
|
||||||
|
└─cryptroot 254:0 0 233.3G 0 crypt # LUKS container
|
||||||
|
├─vg-swap 254:1 0 8G 0 lvm [SWAP] # LVM swap volume
|
||||||
|
└─vg-root 254:2 0 225.3G 0 lvm / # LVM root volume
|
||||||
|
```
|
||||||
|
|
||||||
|
## Partitioning
|
||||||
|
|
||||||
|
```
|
||||||
|
parted /dev/sda -- mklabel gpt
|
||||||
|
|
||||||
|
parted /dev/sda -- mkpart ESP fat32 1MB 512MB
|
||||||
|
|
||||||
|
parted /dev/sda -- mkpart primary 512MB 100%
|
||||||
|
|
||||||
|
parted /dev/sda -- set 1 esp on
|
||||||
|
```
|
||||||
|
|
||||||
|
## Setting up Encryption
|
||||||
|
|
||||||
|
```
|
||||||
|
cryptsetup luksFormat /dev/sda2
|
||||||
|
|
||||||
|
cryptsetup luksOpen /dev/sda2 cryptroot
|
||||||
|
```
|
||||||
|
|
||||||
|
## Setting up LVM
|
||||||
|
|
||||||
|
```
|
||||||
|
pvcreate /dev/mapper/cryptroot
|
||||||
|
|
||||||
|
vgcreate vg /dev/mapper/cryptroot
|
||||||
|
|
||||||
|
lvcreate -L 8G vg -n swap
|
||||||
|
|
||||||
|
lvcreate -l 100%FREE vg -n root
|
||||||
|
```
|
||||||
|
|
||||||
|
## Creating Filesystems
|
||||||
|
|
||||||
|
```
|
||||||
|
mkfs.fat -F 32 -n boot /dev/sda1
|
||||||
|
|
||||||
|
mkfs.ext4 -L root /dev/vg/root
|
||||||
|
|
||||||
|
mkswap -L swap /dev/vg/swap
|
||||||
|
```
|
||||||
|
|
||||||
|
## Mounting Filesystems
|
||||||
|
|
||||||
|
```
|
||||||
|
mount /dev/vg/root /mnt
|
||||||
|
|
||||||
|
mkdir -p /mnt/boot
|
||||||
|
mount -o umask=077 /dev/sda1 /mnt/boot
|
||||||
|
|
||||||
|
swapon /dev/vg/swap
|
||||||
|
```
|
||||||
|
|
||||||
|
## NixOS configuration
|
||||||
|
|
||||||
|
```sh
|
||||||
|
nixos-generate-config --root /mnt
|
||||||
|
|
||||||
|
# Get UUID of encrypted partition (needed for configuration)
|
||||||
|
blkid -s UUID /dev/sda2
|
||||||
|
```
|
||||||
|
|
||||||
|
Edit `/mnt/etc/nixos/configuration.nix`:
|
||||||
|
|
||||||
|
```nix
|
||||||
|
{ config, lib, pkgs, ... }:
|
||||||
|
{
|
||||||
|
boot = {
|
||||||
|
loader = {
|
||||||
|
systemd-boot.enable = true;
|
||||||
|
efi.canTouchEfiVariables = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
# Encryption configuration
|
||||||
|
initrd = {
|
||||||
|
luks.devices = {
|
||||||
|
cryptroot = {
|
||||||
|
device = "/dev/disk/by-uuid/UUID-OF-SDA2"; # Replace with your UUID
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
# ...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
```
|
||||||
|
nixos-install
|
||||||
|
|
||||||
|
reboot
|
||||||
|
```
|
||||||
|
|
||||||
|
## How it works
|
||||||
|
|
||||||
|
1. **UEFI Phase**
|
||||||
|
- The UEFI firmware loads systemd-boot from the unencrypted /boot partition
|
||||||
|
- systemd-boot loads the NixOS kernel and initrd
|
||||||
|
2. **Early boot**
|
||||||
|
- Kernel starts and loads initrd
|
||||||
|
- initrd asks for LUKS passphrase
|
||||||
|
- after entering correct passphrase, /dev/sda2 will be decrypted
|
||||||
|
3. **LVM setup**
|
||||||
|
- LVM volumes are available after decryption
|
||||||
|
- System can now access root and swap volumes
|
||||||
|
4. **System start**
|
||||||
|
- Root file system is mounted
|
||||||
|
- Control handed over to systemd
|
||||||
|
- regular boot process continues
|
||||||
|
|
||||||
|
## Change of encryption password
|
||||||
|
|
||||||
|
To make this step as easy as possible, I recommend using [GNOME Disks](https://apps.gnome.org/DiskUtility).
|
||||||
|
|
||||||
|

|
||||||
39
src/content/blog/privacy-focused-operating-systems.md
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
---
|
||||||
|
id: 2
|
||||||
|
title: "Privacy-focused operating systems"
|
||||||
|
description: "Good choices for privacy-focused operating systems for desktop and mobile phones."
|
||||||
|
pubDate: "2025-01-16"
|
||||||
|
---
|
||||||
|
|
||||||
|
## Introduction
|
||||||
|
|
||||||
|
Privacy on the Internet is a fundamental right and there are many steps you can take to protect your personal information. Various companies and services have developed sophisticated strategies to collect as much information from you as possible. A fundamental aspect of many people's daily lives is the operating system they run on both their desktop computer and their mobile phone, and while the most popular ones offer great usability and features, they are usually not in favor of your privacy.
|
||||||
|
|
||||||
|
Below are the options that, after a few years of testing, I have found to be the most usable and at the same time the most privacy and security oriented.
|
||||||
|
|
||||||
|
## Desktop
|
||||||
|
|
||||||
|
I would recommend using one of the more popular GNU/Linux distributions here, as they have many contributors and are well maintained. Some options that I find great are:
|
||||||
|
|
||||||
|
- NixOS
|
||||||
|
- ArchLinux
|
||||||
|
- Debian
|
||||||
|
- Fedora
|
||||||
|
|
||||||
|
You should stick with one of them and use it for at least a few months or years, or commit to it completely. Most of them are the same anyway, and with Flatpak there is already a universally usable packaging system that contains most of the relevant software you might need if it is not included in the distribution's package repository.
|
||||||
|
|
||||||
|
There are other great options, such as [FreeBSD](https://www.freebsd.org) and [OpenBSD](https://www.openbsd.org), but these may not be as easy to use as GNU/Linux, and there may be problems with hardware compatibility and software availability.
|
||||||
|
|
||||||
|
## Mobile
|
||||||
|
|
||||||
|
Your main choice here should probably be [GrapheneOS](https://grapheneos.org).
|
||||||
|
|
||||||
|
> GrapheneOS is an open source, privacy and security-focused Android operating system that runs on selected Google Pixel devices, including smartphones, tablets and foldables.
|
||||||
|
|
||||||
|
As mentioned in the quote, note that you need a [supported Google Pixel device](https://grapheneos.org/faq#supported-devices) to use GrapheneOS and I would not recommend using any other privacy focused or hardened mobile operating system as they do not come close to its usability while maintaining these aspects.
|
||||||
|
|
||||||
|
## Conclusion
|
||||||
|
|
||||||
|
Aside from operating systems, there are of course many different components that are needed to meet one's digital needs. If you are interested in digital privacy, I would advise you to check out the [Privacy Guide's Recommendations](https://www.privacyguides.org/en/tools), there are probably several things you can replace with more private alternatives without much effort. Having a VPN connection that is always enabled on all of your devices is also a good utility to have, and it is not that expensive.
|
||||||
|
|
||||||
|
Some services require personal information for verification or payment purposes, but just know that you can greatly limit the information you disclose about yourself by making the right choices.
|
||||||
25
src/content/index/index.md
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
## About
|
||||||
|
|
||||||
|
I am a software developer from Germany who is passionate about building high quality websites and web applications. I value privacy on the Internet and prefer to use free/libre open source software whenever possible. I switched to GNU/Linux for desktop use on all my machines a few years ago and it has been my daily driver ever since.
|
||||||
|
|
||||||
|
## Build
|
||||||
|
|
||||||
|
| Component | Selection |
|
||||||
|
| ------------ | ----------------------------------------------------------------------------- |
|
||||||
|
| CPU | AMD Ryzen 7 7700X 4.5 GHz 8-Core Processor |
|
||||||
|
| CPU Cooler | Noctua NH-U12A chromax.black 60.09 CFM CPU Cooler |
|
||||||
|
| Motherboard | ASRock B650M-HDV/M.2 Micro ATX AM5 Motherboard |
|
||||||
|
| Memory | G.Skill Trident Z5 Neo 32 GB (2 x 16 GB) DDR5-6000 CL30 Memory |
|
||||||
|
| Storage | Samsung 980 Pro 2 TB M.2-2280 PCIe 4.0 X4 NVME Solid State Drive |
|
||||||
|
| Video Card | XFX Speedster MERC 319 Radeon RX 6950 XT 16 GB Video Card |
|
||||||
|
| Case | Fractal Design Pop Mini Air MicroATX Mid Tower Case |
|
||||||
|
| Power Supply | SeaSonic FOCUS PX 850 W 80+ Platinum Certified Fully Modular ATX Power Supply |
|
||||||
|
|
||||||
|
## Software
|
||||||
|
|
||||||
|
- [NixOS](https://nixos.org)
|
||||||
|
- [GNOME](https://www.gnome.org)
|
||||||
|
- [VSCodium](https://vscodium.com)
|
||||||
|
- [Firefox Developer Edition](https://www.mozilla.org/en-US/firefox/developer)
|
||||||
|
- [Tuta Mail](https://tuta.com)
|
||||||
|
- [Mullvad VPN](https://mullvad.net)
|
||||||
12
src/content/legal/legal.md
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
Information according to [§ 5 DDG](https://gesetz-digitale-dienste.de/5-ddg):
|
||||||
|
|
||||||
|
Thilo Hohlt
|
||||||
|
|
||||||
|
c/o IP-Management #3723 \
|
||||||
|
Ludwig-Erhard-Str. 18 \
|
||||||
|
20459 Hamburg
|
||||||
|
|
||||||
|
Contact:
|
||||||
|
|
||||||
|
E-Mail: [contact@thilohohlt.com](mailto:contact@thilohohlt.com) \
|
||||||
|
Phone: [+49 171 7599950](tel:+491717599950)
|
||||||
1
src/img/TH.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg width="134.8" height="71.4" viewBox="0 0 134.8 71.4" xmlns="http://www.w3.org/2000/svg"><g id="svgGroup" stroke-linecap="round" fill-rule="evenodd" font-size="9pt" stroke="#525252" stroke-width="0.125mm" fill="#a1a1a1" style="stroke:#525252;stroke-width:0.125mm;fill:#a1a1a1"><path d="M 92.2 71.4 L 63.1 71.4 L 63.1 67.2 L 64.4 67.2 Q 66.7 67.2 68.55 66.7 Q 70.4 66.2 71.5 64.65 A 5.045 5.045 0 0 0 72.17 63.27 Q 72.587 61.979 72.6 60.121 A 17.219 17.219 0 0 0 72.6 60 L 72.6 11 A 12.54 12.54 0 0 0 72.509 9.435 Q 72.285 7.659 71.509 6.627 A 3.715 3.715 0 0 0 71.45 6.55 Q 70.3 5.1 68.45 4.65 A 15.785 15.785 0 0 0 66.001 4.263 A 19.693 19.693 0 0 0 64.4 4.2 L 63.1 4.2 L 63.1 0 L 92.2 0 L 92.2 4.2 L 90.9 4.2 A 17.148 17.148 0 0 0 88.267 4.395 A 14.51 14.51 0 0 0 86.8 4.7 A 5.523 5.523 0 0 0 84.882 5.604 A 5.171 5.171 0 0 0 83.8 6.7 Q 82.771 8.104 82.705 10.996 A 17.684 17.684 0 0 0 82.7 11.4 L 82.7 31.6 L 115.2 31.6 L 115.2 11.4 A 14.799 14.799 0 0 0 115.113 9.734 Q 114.92 8.032 114.296 6.995 A 3.873 3.873 0 0 0 114.1 6.7 Q 113 5.2 111.15 4.7 A 14.115 14.115 0 0 0 108.926 4.296 A 18.631 18.631 0 0 0 107 4.2 L 105.7 4.2 L 105.7 0 L 134.8 0 L 134.8 4.2 L 133.5 4.2 A 17.148 17.148 0 0 0 130.867 4.395 A 14.51 14.51 0 0 0 129.4 4.7 A 5.523 5.523 0 0 0 127.482 5.604 A 5.171 5.171 0 0 0 126.4 6.7 Q 125.371 8.104 125.305 10.996 A 17.684 17.684 0 0 0 125.3 11.4 L 125.3 60.5 A 11.673 11.673 0 0 0 125.401 62.094 Q 125.512 62.894 125.743 63.543 A 4.544 4.544 0 0 0 126.45 64.85 Q 127.6 66.3 129.45 66.75 A 15.785 15.785 0 0 0 131.899 67.137 A 19.693 19.693 0 0 0 133.5 67.2 L 134.8 67.2 L 134.8 71.4 L 105.7 71.4 L 105.7 67.2 L 107 67.2 Q 109.3 67.2 111.15 66.7 Q 113 66.2 114.1 64.65 A 5.045 5.045 0 0 0 114.77 63.27 Q 115.187 61.979 115.2 60.121 A 17.219 17.219 0 0 0 115.2 60 L 115.2 36.6 L 82.7 36.6 L 82.7 60 A 14.094 14.094 0 0 0 82.787 61.621 Q 82.998 63.435 83.718 64.53 A 4.105 4.105 0 0 0 83.8 64.65 Q 84.9 66.2 86.8 66.7 A 15.233 15.233 0 0 0 89.554 67.151 A 18.189 18.189 0 0 0 90.9 67.2 L 92.2 67.2 L 92.2 71.4 Z M 44.1 71.4 L 13 71.4 L 13 67.2 L 15.3 67.2 A 18.863 18.863 0 0 0 17.664 67.058 A 15.182 15.182 0 0 0 19.35 66.75 Q 21.2 66.3 22.35 64.85 Q 23.179 63.805 23.41 62.007 A 11.832 11.832 0 0 0 23.5 60.5 L 23.5 5 L 13.9 5 Q 11.343 5 9.698 5.828 A 4.991 4.991 0 0 0 7.8 7.45 Q 6.1 9.9 5.7 13.2 L 5.2 17.5 L 0 17.5 L 0.5 0 L 56.8 0 L 57.3 17.5 L 52.1 17.5 L 51.6 13.2 A 13.62 13.62 0 0 0 50.739 9.756 A 11.607 11.607 0 0 0 49.5 7.45 Q 48.152 5.507 45.043 5.105 A 13.611 13.611 0 0 0 43.3 5 L 33.6 5 L 33.6 60 A 14.094 14.094 0 0 0 33.687 61.621 Q 33.898 63.435 34.618 64.53 A 4.105 4.105 0 0 0 34.7 64.65 Q 35.8 66.2 37.7 66.7 A 15.233 15.233 0 0 0 40.454 67.151 A 18.189 18.189 0 0 0 41.8 67.2 L 44.1 67.2 L 44.1 71.4 Z" vector-effect="non-scaling-stroke"/></g></svg>
|
||||||
|
After Width: | Height: | Size: 2.7 KiB |
3
src/img/icons/bars-arrow-down.svg
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M2.25 4.5C2.25 4.08579 2.58579 3.75 3 3.75H17.25C17.6642 3.75 18 4.08579 18 4.5C18 4.91421 17.6642 5.25 17.25 5.25H3C2.58579 5.25 2.25 4.91421 2.25 4.5ZM2.25 9C2.25 8.58579 2.58579 8.25 3 8.25H12.75C13.1642 8.25 13.5 8.58579 13.5 9C13.5 9.41421 13.1642 9.75 12.75 9.75H3C2.58579 9.75 2.25 9.41421 2.25 9ZM17.25 8.25C17.6642 8.25 18 8.58579 18 9V19.1893L20.4697 16.7197C20.7626 16.4268 21.2374 16.4268 21.5303 16.7197C21.8232 17.0126 21.8232 17.4874 21.5303 17.7803L17.7803 21.5303C17.4874 21.8232 17.0126 21.8232 16.7197 21.5303L12.9697 17.7803C12.6768 17.4874 12.6768 17.0126 12.9697 16.7197C13.2626 16.4268 13.7374 16.4268 14.0303 16.7197L16.5 19.1893V9C16.5 8.58579 16.8358 8.25 17.25 8.25ZM2.25 13.5C2.25 13.0858 2.58579 12.75 3 12.75H12.75C13.1642 12.75 13.5 13.0858 13.5 13.5C13.5 13.9142 13.1642 14.25 12.75 14.25H3C2.58579 14.25 2.25 13.9142 2.25 13.5Z" fill="#0F172A"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.0 KiB |
3
src/img/icons/moon.svg
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M7.45519 2.00395C7.68518 2.18765 7.78646 2.4889 7.71414 2.77423C7.57443 3.32547 7.5 3.90348 7.5 4.49996C7.5 8.36595 10.634 11.5 14.5 11.5C15.6435 11.5 16.721 11.2263 17.6724 10.7417C17.9347 10.608 18.2509 10.6402 18.4809 10.8239C18.7109 11.0076 18.8122 11.3089 18.7399 11.5942C17.8069 15.2755 14.4725 18 10.5 18C5.80558 18 2 14.1944 2 9.49996C2 6.19139 3.89048 3.32567 6.64671 1.92168C6.909 1.78807 7.22519 1.82025 7.45519 2.00395Z" fill="#0F172A"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 602 B |
11
src/img/icons/sun.svg
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M10 2C10.4142 2 10.75 2.33579 10.75 2.75V4.25C10.75 4.66421 10.4142 5 10 5C9.58579 5 9.25 4.66421 9.25 4.25V2.75C9.25 2.33579 9.58579 2 10 2Z" fill="#0F172A"/>
|
||||||
|
<path d="M10 15C10.4142 15 10.75 15.3358 10.75 15.75V17.25C10.75 17.6642 10.4142 18 10 18C9.58579 18 9.25 17.6642 9.25 17.25V15.75C9.25 15.3358 9.58579 15 10 15Z" fill="#0F172A"/>
|
||||||
|
<path d="M10 7C8.34315 7 7 8.34315 7 10C7 11.6569 8.34315 13 10 13C11.6569 13 13 11.6569 13 10C13 8.34315 11.6569 7 10 7Z" fill="#0F172A"/>
|
||||||
|
<path d="M15.6568 5.40386C15.9497 5.11096 15.9497 4.63609 15.6568 4.3432C15.3639 4.0503 14.889 4.0503 14.5961 4.3432L13.5355 5.40386C13.2426 5.69675 13.2426 6.17162 13.5355 6.46452C13.8284 6.75741 14.3032 6.75741 14.5961 6.46452L15.6568 5.40386Z" fill="#0F172A"/>
|
||||||
|
<path d="M6.46441 14.5962C6.7573 14.3034 6.7573 13.8285 6.46441 13.5356C6.17151 13.2427 5.69664 13.2427 5.40375 13.5356L4.34309 14.5962C4.05019 14.8891 4.05019 15.364 4.34309 15.6569C4.63598 15.9498 5.11085 15.9498 5.40375 15.6569L6.46441 14.5962Z" fill="#0F172A"/>
|
||||||
|
<path d="M18 10C18 10.4142 17.6642 10.75 17.25 10.75H15.75C15.3358 10.75 15 10.4142 15 10C15 9.58579 15.3358 9.25 15.75 9.25H17.25C17.6642 9.25 18 9.58579 18 10Z" fill="#0F172A"/>
|
||||||
|
<path d="M5 10C5 10.4142 4.66421 10.75 4.25 10.75H2.75C2.33579 10.75 2 10.4142 2 10C2 9.58579 2.33579 9.25 2.75 9.25H4.25C4.66421 9.25 5 9.58579 5 10Z" fill="#0F172A"/>
|
||||||
|
<path d="M14.596 15.6568C14.8889 15.9497 15.3638 15.9497 15.6567 15.6568C15.9496 15.3639 15.9496 14.889 15.6567 14.5961L14.596 13.5355C14.3031 13.2426 13.8283 13.2426 13.5354 13.5355C13.2425 13.8284 13.2425 14.3032 13.5354 14.5961L14.596 15.6568Z" fill="#0F172A"/>
|
||||||
|
<path d="M5.40363 6.46441C5.69653 6.7573 6.1714 6.7573 6.46429 6.46441C6.75719 6.17151 6.75719 5.69664 6.46429 5.40375L5.40363 4.34309C5.11074 4.05019 4.63587 4.05019 4.34297 4.34309C4.05008 4.63598 4.05008 5.11085 4.34297 5.40375L5.40363 6.46441Z" fill="#0F172A"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 2.0 KiB |
31
src/layouts/PageLayout.astro
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
---
|
||||||
|
import Head from "../components/Head.astro";
|
||||||
|
import Nav from "../components/Nav.astro";
|
||||||
|
import Header from "../components/Header.astro";
|
||||||
|
import Footer from "../components/Footer.astro";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
title: string;
|
||||||
|
metaDescription: string;
|
||||||
|
pubDate?: Date;
|
||||||
|
modDate?: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { title, metaDescription, pubDate, modDate } = Astro.props;
|
||||||
|
---
|
||||||
|
|
||||||
|
<html lang="en" class="light">
|
||||||
|
<Head {title} {metaDescription} />
|
||||||
|
<body class="min-h-screen flex flex-col">
|
||||||
|
<Nav />
|
||||||
|
<Header {title} {pubDate} {modDate} />
|
||||||
|
<main class="flex-1 bg-white dark:bg-neutral-800">
|
||||||
|
<div
|
||||||
|
class={`relative prose prose-neutral dark:prose-invert mx-auto px-4 ${pubDate ? "pt-0" : "pt-8"} pb-16 prose-headings:scroll-mt-16 prose-h1:font-bold prose-pre:!bg-neutral-700 prose-a:text-blue-800 prose-a:dark:text-blue-300 prose-a:hover:no-underline`}
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
<Footer />
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
87
src/pages/blog/[slug].astro
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
---
|
||||||
|
import PageLayout from "../../layouts/PageLayout.astro";
|
||||||
|
import { getCollection, getEntry, render } from "astro:content";
|
||||||
|
|
||||||
|
export const getStaticPaths = async () => {
|
||||||
|
const allArticles = await getCollection("blog");
|
||||||
|
return allArticles.map((article) => ({
|
||||||
|
params: { slug: article.id },
|
||||||
|
props: { article },
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
const { slug } = Astro.params;
|
||||||
|
const article = await getEntry("blog", slug);
|
||||||
|
|
||||||
|
if (!article) {
|
||||||
|
throw new Error();
|
||||||
|
}
|
||||||
|
|
||||||
|
const { Content, headings, remarkPluginFrontmatter } = await render(article);
|
||||||
|
---
|
||||||
|
|
||||||
|
<PageLayout
|
||||||
|
title={article.data.title}
|
||||||
|
metaDescription="Blog"
|
||||||
|
pubDate={article.data.pubDate}
|
||||||
|
modDate={remarkPluginFrontmatter.lastModified}
|
||||||
|
>
|
||||||
|
<details class="toc sticky top-0 z-20">
|
||||||
|
<summary
|
||||||
|
title="Table of contents"
|
||||||
|
class="flex mx-auto w-fit cursor-pointer list-none p-2 border-b-2 border-transparent hover:bg-neutral-200 hover:border-neutral-400 hover:dark:bg-neutral-700 hover:dark:border-neutral-600"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="currentColor"
|
||||||
|
class="size-6"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fill-rule="evenodd"
|
||||||
|
d="M2.25 4.5A.75.75 0 0 1 3 3.75h14.25a.75.75 0 0 1 0 1.5H3a.75.75 0 0 1-.75-.75Zm0 4.5A.75.75 0 0 1 3 8.25h9.75a.75.75 0 0 1 0 1.5H3A.75.75 0 0 1 2.25 9Zm15-.75A.75.75 0 0 1 18 9v10.19l2.47-2.47a.75.75 0 1 1 1.06 1.06l-3.75 3.75a.75.75 0 0 1-1.06 0l-3.75-3.75a.75.75 0 1 1 1.06-1.06l2.47 2.47V9a.75.75 0 0 1 .75-.75Zm-15 5.25a.75.75 0 0 1 .75-.75h9.75a.75.75 0 0 1 0 1.5H3a.75.75 0 0 1-.75-.75Z"
|
||||||
|
clip-rule="evenodd"></path>
|
||||||
|
</svg>
|
||||||
|
</summary>
|
||||||
|
<div
|
||||||
|
class="not-prose border border-neutral-400 dark:border-neutral-600 bg-white dark:bg-neutral-800 p-2 max-h-[calc(100vh-4rem)] overflow-y-scroll"
|
||||||
|
>
|
||||||
|
<p class="text-center">
|
||||||
|
<strong class="text-sm">Table of Contents</strong>
|
||||||
|
</p>
|
||||||
|
<ul>
|
||||||
|
{
|
||||||
|
headings
|
||||||
|
.filter(({ depth }) => depth === 2)
|
||||||
|
.map((heading) => (
|
||||||
|
<li>
|
||||||
|
<a
|
||||||
|
class="text-center text-blue-800 dark:text-blue-300 block py-1 px-2 hover:underline"
|
||||||
|
href={`#${heading.slug}`}
|
||||||
|
aria-labelledby={`Section: ${heading.slug}`}
|
||||||
|
>
|
||||||
|
{heading.text}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
<Content />
|
||||||
|
</PageLayout>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const setAnchorListener = () => {
|
||||||
|
const tocLinks = document.querySelectorAll(".toc a");
|
||||||
|
|
||||||
|
for (const link of tocLinks) {
|
||||||
|
link.addEventListener("click", () => {
|
||||||
|
document.querySelector(".toc")?.removeAttribute("open");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
setAnchorListener();
|
||||||
|
document.addEventListener("astro:after-swap", setAnchorListener);
|
||||||
|
</script>
|
||||||
29
src/pages/blog/index.astro
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
---
|
||||||
|
import PageLayout from "../../layouts/PageLayout.astro";
|
||||||
|
import { getCollection } from "astro:content";
|
||||||
|
import Date from "../../components/Date.astro";
|
||||||
|
|
||||||
|
const allArticles = await getCollection("blog");
|
||||||
|
const sortedArticles = allArticles.sort((a, b) => {
|
||||||
|
return b.data.pubDate.valueOf() - a.data.pubDate.valueOf();
|
||||||
|
});
|
||||||
|
---
|
||||||
|
|
||||||
|
<PageLayout title="Blog" metaDescription="Blog">
|
||||||
|
<ul>
|
||||||
|
{
|
||||||
|
sortedArticles.map((article) => (
|
||||||
|
<li class="gap-1">
|
||||||
|
<Date date={article.data.pubDate} />
|
||||||
|
<span>»</span>
|
||||||
|
<a
|
||||||
|
class="text-blue-800 hover:no-underline"
|
||||||
|
href={`/blog/${article.id}`}
|
||||||
|
>
|
||||||
|
{article.data.title}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
</ul>
|
||||||
|
</PageLayout>
|
||||||
7
src/pages/contact.astro
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
---
|
||||||
|
import PageLayout from "../layouts/PageLayout.astro";
|
||||||
|
---
|
||||||
|
|
||||||
|
<PageLayout title="Contact" metaDescription="Contact">
|
||||||
|
<p>Example content</p>
|
||||||
|
</PageLayout>
|
||||||
16
src/pages/index.astro
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
---
|
||||||
|
import PageLayout from "../layouts/PageLayout.astro";
|
||||||
|
import { getEntry, render } from "astro:content";
|
||||||
|
|
||||||
|
const indexContent = await getEntry("index", "index");
|
||||||
|
|
||||||
|
if (!indexContent) {
|
||||||
|
throw new Error();
|
||||||
|
}
|
||||||
|
|
||||||
|
const { Content } = await render(indexContent);
|
||||||
|
---
|
||||||
|
|
||||||
|
<PageLayout title="Thilo Hohlt" metaDescription="Thilo Hohlt">
|
||||||
|
<Content />
|
||||||
|
</PageLayout>
|
||||||
16
src/pages/legal-disclosure.astro
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
---
|
||||||
|
import PageLayout from "../layouts/PageLayout.astro";
|
||||||
|
import { getEntry, render } from "astro:content";
|
||||||
|
|
||||||
|
const legalContent = await getEntry("legal", "legal");
|
||||||
|
|
||||||
|
if (!legalContent) {
|
||||||
|
throw new Error();
|
||||||
|
}
|
||||||
|
|
||||||
|
const { Content } = await render(legalContent);
|
||||||
|
---
|
||||||
|
|
||||||
|
<PageLayout title="Legal Disclosure" metaDescription="Legal Disclosure">
|
||||||
|
<Content />
|
||||||
|
</PageLayout>
|
||||||
4
src/styles/global.css
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
@import "tailwindcss";
|
||||||
|
@plugin "@tailwindcss/typography";
|
||||||
|
|
||||||
|
@custom-variant dark (&:where(.dark, .dark *));
|
||||||
5
tsconfig.json
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"extends": "astro/tsconfigs/strict",
|
||||||
|
"include": [".astro/types.d.ts", "**/*"],
|
||||||
|
"exclude": ["dist"]
|
||||||
|
}
|
||||||