Compare commits
76 commits
Author | SHA1 | Date | |
---|---|---|---|
63b4438463 | |||
7989f98776 | |||
2608fd281d | |||
19e2605b03 | |||
d12dfc6d91 | |||
5a74fb5ef5 | |||
10a550aea2 | |||
1d51866188 | |||
f969c3620d | |||
5057118cc3 | |||
66f85f80f2 | |||
76a9cbb157 | |||
12ae79ecd8 | |||
354c45a260 | |||
186ff13f6c | |||
84585ca242 | |||
ce129548d1 | |||
4f07ca6526 | |||
62166113fa | |||
82774c08f4 | |||
5c642d0626 | |||
6a8a1a5871 | |||
0e156d5f61 | |||
7c973ac48a | |||
0c245cb055 | |||
b5aabf34a3 | |||
d2bb4cf502 | |||
9e9aacfbcf | |||
af1cd7ed91 | |||
4566c85c44 | |||
bd785f0fc0 | |||
7d46ceeae7 | |||
c6891b9e1f | |||
1f342fb6fd | |||
6382e55f70 | |||
41f6a74b69 | |||
2b17edc92b | |||
92507301ac | |||
8b72fb085f | |||
4fbb7f0321 | |||
71dd878651 | |||
![]() |
8e245ce07b | ||
429e7ff3b0 | |||
2dc58f20ce | |||
90bab656bf | |||
614a021b1e | |||
88d816c144 | |||
4a34d5d0c7 | |||
f7c69d649b | |||
4abdde9ec9 | |||
4e19138a89 | |||
e00d1a023e | |||
0f6b1b1b7b | |||
6fc7af6488 | |||
4ba483e420 | |||
0fb6a5ff36 | |||
e4c39ab2fb | |||
8fd2c0d180 | |||
de6ad54657 | |||
1b464c0f93 | |||
2278272c15 | |||
75491a3f7c | |||
ebe9030bfb | |||
16d12bb39c | |||
48a635a68d | |||
b208dbcc9d | |||
d57ed2986f | |||
995cd06420 | |||
20c91cb8dc | |||
35ce9ed2be | |||
e85e882f48 | |||
588660ac62 | |||
b6a29a7f22 | |||
d2033cf715 | |||
e97083c325 | |||
4d980e399c |
1
.env
|
@ -1 +0,0 @@
|
|||
DATABASE_URL=postgres://postgres:localdb@localhost:5432/postgres
|
14
.forgejo/workflows/deploy.yaml
Normal file
|
@ -0,0 +1,14 @@
|
|||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- run: npm i
|
||||
- run: npm run build
|
||||
- run: ssh-keyscan hephaestus >> ~/.ssh/known_hosts
|
||||
- run: /bin/rsync -avzh ./dist ansible@hephaestus:/var/www/polsevev/
|
||||
|
29
.github/workflows/main.yml
vendored
|
@ -1,29 +0,0 @@
|
|||
name: build and deploy to prod!
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
jobs:
|
||||
build:
|
||||
name: Build Frontend
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 18
|
||||
- run: |
|
||||
cd polsevev.dev.frontend
|
||||
npm install
|
||||
npm run build
|
||||
- name: Rsync to server
|
||||
uses: burnett01/rsync-deployments@5.2
|
||||
with:
|
||||
switches: -avzr --delete
|
||||
path: polsevev.dev.frontend/dist
|
||||
remote_path: /home/beepsort/homepage
|
||||
remote_host: ${{secrets.SERVER_IP}}
|
||||
remote_user: beepsort
|
||||
remote_key: ${{secrets.SSH_PRIVATE_KEY}}
|
||||
remote_port: 6969
|
32
.gitignore
vendored
|
@ -1,14 +1,24 @@
|
|||
# Generated by Cargo
|
||||
# will have compiled files and executables
|
||||
debug/
|
||||
target/
|
||||
# build output
|
||||
dist/
|
||||
# generated types
|
||||
.astro/
|
||||
|
||||
# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries
|
||||
# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html
|
||||
Cargo.lock
|
||||
# dependencies
|
||||
node_modules/
|
||||
|
||||
# These are backup files generated by rustfmt
|
||||
**/*.rs.bk
|
||||
# logs
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
|
||||
# MSVC Windows builds of rustc generate these, which store debugging information
|
||||
*.pdb
|
||||
|
||||
# environment variables
|
||||
.env
|
||||
.env.production
|
||||
|
||||
# macOS-specific files
|
||||
.DS_Store
|
||||
|
||||
# jetbrains setting folder
|
||||
.idea/
|
||||
|
|
4
.vscode/extensions.json
vendored
Normal file
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"recommendations": ["astro-build.astro-vscode", "unifiedjs.vscode-mdx"],
|
||||
"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"
|
||||
}
|
||||
]
|
||||
}
|
|
@ -1,7 +0,0 @@
|
|||
# I made my own Kubernetes cluster!
|
||||
|
||||
I have for a looong time wanted to have my own kubernetes cluster for my homelab. Just because it really sounds fun and learning a new skill is something i love!
|
||||
|
||||
You wanna know something cool? This very website is actually hosted on this actual cluster!
|
||||
|
||||
Here it is in action!
|
|
@ -1,7 +0,0 @@
|
|||
# I made my own Kubernetes cluster!
|
||||
|
||||
I have for a looong time wanted to have my own kubernetes cluster for my homelab. Just because it really sounds fun and learning a new skill is something i love!
|
||||
|
||||
You wanna know something cool? This very website is actually hosted on this actual cluster!
|
||||
|
||||
Here it is in action!
|
12
Cargo.toml
|
@ -1,12 +0,0 @@
|
|||
[package]
|
||||
name = "polsevev_dev_backend"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
poem = "3.0.1"
|
||||
poem-openapi = { version = "5.0.2", features = ["swagger-ui", "static-files"] }
|
||||
pulldown-cmark = "0.11.0"
|
||||
tokio = { version = "1", features = ["macros", "rt-multi-thread"] }
|
24
LICENSE
Normal file
|
@ -0,0 +1,24 @@
|
|||
This is free and unencumbered software released into the public domain.
|
||||
|
||||
Anyone is free to copy, modify, publish, use, compile, sell, or
|
||||
distribute this software, either in source code form or as a compiled
|
||||
binary, for any purpose, commercial or non-commercial, and by any
|
||||
means.
|
||||
|
||||
In jurisdictions that recognize copyright laws, the author or authors
|
||||
of this software dedicate any and all copyright interest in the
|
||||
software to the public domain. We make this dedication for the benefit
|
||||
of the public at large and to the detriment of our heirs and
|
||||
successors. We intend this dedication to be an overt act of
|
||||
relinquishment in perpetuity of all present and future rights to this
|
||||
software under copyright law.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||
IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
|
||||
OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
|
||||
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
||||
OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
For more information, please refer to <https://unlicense.org>
|
|
@ -1 +1,5 @@
|
|||
# HELLLO!
|
||||
# Welcome!
|
||||
|
||||
This is a repo for my website [polsevev.dev](polsevev.dev)
|
||||
|
||||
You are welcome to look at, and use, the code. If you want submit a PR you can, but i prolly won't accept it, as this is just a place for mye to publish things on the interwebs while avoiding using social media.
|
||||
|
|
10
astro.config.mjs
Normal file
|
@ -0,0 +1,10 @@
|
|||
import { defineConfig } from 'astro/config';
|
||||
import mdx from '@astrojs/mdx';
|
||||
|
||||
import sitemap from '@astrojs/sitemap';
|
||||
|
||||
// https://astro.build/config
|
||||
export default defineConfig({
|
||||
site: 'https://polsevev.dev',
|
||||
integrations: [mdx(), sitemap()],
|
||||
});
|
|
@ -1,14 +0,0 @@
|
|||
# Use postgres/example user/password credentials
|
||||
version: '3.1'
|
||||
|
||||
services:
|
||||
|
||||
db:
|
||||
image: postgres
|
||||
restart: always
|
||||
environment:
|
||||
POSTGRES_PASSWORD: localdb
|
||||
PGDATA: /data/postgres
|
||||
ports:
|
||||
- 5432:5432
|
||||
|
6981
package-lock.json
generated
Normal file
21
package.json
Normal file
|
@ -0,0 +1,21 @@
|
|||
{
|
||||
"name": "polsevev-dev",
|
||||
"type": "module",
|
||||
"version": "0.0.1",
|
||||
"scripts": {
|
||||
"dev": "astro dev",
|
||||
"start": "astro dev",
|
||||
"build": "astro check && astro build",
|
||||
"preview": "astro preview",
|
||||
"astro": "astro"
|
||||
},
|
||||
"dependencies": {
|
||||
"@astrojs/check": "^0.9.4",
|
||||
"@astrojs/mdx": "^3.1.8",
|
||||
"@astrojs/rss": "^4.0.9",
|
||||
"@astrojs/sitemap": "^3.2.1",
|
||||
"astro": "^4.16.7",
|
||||
"sharp": "^0.33.5",
|
||||
"typescript": "^5.5.4"
|
||||
}
|
||||
}
|
BIN
public/fonts/atkinson-bold.woff
Normal file
BIN
public/fonts/atkinson-regular.woff
Normal file
BIN
public/images/self.png
Normal file
After Width: | Height: | Size: 79 KiB |
40
public/svg/Forgejo_logo.svg
Normal file
|
@ -0,0 +1,40 @@
|
|||
<svg viewBox="0 0 212 212" xmlns="http://www.w3.org/2000/svg">
|
||||
<metadata
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
>
|
||||
<rdf:RDF>
|
||||
<cc:Work rdf:about="https://codeberg.org/forgejo/meta/src/branch/readme/branding#logo">
|
||||
<dc:title>Forgejo logo</dc:title>
|
||||
<cc:creator rdf:resource="https://caesarschinas.com/"><cc:attributionName>Caesar Schinas</cc:attributionName></cc:creator>
|
||||
<cc:license rdf:resource="http://creativecommons.org/licenses/by-sa/4.0/" />
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<style type="text/css">
|
||||
circle {
|
||||
fill: none;
|
||||
stroke: #000;
|
||||
stroke-width: 15;
|
||||
}
|
||||
path {
|
||||
fill: none;
|
||||
stroke: #000;
|
||||
stroke-width: 25;
|
||||
}
|
||||
.orange {
|
||||
stroke:#ff6600;
|
||||
}
|
||||
.red {
|
||||
stroke:#d40000;
|
||||
}
|
||||
</style>
|
||||
<g transform="translate(6,6)">
|
||||
<path d="M58 168 v-98 a50 50 0 0 1 50-50 h20" class="orange" />
|
||||
<path d="M58 168 v-30 a50 50 0 0 1 50-50 h20" class="red" />
|
||||
<circle cx="142" cy="20" r="18" class="orange" />
|
||||
<circle cx="142" cy="88" r="18" class="red" />
|
||||
<circle cx="58" cy="180" r="18" class="red" />
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 1.2 KiB |
49
src/components/BaseHead.astro
Normal file
|
@ -0,0 +1,49 @@
|
|||
---
|
||||
// Import the global.css file here so that it is included on
|
||||
// all pages through the use of the <BaseHead /> component.
|
||||
import '../styles/global.css';
|
||||
|
||||
interface Props {
|
||||
title: string;
|
||||
description?: string;
|
||||
image?: string;
|
||||
}
|
||||
|
||||
const canonicalURL = new URL(Astro.url.pathname, Astro.site);
|
||||
|
||||
const { title, description, image = '/self.png' } = Astro.props;
|
||||
---
|
||||
|
||||
<!-- Global Metadata -->
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
||||
<link rel="icon" type="image/svg+xml" href="/self.png" />
|
||||
<meta name="generator" content={Astro.generator} />
|
||||
|
||||
<!-- Font preloads -->
|
||||
<link rel="preload" href="/fonts/atkinson-regular.woff" as="font" type="font/woff" crossorigin />
|
||||
<link rel="preload" href="/fonts/atkinson-bold.woff" as="font" type="font/woff" crossorigin />
|
||||
|
||||
<!-- Canonical URL -->
|
||||
<link rel="canonical" href={canonicalURL} />
|
||||
|
||||
<!-- Primary Meta Tags -->
|
||||
<title>{title}</title>
|
||||
<meta name="title" content={title} />
|
||||
<meta name="description" content={description} />
|
||||
|
||||
<!-- Open Graph / Facebook -->
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="og:url" content={Astro.url} />
|
||||
<meta property="og:title" content={title} />
|
||||
<meta property="og:description" content={description} />
|
||||
<meta property="og:image" content={new URL(image, Astro.url)} />
|
||||
|
||||
<!-- Twitter -->
|
||||
<meta property="twitter:card" content="summary_large_image" />
|
||||
<meta property="twitter:url" content={Astro.url} />
|
||||
<meta property="twitter:title" content={title} />
|
||||
<meta property="twitter:description" content={description} />
|
||||
<meta property="twitter:image" content={new URL(image, Astro.url)} />
|
||||
|
||||
<script defer src="https://umami.polsevev.dev/script.js" data-website-id="84ead158-3295-4160-8b05-48d56cf7337b"></script>
|
47
src/components/Footer.astro
Normal file
|
@ -0,0 +1,47 @@
|
|||
---
|
||||
const today = new Date();
|
||||
---
|
||||
|
||||
<footer>
|
||||
© {today.getFullYear()} Rolf Martin Glomsrud. All rights reserved.
|
||||
<div class="social-links">
|
||||
<a href="https://code.polsevev.dev" target="_blank" >
|
||||
<span class="sr-only">My Github?</span>
|
||||
<img src="/svg/Forgejo_logo.svg"/>
|
||||
<svg viewBox="0 0 16 16" aria-hidden="true" width="32" height="32" astro-icon="social/github" ></svg>
|
||||
</a>
|
||||
</a>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<style>
|
||||
footer {
|
||||
padding: 1em 1em 1em 1em;
|
||||
color: white;
|
||||
background-color: rgb(198, 60, 81);
|
||||
text-align: center;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
margin-top: 2rem;
|
||||
margin-bottom: 2rem;
|
||||
border-radius: 10px;
|
||||
width: 90%;
|
||||
|
||||
max-width: 1000px;
|
||||
}
|
||||
.social-links {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
.social-links a {
|
||||
text-decoration: none;
|
||||
color: rgb(var(--gray));
|
||||
}
|
||||
.social-links a:hover {
|
||||
color: rgb(var(--gray-dark));
|
||||
}
|
||||
img{
|
||||
background-color: white;
|
||||
padding: 5px;
|
||||
}
|
||||
</style>
|
17
src/components/FormattedDate.astro
Normal file
|
@ -0,0 +1,17 @@
|
|||
---
|
||||
interface Props {
|
||||
date: Date;
|
||||
}
|
||||
|
||||
const { date } = Astro.props;
|
||||
---
|
||||
|
||||
<time datetime={date.toISOString()}>
|
||||
{
|
||||
date.toLocaleDateString('en-us', {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
})
|
||||
}
|
||||
</time>
|
78
src/components/Header.astro
Normal file
|
@ -0,0 +1,78 @@
|
|||
---
|
||||
import HeaderLink from './HeaderLink.astro';
|
||||
import { SITE_TITLE } from '../consts';
|
||||
---
|
||||
|
||||
<header>
|
||||
<nav>
|
||||
<h2><a href="/">{SITE_TITLE}</a></h2>
|
||||
<div class="internal-links">
|
||||
<HeaderLink href="/">~/</HeaderLink>
|
||||
<HeaderLink href="/blog">~/blog</HeaderLink>
|
||||
<HeaderLink href="/about">~/about</HeaderLink>
|
||||
</div>
|
||||
<div class="social-links">
|
||||
<a href="https://code.polsevev.dev" target="_blank" >
|
||||
<span class="sr-only">My Github?</span>
|
||||
<img src="/svg/Forgejo_logo.svg"/>
|
||||
<svg viewBox="0 0 16 16" aria-hidden="true" width="32" height="32" astro-icon="social/github" ></svg>
|
||||
</a>
|
||||
</div>
|
||||
</nav>
|
||||
</header>
|
||||
<style>
|
||||
header {
|
||||
padding: 0.5em 0.5em 0.5em 0.5em;
|
||||
background-color: rgb(198, 60, 81);
|
||||
box-shadow: 0 2px 8px rgba(var(--black), 5%);
|
||||
border-radius: 10px;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
margin-top: 2rem;
|
||||
margin-bottom: 2rem;
|
||||
color: white;
|
||||
width: 90%;
|
||||
max-width: 1000px;
|
||||
}
|
||||
h2 {
|
||||
margin: 0;
|
||||
font-size: 1em;
|
||||
color: white;
|
||||
}
|
||||
|
||||
h2 a,
|
||||
h2 a.active {
|
||||
text-decoration: none;
|
||||
}
|
||||
nav {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
nav a {
|
||||
padding: 0.5em 0.5em;
|
||||
color: var(--black);
|
||||
border-bottom: 4px solid transparent;
|
||||
text-decoration: none;
|
||||
}
|
||||
img{
|
||||
background-color: white;
|
||||
padding: 5px;
|
||||
}
|
||||
nav a.active {
|
||||
text-decoration: none;
|
||||
border-bottom-color: cyan;
|
||||
}
|
||||
.social-links,
|
||||
.social-links a {
|
||||
display: flex;
|
||||
}
|
||||
@media (max-width: 720px) {
|
||||
.social-links {
|
||||
display: none;
|
||||
}
|
||||
nav a {
|
||||
padding: 0.2em 0.2em;
|
||||
}
|
||||
}
|
||||
</style>
|
26
src/components/HeaderLink.astro
Normal file
|
@ -0,0 +1,26 @@
|
|||
---
|
||||
import type { HTMLAttributes } from 'astro/types';
|
||||
|
||||
type Props = HTMLAttributes<'a'>;
|
||||
|
||||
const { href, class: className, ...props } = Astro.props;
|
||||
|
||||
const { pathname } = Astro.url;
|
||||
const subpath = pathname.match(/[^\/]+/g);
|
||||
const isActive = href === pathname || href === '/' + subpath?.[0];
|
||||
---
|
||||
|
||||
<a href={href} class:list={[className, { active: isActive }]} {...props}>
|
||||
<slot />
|
||||
</a>
|
||||
<style>
|
||||
a {
|
||||
display: inline-block;
|
||||
text-decoration: none;
|
||||
|
||||
}
|
||||
a.active {
|
||||
font-weight: bolder;
|
||||
text-decoreation: underline;
|
||||
}
|
||||
</style>
|
5
src/consts.ts
Normal file
|
@ -0,0 +1,5 @@
|
|||
// Place any global data in this file.
|
||||
// You can import this data from anywhere in your site by using the `import` keyword.
|
||||
|
||||
export const SITE_TITLE = 'polsevev.dev';
|
||||
export const SITE_DESCRIPTION = 'I add stuff here sometimes';
|
BIN
src/content/blog/images/keyboard.jpg
Normal file
After Width: | Height: | Size: 2.6 MiB |
11
src/content/blog/split_keyboard.md
Normal file
|
@ -0,0 +1,11 @@
|
|||
---
|
||||
title: I got a split keyboard!
|
||||
description: I am now a superior developer
|
||||
pubDate: 1.30.2025
|
||||
---
|
||||
|
||||
As can be read in the title, i finally caved and bought one. I am currently writing this post using it and it is better than i expected.
|
||||
|
||||
I will do a full writeup of my experience learning a split keyboard sometime in the future, just wanted to share the excitement!
|
||||
|
||||

|
34
src/content/config.ts
Normal file
|
@ -0,0 +1,34 @@
|
|||
import { defineCollection, z } from 'astro:content';
|
||||
|
||||
const homelab = defineCollection({
|
||||
type: 'content',
|
||||
schema: z.object({
|
||||
title: z.string(),
|
||||
description: z.string(),
|
||||
// Transform string to Date object
|
||||
pubDate: z.coerce.date(),
|
||||
updatedDate: z.coerce.date().optional(),
|
||||
})
|
||||
})
|
||||
|
||||
const servers = defineCollection({
|
||||
type: 'content',
|
||||
schema: z.object({
|
||||
title: z.string(),
|
||||
lastUpdated: z.coerce.date()
|
||||
})
|
||||
})
|
||||
|
||||
const blog = defineCollection({
|
||||
type: 'content',
|
||||
// Type-check frontmatter using a schema
|
||||
schema: z.object({
|
||||
title: z.string(),
|
||||
description: z.string(),
|
||||
// Transform string to Date object
|
||||
pubDate: z.coerce.date(),
|
||||
updatedDate: z.coerce.date().optional(),
|
||||
}),
|
||||
});
|
||||
|
||||
export const collections = { blog, homelab, servers };
|
28
src/content/homelab/homelab_oct_24.md
Normal file
|
@ -0,0 +1,28 @@
|
|||
---
|
||||
title: Homelab update
|
||||
description: I did some major renovations to my homelab this week
|
||||
pubDate: 10.19.2024
|
||||
---
|
||||
|
||||
## My homelab used to be on the floor
|
||||
|
||||
Yeah, you read that right, no rack here. While i don't have any pictures, because i actually was ashamed of it, my homelab used to be on the floor under my desk. With a ratsnest of wires behind all the "servers", and the heat eminating from it mimicking the scorching Norwegian summer (so not that hot lol). On a random wednesday i decided to do something about this.
|
||||
|
||||
### Well, what?
|
||||
|
||||
As i have seen time and time again on [r/homelab](https://reddit.com/r/homelab), people use proper server racks. However, i live in a 32 m² appartment (more an overgrown hotel room), so finding a small but still usable rack turned out to be a challenge. I did some research and looked at the options availible to me. Turns out getting a proper rack would be quite expensive where i live. This is for several reasons, i don't have easy access to a large van, so it had to be shipped, meaning i can't go for something used, I don't have any proper rack mount servers and getting shelves for a rack is also not cheap. Therefore, after browsing reddit for a bit, i decided to go with a Ikea [Ekenabben](https://www.ikea.com/no/no/p/ekenabben-apen-hylle-asp-hvit-10487816/).
|
||||
|
||||
### The pain, but in the end, glory!
|
||||
|
||||
I fucking hate cable managing. There is no fun in it in my opinion, it takes ages and makes it difficult to make changes. But the end result is so satisfying. I think the picture speaks for itself, cables are routed behind the legs of the rack (where applicable) and the power adapters for the mini pcs are hidden below the bottom shelf. I love this.
|
||||

|
||||
|
||||
To give a super quick rundown. At the top i have my 3d printer and free space for random things (see the raspberry pi). On the shelf below there is a wireless keyboard and mouse (for debugging broken machines), a label maker (god i love that thing), 5 mini pcs used as proxmox hosts, a 24 port unmanaged gigabit switch (behind the pcs) as well as a wifi access point. On the bottom shelf i have the two most powerful proxmox hosts, and free space for future servers (i have a problem).
|
||||
|
||||
If you wish to read in a bit more detail about my homelab, check out [/homelab](/homelab)
|
||||
|
||||
### Fin
|
||||
|
||||
I just wanted to share the excitement i have now, as my homelab always used to be a mess. So i hope i can finally maintain it this time (probably not xD). Thanks for reading!
|
||||
|
||||

|
BIN
src/content/homelab/images/borat-borat-very-nice.gif
Normal file
After Width: | Height: | Size: 7.9 MiB |
BIN
src/content/homelab/images/cable_management.jpg
Normal file
After Width: | Height: | Size: 226 KiB |
BIN
src/content/homelab/images/homelab_revamp.jpg
Normal file
After Width: | Height: | Size: 2.8 MiB |
BIN
src/content/homelab/images/i-have-homelab.webp
Normal file
After Width: | Height: | Size: 28 KiB |
140
src/content/homelab/terraform.md
Normal file
|
@ -0,0 +1,140 @@
|
|||
---
|
||||
title: Terraform + Proxmox = <3
|
||||
description: Using Terraform to deploy Virtual Machines
|
||||
pubDate: 09.29.2024
|
||||
---
|
||||
|
||||
## I am tired of UIs
|
||||
|
||||
Up until now, every time i wanted to deploy a VM in my homelab i did it with the Proxmox GUI. Don't get me wrong, the GUI is nice, but i would like to not have to repeat the same mundane task every time i want a new VM.
|
||||
There is an argument to be made that i probably shouldn't run that many VMs, and that for the number of VMs i need, this isn't worth it. However, being able to deploy a VM by just changing a few variables in a file and running `terraform apply` just tickles something in my nerd brain.
|
||||
|
||||
I also really like the repeatability of this, as i use these VM definitions to deploy K3S hosts, docker hosts and others where i want a "default" setup.
|
||||
|
||||
## Initial requirements
|
||||
|
||||
To use Terraform with Proxmox we use a privoder created by [Telmate](https://github.com/Telmate/terraform-provider-proxmox). We create the initial `provider.tf` file as so:
|
||||
```json
|
||||
terraform {
|
||||
required_providers {
|
||||
proxmox = {
|
||||
source = "telmate/proxmox"
|
||||
version = "3.0.1-rc3"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
variable "proxmox_api_url" {
|
||||
type = string
|
||||
}
|
||||
|
||||
variable "proxmox_user" {
|
||||
type = string
|
||||
sensitive = true
|
||||
}
|
||||
|
||||
variable "proxmox_password" {
|
||||
type = string
|
||||
sensitive = true
|
||||
}
|
||||
|
||||
variable "ssh_public_key" {
|
||||
type = string
|
||||
}
|
||||
|
||||
provider "proxmox"{
|
||||
pm_api_url = var.proxmox_api_url
|
||||
pm_user = var.proxmox_user
|
||||
pm_password = var.proxmox_password
|
||||
pm_tls_insecure = true
|
||||
pm_otp = ""
|
||||
}
|
||||
```
|
||||
|
||||
I have stored all secrets to access the proxmox API in custom variables located in a `.auto.tfvars` file that i do not track in git. Those variables are defined in the provider such that if they are not present Terraform will complain.
|
||||
|
||||
In this step i had some trouble, as you can see i use a release candidate version. There seems to be a bug in version `2.9.3`, and instead of trying to track it down i just switched to release candidate.
|
||||
|
||||
## Defining the virtual machine
|
||||
|
||||
Now as i mentioned previously, i already have templates created in my proxmox cluster (i made these with Ansible). Therefore i can use these as a base for the provisioning of a new VM.
|
||||
|
||||
Here is an example definition of a VM
|
||||
|
||||
```terraform
|
||||
resource "proxmox_vm_qemu" "havneboks" {
|
||||
name = "havneboks"
|
||||
desc = "Docker master"
|
||||
target_node = "poseidon"
|
||||
|
||||
agent = 1
|
||||
onboot = true
|
||||
|
||||
clone = "VM 9001"
|
||||
cores = 4
|
||||
sockets = 1
|
||||
cpu = "host"
|
||||
memory = 3096
|
||||
|
||||
# Setup the disk
|
||||
disks {
|
||||
ide {
|
||||
ide2 {
|
||||
cloudinit {
|
||||
storage = "basseng"
|
||||
}
|
||||
}
|
||||
}
|
||||
scsi {
|
||||
scsi0 {
|
||||
disk {
|
||||
size = "10G"
|
||||
storage = "basseng"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
network {
|
||||
bridge = "vmbr0"
|
||||
model = "virtio"
|
||||
}
|
||||
scsihw = "virtio-scsi-pci"
|
||||
os_type = "cloud-init"
|
||||
ipconfig0 = "ip=192.168.1.51/24,gw=192.168.1.1"
|
||||
nameserver = "192.168.1.69"
|
||||
ciuser = "ansible"
|
||||
sshkeys = var.ssh_public_key
|
||||
}
|
||||
```
|
||||
|
||||
A VM is defined using the resource type of `proxmox_vm_qemu` with a name. I really like to use the names of the service just translated to norwegian, so in this case, this VM is called `havneboks` (meaning docker box).
|
||||
|
||||
- name: The name of the VM (hostname)
|
||||
- desc: A description of the VM
|
||||
- target_node: Which node in the cluster should the VM be provisioned to, in this case i provision it to the node `poseidon`(hostname)
|
||||
- agent: Just select 1
|
||||
- onboot: Set the VM to start when the host boots
|
||||
- clone: Which template to clone the VM from
|
||||
- cores: How much horsepowa u want?
|
||||
- sockets: I only got 1 cpu in each of my boxes
|
||||
- memory: How much RAM u want?
|
||||
|
||||
Now, a really important part of this is the disk setup, as you have to mimic the setup of the template (sizes can be chosen freely). So in my case, i have the cloud-init disk on `ide2` and the OS disk on `scsi0` in the template. Therefore we create the same exact setup for this resource.
|
||||
|
||||
- network: Just mimic the cloud-init
|
||||
- scsihw: Which hardware do you want the host system to use to provide scsi?
|
||||
- os_type: I use cloud-init, so we select cloud-init
|
||||
- ipconfig: Now this is quite interesting, you should set a static IP and gateway such that the VM starts with a proper IP adress (you can also use DHCP here)
|
||||
- namesever: I use a custom dns on adress `.69`(nice) so i set that, but if this is not set it will use "same as host"
|
||||
- ciuser: A user to be created by cloud-init for this VM
|
||||
- sshkeys: Initial SSH public keys to allow access. This is stored in a variable in my case.
|
||||
|
||||
## Just run?
|
||||
|
||||
Bing bang bom. You can now deploy your VM fully automatically and about 43 seconds later access it via SSH. Now all i did was configure it with ansible, and i have a fully reproducible homelab setup.
|
||||
|
||||
If you want to have a look at my homelab infrastructure as code repo, you can find it at [polsevev/homelab](https://github.com/polsevev/homelab)
|
||||
|
||||

|
44
src/content/homelab/woodpecker.md
Normal file
|
@ -0,0 +1,44 @@
|
|||
---
|
||||
title: Woodpecker as CI
|
||||
description: Setting up my own CI with Woodpecker!
|
||||
pubDate: 08.03.2024
|
||||
---
|
||||
|
||||
## Self hosted CI/CD!
|
||||
|
||||
Yesterday, i decided that i was tired of using Github Actions to deploy my code from Github. So i decided to self host my own CI/CD. This is mostly because i am not comfortable having port 22 on my web server be publicly accessible on the internet.
|
||||
|
||||
To do this, i went down quite the rabbit hole. First i looked at [TeamCity](https://www.jetbrains.com/teamcity/) from JetBrains as this is what i use at work. It looked promising, however i decided it was too complex and had too many features i did not really care about.
|
||||
|
||||
Then i stumbled across [Drone](https://www.drone.io/), which seemed to fit my use-case. However it has since gotten very corporate when it was bought up, so i decided to go with the open source fork called [Woodpecker](https://woodpecker-ci.org/)
|
||||
|
||||
This turned out to be a bit of a bigger challenge than i first thought, but i somehow got it up and running in the end. Follow along!
|
||||
|
||||
### Installing
|
||||
|
||||
This was by far the easiest. As i have not gotten around to setting up my Kubernetes cluster yet, and i wanted to start of simple. I just used the docker compose provided by the Woodpecker team.
|
||||
|
||||
To provide integration with Github, i use a github Oauth2 App i made in the github UI, this is very well explained in the documentation of Woodpecker.
|
||||
|
||||
Now that i have configured the docker compose, i created a VM on [cronus](/homelab/servers/oceanus) and deployed the compose file using Ansible.
|
||||
|
||||
### Setting up a pipeline
|
||||
|
||||
Writing the pipeline was simple enough, as it has a very similar syntax to Github actions. However, i needed a docker image for the pipeline to run in. This actually turned out to be a challenge, as i currently do not have a docker registry in my homelab (i will be setting up Gitea in the future dw).
|
||||
|
||||
To solve the docker registry problem, i decided to simply build the image locally on the server. Ansible comes to the rescue again. I wrote a simple playbook that builds the docker image on the VM such that it is available to Woodpecker. Missing registry problem solved!
|
||||
|
||||
Now finally, i could make my own image that had all the dependencies for this website!
|
||||
|
||||
We were not out of the woods yet however, i still had to figure out how i wanted to transfer the files to the server they will be hosted on. Previously i had used rsync to transfer them into a directory on my web server, and i decided to do something similar this time.
|
||||
|
||||
However, since i did not want to put the SSH keys into the docker image.
|
||||
So i ended up adding a private ssh key into the secret store of Woodpecker. This allows rsync to communicate with my web server.
|
||||
|
||||
This meant i have to distribute the public key to the web server beforehand, ansible saved the day again.
|
||||
|
||||
### Pipeline finished
|
||||
|
||||
Finally, i could deploy this very website, using woodpecker to my own web server, all without exposing port 22 on the web server to the internet.
|
||||
|
||||

|
29
src/content/servers/Oceanus.md
Normal file
|
@ -0,0 +1,29 @@
|
|||
---
|
||||
title: 'Oceanus'
|
||||
lastUpdated: '03.08.2024'
|
||||
---
|
||||
|
||||
## What is it?
|
||||
|
||||
Oceanus is my main VM server. It used to be an office PC, but has since been re-purposed as a tiny home server.
|
||||
|
||||
## Specs
|
||||
|
||||
- MODEL: HP Elitedesk 600 G3
|
||||
- CPU: i7 6700
|
||||
- RAM: 32 GB DDR4
|
||||
- Storage:
|
||||
- 256 GB NVME SSD
|
||||
- 128 GB SATA SSD
|
||||
|
||||
## What is on it?
|
||||
|
||||
Currently this server has quite a bit of purpose, but i will probably move most of the stateless applications from this into my K3S cluster.
|
||||
|
||||
- Opnsense
|
||||
- This server currently runs my firewall, which i have behind the ISP router using DMZ to not have double NAT (even though is technically still is double NAT)
|
||||
- Game servers
|
||||
- I run Factorio and Feed the Beast servers on this machine
|
||||
|
||||
|
||||

|
27
src/content/servers/ares.md
Normal file
|
@ -0,0 +1,27 @@
|
|||
---
|
||||
title: 'Ares'
|
||||
lastUpdated: '03.08.2024'
|
||||
---
|
||||
## What is it?
|
||||
|
||||
Ares is a node in my cluster of 5 mini PCs. It follows the exact same configuration as the other nodes for consistency. It used to be an office PC, but has since been re-purposed as a tiny home server.
|
||||
|
||||
## Specs
|
||||
|
||||
- MODEL: HP Elitedesk 705 G3 mini
|
||||
- CPU: AMD A10-8770E
|
||||
- RAM: 16 GB DDR4
|
||||
- Storage
|
||||
- 256 GB NVME SSD
|
||||
- 128 GB SATA SSD
|
||||
|
||||
## What is on it?
|
||||
|
||||
This server is running proxmox at the moment, to allow for virtualization of all my services. Currently this server has 2 VMS.
|
||||
|
||||
- k3s_master
|
||||
- Master in my K3s cluster
|
||||
- k3s_worker
|
||||
- Worker in my K3S cluster
|
||||
|
||||

|
25
src/content/servers/cronus.md
Normal file
|
@ -0,0 +1,25 @@
|
|||
---
|
||||
title: 'Cronus'
|
||||
lastUpdated: '03.08.2024'
|
||||
---
|
||||
|
||||
## What is it?
|
||||
|
||||
Cronus was my first server, and i built it myself. This was my entry into homelabing and i am so thankful i spent the money on it, even though i was a struggling student.
|
||||
|
||||
## Specs
|
||||
|
||||
- CPU: 1x Xeon E5 2678 V3 12 core
|
||||
- RAM: 24 GB DDR4
|
||||
- MOTHERBOARD: MSI X99S SLI-PLUS
|
||||
- Storage
|
||||
- 7x 4 TB Ironwolf HDD
|
||||
- 1x 1 TB NVME SSD
|
||||
- GPU: Nvidia GTX 1050 ti
|
||||
## What is on it?
|
||||
|
||||
This server is running [Unraid](https://unraid.net). I made this choice because i was new to servers when i built it, and wanted a simple entry. Turns out that was quite a wise choice, as it allowed me to build my skills over time without making it super difficult in the beginning.
|
||||
|
||||
Currently this server mostly only contains files and serves its purpose as a NAS. I do some video transcoding with ffmpeg on it as well to minimize the space my videos take. This is actually a breeze because of the GPU
|
||||
|
||||

|
25
src/content/servers/hades.md
Normal file
|
@ -0,0 +1,25 @@
|
|||
---
|
||||
title: 'Hades'
|
||||
lastUpdated: '03.08.2024'
|
||||
---
|
||||
## What is it?
|
||||
|
||||
Hades is a node in my cluster of 5 mini PCs. It follows the exact same configuration as the other nodes for consistency. It used to be an office PC, but has since been re-purposed as a tiny home server.
|
||||
## Specs
|
||||
- MODEL: HP Elitedesk 705 G3 mini
|
||||
- CPU: AMD A10-8770E
|
||||
- RAM: 16 GB DDR4
|
||||
- Storage
|
||||
- 256 GB NVME SSD
|
||||
- 128 GB SATA SSD
|
||||
|
||||
## What is on it?
|
||||
|
||||
This server is running proxmox at the moment, to allow for virtualization of all my services. Currently this server has 2 VMS.
|
||||
|
||||
- k3s_master
|
||||
- Master in my K3s cluster
|
||||
- k3s_worker
|
||||
- Worker in my K3S cluster
|
||||
|
||||

|
24
src/content/servers/hermes.md
Normal file
|
@ -0,0 +1,24 @@
|
|||
---
|
||||
title: 'Hermes'
|
||||
lastUpdated: '03.08.2024'
|
||||
---
|
||||
## What is it?
|
||||
Hermes is a node in my cluster of 5 mini PCs. It follows the exact same configuration as the other nodes for consistency. It used to be an office PC, but has since been re-purposed as a tiny home server.
|
||||
## Specs
|
||||
- MODEL: HP Elitedesk 705 G3 mini
|
||||
- CPU: AMD A10-8770E
|
||||
- RAM: 16 GB DDR4
|
||||
- Storage
|
||||
- 256 GB NVME SSD
|
||||
- 128 GB SATA SSD
|
||||
|
||||
## What is on it?
|
||||
|
||||
This server is running proxmox at the moment, to allow for virtualization of all my services. Currently this server has 2 VMS.
|
||||
|
||||
- k3s_master
|
||||
- Master in my K3s cluster
|
||||
- k3s_worker
|
||||
- Worker in my K3S cluster
|
||||
|
||||

|
23
src/content/servers/hyperion.md
Normal file
|
@ -0,0 +1,23 @@
|
|||
---
|
||||
title: 'Hyperion'
|
||||
lastUpdated: '03.08.2024'
|
||||
---
|
||||
|
||||
## What is it?
|
||||
|
||||
Oceanus is my main VM server. It used to be an office PC, but has since been re-purposed as a tiny home server.
|
||||
|
||||
## Specs
|
||||
|
||||
- MODEL: HP Elitedesk 600 G3
|
||||
- CPU: i5 6500
|
||||
- RAM: 16 GB DDR4
|
||||
- Storage:
|
||||
- 256 GB NVME SSD
|
||||
- 128 GB SATA SSD
|
||||
|
||||
## What is on it?
|
||||
|
||||
This is one of the more powerful nodes in my Proxmox cluster. Mostly used for decent CPU performance tasks.
|
||||
|
||||

|
BIN
src/content/servers/images/cluster.jpg
Normal file
After Width: | Height: | Size: 2.2 MiB |
BIN
src/content/servers/images/cronus.png
Normal file
After Width: | Height: | Size: 936 KiB |
BIN
src/content/servers/images/elitedesk_800.jpg
Normal file
After Width: | Height: | Size: 2.5 MiB |
24
src/content/servers/poseidon.md
Normal file
|
@ -0,0 +1,24 @@
|
|||
---
|
||||
title: 'Poseidon'
|
||||
lastUpdated: '03.08.2024'
|
||||
---
|
||||
## What is it?
|
||||
Poseidon is a node in my cluster of 5 mini PCs. It follows the exact same configuration as the other nodes for consistency. It used to be an office PC, but has since been re-purposed as a tiny home server.
|
||||
## Specs
|
||||
- MODEL: HP Elitedesk 705 G3 mini
|
||||
- CPU: AMD A10-8770E
|
||||
- RAM: 16 GB DDR4
|
||||
- Storage
|
||||
- 256 GB NVME SSD
|
||||
- 128 GB SATA SSD
|
||||
|
||||
## What is on it?
|
||||
|
||||
This server is running proxmox at the moment, to allow for virtualization of all my services. Currently this server has 2 VMS.
|
||||
|
||||
- k3s_master
|
||||
- Master in my K3s cluster
|
||||
- k3s_worker
|
||||
- Worker in my K3S cluster
|
||||
|
||||

|
25
src/content/servers/zeus.md
Normal file
|
@ -0,0 +1,25 @@
|
|||
---
|
||||
title: 'Zeus'
|
||||
lastUpdated: '03.08.2024'
|
||||
---
|
||||
|
||||
## What is it?
|
||||
Zeus is a node in my cluster of 5 mini PCs. It follows the exact same configuration as the other nodes for consistency. It used to be an office PC, but has since been re-purposed as a tiny home server.
|
||||
## Specs
|
||||
- MODEL: HP Elitedesk 705 G3 mini
|
||||
- CPU: AMD A10-8770E
|
||||
- RAM: 16 GB DDR4
|
||||
- Storage
|
||||
- 256 GB NVME SSD
|
||||
- 128 GB SATA SSD
|
||||
|
||||
## What is on it?
|
||||
|
||||
This server is running proxmox at the moment, to allow for virtualization of all my services. Currently this server has 2 VMS.
|
||||
|
||||
- k3s_master
|
||||
- Master in my K3s cluster
|
||||
- k3s_worker
|
||||
- Worker in my K3S cluster
|
||||
|
||||

|
2
src/env.d.ts
vendored
Normal file
|
@ -0,0 +1,2 @@
|
|||
/// <reference path="../.astro/types.d.ts" />
|
||||
/// <reference types="astro/client" />
|
76
src/layouts/BlogPost.astro
Normal file
|
@ -0,0 +1,76 @@
|
|||
---
|
||||
import type { CollectionEntry } from 'astro:content';
|
||||
import BaseHead from '../components/BaseHead.astro';
|
||||
import Header from '../components/Header.astro';
|
||||
import Footer from '../components/Footer.astro';
|
||||
import FormattedDate from '../components/FormattedDate.astro';
|
||||
|
||||
type Props = CollectionEntry<'blog'>['data'];
|
||||
|
||||
const { title, description, pubDate, updatedDate} = Astro.props;
|
||||
---
|
||||
|
||||
<html lang="en">
|
||||
<head>
|
||||
<BaseHead title={title} description={description} />
|
||||
<style>
|
||||
main {
|
||||
width: calc(100% - 2em);
|
||||
max-width: 100%;
|
||||
margin: 0;
|
||||
}
|
||||
.prose {
|
||||
width: 720px;
|
||||
max-width: calc(100% - 2em);
|
||||
margin: auto;
|
||||
padding: 1em;
|
||||
color: cyan;
|
||||
}
|
||||
.title {
|
||||
margin-bottom: 1em;
|
||||
padding: 1em 0;
|
||||
text-align: center;
|
||||
line-height: 1;
|
||||
}
|
||||
.title h1 {
|
||||
margin: 0 0 0.5em 0;
|
||||
}
|
||||
.date {
|
||||
margin-bottom: 0.5em;
|
||||
color: rgb(var(--gray));
|
||||
}
|
||||
.last-updated-on {
|
||||
font-style: italic;
|
||||
}
|
||||
img{
|
||||
align-self: center;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<Header />
|
||||
<main>
|
||||
<article>
|
||||
<div class="prose">
|
||||
<div class="title">
|
||||
<div class="date">
|
||||
<FormattedDate date={pubDate} />
|
||||
{
|
||||
updatedDate && (
|
||||
<div class="last-updated-on">
|
||||
Last updated on <FormattedDate date={updatedDate} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
<h1>{title}</h1>
|
||||
<hr />
|
||||
</div>
|
||||
<slot />
|
||||
</div>
|
||||
</article>
|
||||
</main>
|
||||
<Footer />
|
||||
</body>
|
||||
</html>
|
67
src/layouts/ServerDescription.astro
Normal file
|
@ -0,0 +1,67 @@
|
|||
---
|
||||
import type { CollectionEntry } from 'astro:content';
|
||||
import BaseHead from '../components/BaseHead.astro';
|
||||
import Header from '../components/Header.astro';
|
||||
import Footer from '../components/Footer.astro';
|
||||
import FormattedDate from '../components/FormattedDate.astro';
|
||||
|
||||
type Props = CollectionEntry<'servers'>['data'];
|
||||
|
||||
const { title, lastUpdated} = Astro.props;
|
||||
---
|
||||
|
||||
<html lang="en">
|
||||
<head>
|
||||
<BaseHead title={title} />
|
||||
<style>
|
||||
main {
|
||||
width: calc(100% - 2em);
|
||||
max-width: 100%;
|
||||
margin: 0;
|
||||
}
|
||||
.prose {
|
||||
width: 720px;
|
||||
max-width: calc(100% - 2em);
|
||||
margin: auto;
|
||||
padding: 1em;
|
||||
color: cyan;
|
||||
}
|
||||
.title {
|
||||
margin-bottom: 1em;
|
||||
padding: 1em 0;
|
||||
text-align: center;
|
||||
line-height: 1;
|
||||
}
|
||||
.title h1 {
|
||||
margin: 0 0 0.5em 0;
|
||||
|
||||
}
|
||||
.date {
|
||||
margin-bottom: 0.5em;
|
||||
color: rgb(var(--gray));
|
||||
}
|
||||
img{
|
||||
width: 100px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<Header />
|
||||
<main>
|
||||
<article>
|
||||
<div class="prose">
|
||||
<div class="title">
|
||||
<div class="date">
|
||||
<FormattedDate date={lastUpdated} />
|
||||
</div>
|
||||
<h1>🖥️ {title}</h1>
|
||||
<hr />
|
||||
</div>
|
||||
<slot />
|
||||
</div>
|
||||
</article>
|
||||
</main>
|
||||
<Footer />
|
||||
</body>
|
||||
</html>
|
43
src/main.rs
|
@ -1,43 +0,0 @@
|
|||
mod prepare;
|
||||
use std::fs;
|
||||
|
||||
use poem::{endpoint::StaticFilesEndpoint, listener::TcpListener, Route};
|
||||
use poem_openapi::{param::Query, payload::Html, payload::PlainText, OpenApi, OpenApiService};
|
||||
use prepare::{prep, BlogPost};
|
||||
|
||||
struct Api {
|
||||
blogPosts: Vec<BlogPost>,
|
||||
}
|
||||
|
||||
#[OpenApi]
|
||||
impl Api {
|
||||
#[oai(path = "/posts", method = "get")]
|
||||
async fn posts(&self) -> Html<String> {
|
||||
Html(self.blogPosts.iter().fold(String::new(), |posts, post| {
|
||||
posts + "<li>" + &post.name + "</li>"
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), std::io::Error> {
|
||||
// Load files
|
||||
|
||||
let blog = prep("Blog".to_string());
|
||||
|
||||
let api_service = OpenApiService::new(Api { blogPosts: blog }, "Hello World", "1.0")
|
||||
.server("http://localhost:3000/api");
|
||||
|
||||
let ui = api_service.swagger_ui();
|
||||
let app = Route::new()
|
||||
.nest(
|
||||
"/",
|
||||
StaticFilesEndpoint::new("static").index_file("index.html"),
|
||||
)
|
||||
.nest("/api", api_service)
|
||||
.nest("/swagger", ui);
|
||||
|
||||
poem::Server::new(TcpListener::bind("127.0.0.1:3000"))
|
||||
.run(app)
|
||||
.await
|
||||
}
|
28
src/pages/about.astro
Normal file
|
@ -0,0 +1,28 @@
|
|||
---
|
||||
import Layout from "../layouts/BlogPost.astro";
|
||||
---
|
||||
|
||||
<Layout
|
||||
title="About me"
|
||||
description="Lorem ipsum dolor sit amet"
|
||||
pubDate={new Date("08.02.2024")}
|
||||
>
|
||||
<p>Hi!</p>
|
||||
<p>
|
||||
My name is Rolf, i am 2024-2000 years old and live in Norway.
|
||||
</p>
|
||||
<p>
|
||||
I currently work as a devops engineer / infrastructure guy for an insurance company. As you probably can tell from this website, front-end is not really my strong suit hehe.
|
||||
</p>
|
||||
<p>
|
||||
I have a strong love for all things self-hosted. Due to this, i currently run my own "little" homelab from my living room, i use this lab as a learning platform and just an all around
|
||||
way of tinkering with fun technologies. Perhaps i will introduce it on this page some day?
|
||||
</p>
|
||||
<p>
|
||||
I do love board games! Currently my favorite has to be Magic The Gathering, it eats up way too much of my time and salary, but i find the game so engaging and fun.
|
||||
</p>
|
||||
<p>
|
||||
I also spend a lot of my time as a member of the student organization <a href="https://fribyte.no">friByte</a>. Even though i am no longer a student, it is hard to quit an orginization with so many dedicated
|
||||
and nerdy members
|
||||
</p>
|
||||
</Layout>
|
23
src/pages/blog/[...slug].astro
Normal file
|
@ -0,0 +1,23 @@
|
|||
---
|
||||
import { type CollectionEntry, getCollection } from 'astro:content';
|
||||
import BlogPost from '../../layouts/BlogPost.astro';
|
||||
|
||||
export async function getStaticPaths() {
|
||||
const posts = await getCollection('blog');
|
||||
return posts.map((post) => ({
|
||||
params: { slug: post.slug },
|
||||
props: post,
|
||||
}));
|
||||
}
|
||||
type Props = CollectionEntry<'blog'>;
|
||||
|
||||
const post = Astro.props;
|
||||
const { Content } = await post.render();
|
||||
---
|
||||
|
||||
<BlogPost {...post.data}>
|
||||
<a href="/blog">
|
||||
<h5><--</h5>
|
||||
</a>
|
||||
<Content />
|
||||
</BlogPost>
|
134
src/pages/blog/index.astro
Normal file
|
@ -0,0 +1,134 @@
|
|||
---
|
||||
import BaseHead from '../../components/BaseHead.astro';
|
||||
import Header from '../../components/Header.astro';
|
||||
import Footer from '../../components/Footer.astro';
|
||||
import { SITE_TITLE, SITE_DESCRIPTION } from '../../consts';
|
||||
import { getCollection } from 'astro:content';
|
||||
import FormattedDate from '../../components/FormattedDate.astro';
|
||||
|
||||
const posts = (await getCollection('blog')).sort(
|
||||
(b, a) => a.data.pubDate.valueOf() - b.data.pubDate.valueOf()
|
||||
);
|
||||
const homelab_posts = (await getCollection('homelab')).sort(
|
||||
(b, a) => a.data.pubDate.valueOf() - b.data.pubDate.valueOf()
|
||||
);
|
||||
---
|
||||
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<BaseHead title={SITE_TITLE} description={SITE_DESCRIPTION} />
|
||||
<style>
|
||||
main {
|
||||
width: 960px;
|
||||
}
|
||||
ul {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 2rem;
|
||||
list-style-type: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
ul li {
|
||||
width: calc(50% - 1rem);
|
||||
}
|
||||
ul li * {
|
||||
text-decoration: none;
|
||||
transition: 0.2s ease;
|
||||
}
|
||||
ul li:first-child {
|
||||
width: 100%;
|
||||
margin-bottom: 1rem;
|
||||
text-align: center;
|
||||
}
|
||||
ul li:first-child img {
|
||||
width: 100%;
|
||||
}
|
||||
ul li:first-child .title {
|
||||
font-size: 2.369rem;
|
||||
}
|
||||
ul li img {
|
||||
margin-bottom: 0.5rem;
|
||||
border-radius: 12px;
|
||||
}
|
||||
ul li a {
|
||||
display: block;
|
||||
}
|
||||
.title {
|
||||
margin: 0;
|
||||
color: rgb(0,255,159);
|
||||
line-height: 1;
|
||||
}
|
||||
.date {
|
||||
margin: 0;
|
||||
color: rgb(white);
|
||||
}
|
||||
ul li a:hover h4,
|
||||
ul li a:hover .date {
|
||||
color: rgb(var(--accent));
|
||||
}
|
||||
ul a:hover img {
|
||||
box-shadow: var(--box-shadow);
|
||||
}
|
||||
@media (max-width: 720px) {
|
||||
ul {
|
||||
gap: 0.5em;
|
||||
}
|
||||
ul li {
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
}
|
||||
ul li:first-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
ul li:first-child .title {
|
||||
font-size: 1.563em;
|
||||
}
|
||||
}
|
||||
blogheader {
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<Header />
|
||||
<main>
|
||||
<blogheader>
|
||||
<h1>Blog</h1>
|
||||
<p>
|
||||
This page contains a blog about nothing and everything going on with me.
|
||||
I will probably post mostly random stuff or opinion pieces. What it will be about i have no idea.
|
||||
So stay tuned!
|
||||
</p>
|
||||
</blogheader>
|
||||
<section>
|
||||
<ul>
|
||||
{
|
||||
posts.map((post) => (
|
||||
<li>
|
||||
<a href={`/blog/${post.slug}/`}>
|
||||
<h4 class="title">{post.data.title}</h4>
|
||||
<p class="date">
|
||||
<FormattedDate date={post.data.pubDate} />
|
||||
</p>
|
||||
</a>
|
||||
</li>
|
||||
))
|
||||
}
|
||||
{ homelab_posts.map((post) => (
|
||||
<li>
|
||||
<a href={`/homelab/${post.slug}/`}>
|
||||
<h4 class="title">{post.data.title}</h4>
|
||||
<p class="date">
|
||||
<FormattedDate date={post.data.pubDate} />
|
||||
</p>
|
||||
</a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</section>
|
||||
</main>
|
||||
<Footer />
|
||||
</body>
|
||||
</html>
|
26
src/pages/homelab/[...slug].astro
Normal file
|
@ -0,0 +1,26 @@
|
|||
---
|
||||
import { type CollectionEntry, getCollection } from 'astro:content';
|
||||
import BlogPost from '../../layouts/BlogPost.astro';
|
||||
|
||||
|
||||
export async function getStaticPaths() {
|
||||
const posts = await getCollection('homelab');
|
||||
|
||||
return posts.map((post)=> ({
|
||||
params: { slug: post.slug },
|
||||
props: post,
|
||||
}));
|
||||
}
|
||||
|
||||
type Props = CollectionEntry<'homelab'>;
|
||||
|
||||
const post = Astro.props;
|
||||
const { Content } = await post.render();
|
||||
---
|
||||
|
||||
<BlogPost {...post.data}>
|
||||
<a href="/homelab">
|
||||
<h5><--</h5>
|
||||
</a>
|
||||
<Content />
|
||||
</BlogPost>
|
78
src/pages/homelab/index.astro
Normal file
|
@ -0,0 +1,78 @@
|
|||
---
|
||||
import { getCollection } from "astro:content";
|
||||
import BaseHead from "../../components/BaseHead.astro";
|
||||
import Footer from "../../components/Footer.astro";
|
||||
import FormattedDate from "../../components/FormattedDate.astro";
|
||||
import Header from "../../components/Header.astro";
|
||||
import { SITE_TITLE, SITE_DESCRIPTION } from "../../consts";
|
||||
|
||||
|
||||
|
||||
|
||||
const servers = (await getCollection('servers'));
|
||||
---
|
||||
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<BaseHead title={SITE_TITLE} description={SITE_DESCRIPTION} />
|
||||
<style>
|
||||
main {
|
||||
width: 960px;
|
||||
}
|
||||
|
||||
.title {
|
||||
margin: 0;
|
||||
color: rgb(0,255,159);
|
||||
line-height: 1;
|
||||
}
|
||||
.date {
|
||||
margin: 0;
|
||||
color: rgb(var(--gray));
|
||||
}
|
||||
|
||||
a {
|
||||
margin-top: 0.5em;
|
||||
margin-bottom: 0.5em;
|
||||
padding: 5px;
|
||||
}
|
||||
site_header{
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
box{
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<Header />
|
||||
<main>
|
||||
<site_header>
|
||||
<h2>What is in my homelab now?</h2>
|
||||
</site_header>
|
||||
<box>
|
||||
<p>
|
||||
So my current homelab has gotten quite extensive, which honestly is a pain because electricity is expensive as hell.
|
||||
</p>
|
||||
<p>I have waaaaay too many servers, and they are not at all used to their full capabilites. But i am a tinkerer at heart.
|
||||
Owning a proxmox cluster with 6 nodes, or a 5 node K3S cluster is just fun to me. It doesn't really matter that it costs money, it is a hobby. So without further ado,
|
||||
let me explain each server, and what it is currently being used for.
|
||||
</p>
|
||||
<p>As you can probably tell, i use greek mythology for the naming scheme of physical machines :P</p>
|
||||
<box>
|
||||
{
|
||||
servers.map((s) => (
|
||||
<a href={`/homelab/servers/${s.slug}/`}>
|
||||
<h5 class="title">🖥️ {s.data.title}</h5>
|
||||
</a>
|
||||
))
|
||||
}
|
||||
</box>
|
||||
</box>
|
||||
|
||||
</main>
|
||||
<Footer />
|
||||
</body>
|
||||
</html>
|
28
src/pages/homelab/servers/[...slug].astro
Normal file
|
@ -0,0 +1,28 @@
|
|||
---
|
||||
import { type CollectionEntry, getCollection } from 'astro:content';
|
||||
import BlogPost from '../../../layouts/BlogPost.astro';
|
||||
import ServerDescription from '../../../layouts/ServerDescription.astro';
|
||||
import type { ACTION_ERROR_CODES } from 'astro:actions';
|
||||
|
||||
|
||||
export async function getStaticPaths() {
|
||||
const servers = await getCollection('servers');
|
||||
|
||||
return servers.map((s)=> ({
|
||||
params: { slug: s.slug },
|
||||
props: s,
|
||||
}));
|
||||
}
|
||||
|
||||
type Props = CollectionEntry<'servers'>;
|
||||
|
||||
const servers = Astro.props;
|
||||
const { Content } = await servers.render();
|
||||
---
|
||||
|
||||
<ServerDescription {...servers.data}>
|
||||
<a href="/homelab">
|
||||
<h5><--</h5>
|
||||
</a>
|
||||
<Content />
|
||||
</BlogPost>
|
64
src/pages/index.astro
Normal file
|
@ -0,0 +1,64 @@
|
|||
---
|
||||
import BaseHead from "../components/BaseHead.astro";
|
||||
import Header from "../components/Header.astro";
|
||||
import Footer from "../components/Footer.astro";
|
||||
import { SITE_TITLE, SITE_DESCRIPTION } from "../consts";
|
||||
---
|
||||
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<BaseHead title={SITE_TITLE} description={SITE_DESCRIPTION} />
|
||||
<style>
|
||||
body {
|
||||
text-align: center;
|
||||
}
|
||||
main {
|
||||
}
|
||||
main left,
|
||||
right {
|
||||
margin: 0.2em;
|
||||
padding: 0.5em;
|
||||
}
|
||||
left {
|
||||
max-width: 600px;
|
||||
text-align: center;
|
||||
}
|
||||
right {
|
||||
max-width: 400px;
|
||||
text-align: center;
|
||||
}
|
||||
box {
|
||||
display: flex;
|
||||
}
|
||||
p{
|
||||
text-align: left;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<Header />
|
||||
|
||||
<main>
|
||||
<h1>Rolf Glomsrud</h1>
|
||||
<img src="images/self.png" />
|
||||
|
||||
<h3>Hello!</h3>
|
||||
<p>Welcome to my little corner of the interwebs, glad to have you!</p>
|
||||
|
||||
<p>This site is a little project of mine where is hope to share my adventures both on and off line.</p>
|
||||
|
||||
<p>Here are some useful links to my stuff:</p>
|
||||
<ul>
|
||||
<li>
|
||||
<p>I self host my code online using Forgejo! <a href="https://code.polsevev.dev">code.polsevev.dev</a></p>
|
||||
</li>
|
||||
<li>
|
||||
<p>I document my homelab! <a href="/homelab">/homelab</a></p>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
</main>
|
||||
<Footer />
|
||||
</body>
|
||||
</html>
|
20
src/pages/rss.xml.js
Normal file
|
@ -0,0 +1,20 @@
|
|||
import rss from '@astrojs/rss';
|
||||
import { getCollection } from 'astro:content';
|
||||
import { SITE_TITLE, SITE_DESCRIPTION } from '../consts';
|
||||
|
||||
export async function GET(context) {
|
||||
const posts = await getCollection('blog');
|
||||
const homelab = await getCollection('homelab');
|
||||
return rss({
|
||||
title: SITE_TITLE,
|
||||
description: SITE_DESCRIPTION,
|
||||
site: context.site,
|
||||
items: posts.map((post) => ({
|
||||
...post.data,
|
||||
link: `/blog/${post.slug}/`,
|
||||
})).concat(homelab.map((post) => ({
|
||||
...post.data,
|
||||
link: `/homelab/${post.slug}`
|
||||
}))),
|
||||
});
|
||||
}
|
|
@ -1,40 +0,0 @@
|
|||
use std::fs;
|
||||
|
||||
use pulldown_cmark::{Options, Parser};
|
||||
|
||||
pub fn prep(blogPath: String) -> Vec<BlogPost>{
|
||||
let blogPaths = fs::read_dir(&blogPath);
|
||||
|
||||
let parsed = match blogPaths {
|
||||
Ok(paths) => paths
|
||||
.into_iter()
|
||||
.map(|x| x.unwrap().path().to_str().unwrap().to_string())
|
||||
.collect::<Vec<String>>(),
|
||||
Err(_) => panic!("Could not load directory of blog"),
|
||||
};
|
||||
|
||||
dbg!(&parsed);
|
||||
|
||||
let mut options = Options::empty();
|
||||
|
||||
|
||||
let compiled_html = parsed.into_iter().map(|mdFilePath| {
|
||||
let fileContents = fs::read_to_string(&mdFilePath).unwrap();
|
||||
let name = mdFilePath.strip_prefix(&format!("{}/", &blogPath)).unwrap().to_string();
|
||||
let parser = Parser::new_ext(&fileContents, options);
|
||||
|
||||
let mut html = String::new();
|
||||
pulldown_cmark::html::push_html(&mut html, parser);
|
||||
BlogPost{
|
||||
name,
|
||||
html
|
||||
}
|
||||
}).collect::<Vec<_>>();
|
||||
|
||||
compiled_html
|
||||
}
|
||||
|
||||
pub struct BlogPost {
|
||||
pub name: String,
|
||||
pub html: String,
|
||||
}
|
174
src/styles/global.css
Normal file
|
@ -0,0 +1,174 @@
|
|||
/*
|
||||
The CSS in this style tag is based off of Bear Blog's default CSS.
|
||||
https://github.com/HermanMartinus/bearblog/blob/297026a877bc2ab2b3bdfbd6b9f7961c350917dd/templates/styles/blog/default.css
|
||||
License MIT: https://github.com/HermanMartinus/bearblog/blob/master/LICENSE.md
|
||||
*/
|
||||
|
||||
:root {
|
||||
--accent: #2337ff;
|
||||
--accent-dark: #000d8a;
|
||||
--black: 15, 18, 25;
|
||||
--gray: 96, 115, 159;
|
||||
--gray-light: 229, 233, 240;
|
||||
--gray-dark: 34, 41, 57;
|
||||
--gray-gradient: rgba(var(--gray-light), 50%), #fff;
|
||||
--box-shadow: 0 2px 6px rgba(var(--gray), 25%), 0 8px 24px rgba(var(--gray), 33%),
|
||||
0 16px 32px rgba(var(--gray), 33%);
|
||||
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Atkinson';
|
||||
src: url('/fonts/atkinson-regular.woff') format('woff');
|
||||
font-weight: 400;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Atkinson';
|
||||
src: url('/fonts/atkinson-bold.woff') format('woff');
|
||||
font-weight: 700;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Atkinson', sans-serif;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
min-height: 100vh;
|
||||
height: 100%;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
text-align: left;
|
||||
background: repeating-linear-gradient(to bottom, #522258 0%, #8c3061, #522258 100%);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
word-wrap: break-word;
|
||||
overflow-wrap: break-word;
|
||||
color: cyan;
|
||||
font-size: 20px;
|
||||
line-height: 1.7;
|
||||
}
|
||||
@media screen and (min-width:600px) {
|
||||
body {
|
||||
width: 95%;
|
||||
}
|
||||
}
|
||||
@media screen and (max-width:600px){
|
||||
body{
|
||||
width: 95%;
|
||||
}
|
||||
}
|
||||
main {
|
||||
flex: 1;
|
||||
width: 720px;
|
||||
max-width: calc(100% - 2em);
|
||||
margin: auto;
|
||||
min-height: 100%;
|
||||
padding: 1em 1em;
|
||||
margin-bottom: -2em;
|
||||
}
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
margin: 0 0 0.5rem 0;
|
||||
color: rgb(0,255,159);
|
||||
line-height: 1.2;
|
||||
}
|
||||
h1 {
|
||||
font-size: 3.052em;
|
||||
}
|
||||
h2 {
|
||||
font-size: 2.441em;
|
||||
}
|
||||
h3 {
|
||||
font-size: 1.953em;
|
||||
}
|
||||
h4 {
|
||||
font-size: 1.563em;
|
||||
}
|
||||
h5 {
|
||||
font-size: 1.25em;
|
||||
}
|
||||
strong,
|
||||
b {
|
||||
font-weight: 700;
|
||||
}
|
||||
a {
|
||||
color: white;
|
||||
}
|
||||
a:hover {
|
||||
color: black;
|
||||
}
|
||||
p {
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
.prose p {
|
||||
margin-bottom: 2em;
|
||||
}
|
||||
textarea {
|
||||
width: 100%;
|
||||
font-size: 16px;
|
||||
}
|
||||
input {
|
||||
font-size: 16px;
|
||||
}
|
||||
table {
|
||||
width: 100%;
|
||||
}
|
||||
img {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
border-radius: 8px;
|
||||
}
|
||||
code {
|
||||
padding: 2px 5px;
|
||||
background-color: black;
|
||||
border-radius: 10px;
|
||||
}
|
||||
pre {
|
||||
padding: 1.5em;
|
||||
border-radius: 8px;
|
||||
}
|
||||
pre > code {
|
||||
all: unset;
|
||||
}
|
||||
blockquote {
|
||||
border-left: 4px solid var(--accent);
|
||||
padding: 0 0 0 20px;
|
||||
margin: 0px;
|
||||
font-size: 1.333em;
|
||||
}
|
||||
hr {
|
||||
border: none;
|
||||
border-top: 1px solid rgb(var(--gray-light));
|
||||
}
|
||||
@media (max-width: 720px) {
|
||||
body {
|
||||
font-size: 18px;
|
||||
}
|
||||
main {
|
||||
padding: 1em;
|
||||
}
|
||||
}
|
||||
|
||||
.sr-only {
|
||||
border: 0;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
position: absolute !important;
|
||||
height: 1px;
|
||||
width: 1px;
|
||||
overflow: hidden;
|
||||
/* IE6, IE7 - a 0 height clip, off to the bottom right of the visible 1px box */
|
||||
clip: rect(1px 1px 1px 1px);
|
||||
/* maybe deprecated but we need to support legacy browsers */
|
||||
clip: rect(1px, 1px, 1px, 1px);
|
||||
/* modern browsers, clip-path works inwards from each corner */
|
||||
clip-path: inset(50%);
|
||||
/* added line to stop words getting smushed together (as they go onto separate lines and some screen readers do not understand line feeds as a space */
|
||||
white-space: nowrap;
|
||||
}
|
|
@ -1,17 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<head>
|
||||
<link rel="stylesheet" href="style.css"/>
|
||||
<script src="htmx.min.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div hx-confirm="Are you sure?">
|
||||
<button hx-delete="/account">
|
||||
Delete My Account
|
||||
</button>
|
||||
<button hx-put="/account">
|
||||
Update My Account
|
||||
</button>
|
||||
</div>
|
||||
|
||||
|
||||
</body>
|
1
static/htmx.min.js
vendored
|
@ -1,11 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<head>
|
||||
<link rel="stylesheet" href="style.css"/>
|
||||
<script src="htmx.min.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<ul hx-get="/api/posts" hx-swap="innerHTML" hx-trigger="load">
|
||||
This is a test
|
||||
</ul>
|
||||
</body>
|
6
tsconfig.json
Normal file
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"extends": "astro/tsconfigs/strict",
|
||||
"compilerOptions": {
|
||||
"strictNullChecks": true
|
||||
}
|
||||
}
|