Compare commits

2 Commits

Author SHA1 Message Date
4019d4cb10 remove mention of *arr stack 2026-01-13 00:25:49 -06:00
dee891ef68 added markdown support for the blog style pages 2026-01-13 00:25:07 -06:00
3 changed files with 144 additions and 4 deletions

BIN
public/homelabber.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 116 KiB

View File

@@ -5,6 +5,60 @@ interface RichTextRendererProps {
content: string; content: string;
} }
const parseInline = (text: string) => {
const parts = text.split(/(\*\*.*?\*\*)/g);
return parts.map((part, index) => {
if (part.startsWith('**') && part.endsWith('**')) {
return <strong key={index} style={{ color: 'white' }}>{part.slice(2, -2)}</strong>;
}
return part;
});
};
const parseMarkdown = (text: string) => {
const lines = text.split('\n');
const elements: React.ReactNode[] = [];
let currentListItems: React.ReactNode[] = [];
lines.forEach((line, i) => {
const trimmed = line.trim();
if (trimmed.startsWith('* ') || trimmed.startsWith('- ')) {
const content = trimmed.substring(2);
currentListItems.push(
<li key={`li-${i}`} style={{ marginBottom: '4px' }}>
{parseInline(content)}
</li>
);
} else {
if (currentListItems.length > 0) {
elements.push(
<ul key={`ul-${i}`} style={{ paddingLeft: '20px', marginBottom: '16px', listStyleType: 'disc' }}>
{currentListItems}
</ul>
);
currentListItems = [];
}
if (trimmed.length > 0) {
elements.push(
<p key={`p-${i}`} style={{ marginBottom: '16px' }}>
{parseInline(line)}
</p>
);
}
}
});
if (currentListItems.length > 0) {
elements.push(
<ul key="ul-end" style={{ paddingLeft: '20px', marginBottom: '16px', listStyleType: 'disc' }}>
{currentListItems}
</ul>
);
}
return elements;
};
const RichTextRenderer: React.FC<RichTextRendererProps> = ({ content }) => { const RichTextRenderer: React.FC<RichTextRendererProps> = ({ content }) => {
if (!content) return null; if (!content) return null;
@@ -22,11 +76,11 @@ const RichTextRenderer: React.FC<RichTextRendererProps> = ({ content }) => {
return <CodeBlock key={index} language={language} code={code} />; return <CodeBlock key={index} language={language} code={code} />;
} }
// Regular text (split by newlines for paragraph handling) // Regular text with markdown parsing
return ( return (
<span key={index} style={{ whiteSpace: 'pre-wrap' }}> <div key={index}>
{part} {parseMarkdown(part)}
</span> </div>
); );
})} })}
</div> </div>

View File

@@ -103,6 +103,92 @@ jobs:
content: "The deployment script is a PowerShell script that is used to deploy the digital resume frontend to a Windows server. It first checks if the project directory exists, if it does it pulls the latest changes from the repository, if it doesn't it clones the repository. Then it stops the container, removes it, builds the image, and runs the container. Finally it verifies that the container is running and reports to gitea if it was successfull" content: "The deployment script is a PowerShell script that is used to deploy the digital resume frontend to a Windows server. It first checks if the project directory exists, if it does it pulls the latest changes from the repository, if it doesn't it clones the repository. Then it stops the container, removes it, builds the image, and runs the container. Finally it verifies that the container is running and reports to gitea if it was successfull"
}, },
] ]
},
{
id: 102,
title: "Home Lab Architecture",
description: "A comprehensive self-hosted homelab running 20+ services. Orchestrated with Docker Compose for media automation, personal cloud storage, and secure remote access.",
techStack: ["Docker", "Nginx", "Tailscale", "Authentik", "PostgreSQL"],
image: "/homelabber.png", // Placeholder
links: {},
hasDetails: true,
sections: [
{
type: 'text',
title: 'Overview',
content: "My homelab acts as my personal cloud, giving me control over my data and providing a playground for learning system administration. It runs on a dedicated machine using Docker Compose to manage a suite of interconnected microservices. Access is secured via Tailscale VPN and Authentik SSO."
},
{
type: 'text',
title: 'Media & Automation',
content: "The core of the lab is the media stack. **Jellyfin** serves as the streaming frontend. Behind the scenes, automation tools handle content acquisition and organization. **Jellyseerr** provides a clean interface for requesting new additions to the library."
},
{
type: 'text',
title: 'Personal Cloud & Productivity',
content: "I've replaced several commercial services with self-hosted alternatives:\n\n* **Immich**: A powerful photo and video backup solution, replacing Google Photos.\n* **Gitea**: My self-hosted Git server for code management (including CI/CD runners).\n* **Mealie**: A recipe manager to organize and plan meals."
},
{
type: 'text',
title: 'Infrastructure & Security',
content: "Reliability and security are key. **Nginx** handles reverse proxying with **Certbot** managing SSL certificates. **Fail2ban** prevents intrusion attempts. **Authentik** provides a unified identity layer. **Portainer** offers a GUI for Docker management, and **Tailscale** creates a secure mesh network for remote access without exposing ports."
},
{
type: 'text',
title: 'Monitoring',
content: "**Homepage** serves as the main dashboard. **Beszel** and **Uptime Kuma** monitor system resources and service availability, while **Speedtest Tracker** keeps a historic log of network performance."
},
{
type: 'code',
language: 'yaml',
title: 'Docker Compose Configuration',
code: `services:
nginx:
image: nginx:stable
restart: unless-stopped
ports: ["80:80", "443:443"]
# ... configurations
immich-server:
image: ghcr.io/immich-app/immich-server:release
environment:
- DB_HOSTNAME=immich-postgres
- REDIS_HOSTNAME=immich-redis
depends_on: [immich-redis, immich-postgres]
gitea:
image: gitea/gitea:latest
ports: ["3000:3000", "2222:22"]
environment:
- GITEA__actions__ENABLED=true
jellyfin:
image: jellyfin/jellyfin:latest
ports: ["8096:8096"]
volumes:
- ./media:/media
# Monitoring
beszel:
image: henrygd/beszel:latest
ports: ["8090:8090"]
uptime-kuma:
image: louislam/uptime-kuma:latest
ports: ["3002:3001"]
# Security & Networking
tailscale:
image: tailscale/tailscale:latest
network_mode: host
authentik-server:
image: ghcr.io/goauthentik/server:latest
ports: ["9001:9000", "9444:9443"]
# ... full configuration includes 20+ services`
}
]
} }
]; ];