first template launch #2

Merged
michael_dileo merged 1 commits from first-version into main 2025-08-14 00:11:50 +00:00
11 changed files with 902 additions and 23 deletions

28
.gitignore vendored Normal file
View File

@@ -0,0 +1,28 @@
# Dependencies
node_modules/
# Build outputs
dist/
public/css/optimized/
# Test files
public/*-optimized.html
public/*-minimal.html
# Package lock (can be regenerated)
package-lock.json
# IDE
.idea/
.vscode/
.cursor/
# OS
.DS_Store
Thumbs.db
# Logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*

View File

@@ -1,13 +1,24 @@
FROM nginx:1.25-alpine FROM nginx:1.25-alpine
# Create nginx user and required directories with proper permissions
RUN mkdir -p /var/cache/nginx/client_temp \
/var/cache/nginx/proxy_temp \
/var/cache/nginx/fastcgi_temp \
/var/cache/nginx/uwsgi_temp \
/var/cache/nginx/scgi_temp \
/tmp \
&& chown -R nginx:nginx /var/cache/nginx \
&& chown -R nginx:nginx /tmp \
&& chmod -R 755 /var/cache/nginx \
&& chmod -R 755 /tmp
COPY nginx.conf /etc/nginx/nginx.conf COPY nginx.conf /etc/nginx/nginx.conf
COPY public/ /usr/share/nginx/html/ COPY dist/ /usr/share/nginx/html/
RUN chown -R nginx:nginx /usr/share/nginx/html \ RUN chown -R nginx:nginx /usr/share/nginx/html \
&& chmod -R 755 /usr/share/nginx/html && chmod -R 755 /usr/share/nginx/html
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \ HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
CMD wget --no-verbose --tries=1 --spider http://localhost/ || exit 1 CMD wget --no-verbose --tries=1 --spider http://localhost/ || exit 1

266
README.md
View File

@@ -1,3 +1,265 @@
# Keyboard-Vagabond-Web # Keyboard Vagabond Web
static website landing page for Keyboard Vagabond Static website landing page for the Keyboard Vagabond fediverse community. This project creates an optimized, containerized website for deployment on Kubernetes.
## 🌟 Features
- **Ultra-optimized CSS**: 94.4% reduction (154KB → 8.6KB) using PurgeCSS and cssnano
- **Minified HTML**: 16.3% size reduction with html-minifier-terser
- **Containerized**: Docker + nginx for Kubernetes deployment
- **Modern CSS Framework**: Built with Pico CSS
- **ARM64 Compatible**: Optimized for ARM-based Kubernetes clusters
## 🏗️ Project Structure
```
Keyboard-Vagabond-Web/
├── dist/ # 🚀 Production build (optimized)
│ ├── index.html # Minified landing page
│ ├── about.html # Minified about page
│ ├── css/styles.css # Optimized CSS (8.6KB)
│ └── assets/ # Static assets
├── public/ # 📝 Development files
├── build.sh # 🐳 Container build script
├── minify-build.js # 🗜️ CSS/HTML optimization
├── Dockerfile # 🐳 Container definition
├── nginx.conf # 🌐 Web server config
└── package.json # 📦 Dependencies & scripts
```
## 🚀 Quick Start
### Prerequisites
- **Node.js** (v16+ recommended)
- **Docker**
- **npm**
### Development
```bash
# Clone and setup
git clone <your-repo>
cd Keyboard-Vagabond-Web
npm install
# View development version
open public/index.html
```
### Build & Deploy
```bash
# Full production build (recommended)
npm run build
# to run locally
docker build -t keyboard-vagabond-local .
docker run -d -p 8080:80 --name test-container keyboard-vagabond-local
```
## 📋 Available Scripts
| Command | Description |
|---------|-------------|
| `npm run build` | **Full production build** - Optimize + containerize + push |
| `npm run dist-minified` | Create optimized `dist/` folder (CSS + HTML minified) |
| `npm run dist` | Create `dist/` folder with CSS optimization (auto-runs PurgeCSS) |
| `npm run build-basic` | Basic build using `dist` command |
| `npm run optimize-css` | CSS optimization only (PurgeCSS) |
| `npm run clean` | Clean all build artifacts |
| `./build.sh` | Build and push Docker container |
## 🚢 Deployment Guide
### Step 1: Prepare Environment
```bash
# Ensure you have access to the registry
docker login registry.keyboardvagabond.com
# Install dependencies
npm install
```
### Step 2: Build Production Assets
```bash
# Create optimized build (recommended - includes minification)
npm run dist-minified
# OR create basic optimized build
npm run dist
```
This creates a `dist/` folder with:
- ✅ CSS optimized from 154KB to 8.6KB (94.4% reduction)
- ✅ HTML minified (16.3% reduction)
- ✅ Production-ready static files
### Step 3: Container Build & Push
```bash
# Build and push container
./build.sh
```
Or use the complete pipeline:
```bash
# Full build pipeline
npm run build
```
### Step 4: Kubernetes Deployment
The container is pushed to `registry.keyboardvagabond.com/library/keyboard-vagabond-web:latest`
Example Kubernetes deployment:
```yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: keyboard-vagabond-web
spec:
replicas: 2
selector:
matchLabels:
app: keyboard-vagabond-web
template:
metadata:
labels:
app: keyboard-vagabond-web
spec:
containers:
- name: web
image: registry.keyboardvagabond.com/library/keyboard-vagabond-web:latest
ports:
- containerPort: 80
resources:
requests:
memory: "64Mi"
cpu: "50m"
limits:
memory: "128Mi"
cpu: "100m"
---
apiVersion: v1
kind: Service
metadata:
name: keyboard-vagabond-web
spec:
selector:
app: keyboard-vagabond-web
ports:
- port: 80
targetPort: 80
type: ClusterIP
```
## 🎯 Optimization Results
| Metric | Before | After | Improvement |
|--------|--------|-------|-------------|
| **CSS Size** | 154.6KB | 8.6KB | **94.4% reduction** |
| **HTML Size** | 14.7KB | 12.3KB | **16.3% reduction** |
| **Total Saved** | - | 148.4KB | **Massive performance gain** |
## 🔧 Development Workflow
### Making Changes
1. **Edit source files** in `public/`
2. **Test locally** by opening `public/index.html`
3. **Build optimized version**: `npm run dist-minified`
4. **Test production build**: `open dist/index.html`
5. **Deploy**: `npm run build`
### CSS Customization
- **Custom styles**: Edit `public/site-styles/style.css`
- **Pico CSS**: Uses green theme with custom variables
- **Optimization**: Automatically removes unused CSS on build
### Content Updates
- **Main page**: `public/index.html`
- **About page**: `public/about.html`
- **Assets**: Add to `public/assets/`
## 🐳 Container Details
- **Base Image**: `nginx:1.25-alpine`
- **Platform**: `linux/arm64` (ARM Kubernetes optimized)
- **Port**: 80
- **Health Check**: Built-in HTTP health endpoint
- **Security**: Runs as non-root nginx user
## 🏷️ Tech Stack
- **HTML/CSS**: Semantic HTML with Pico CSS framework
- **Build Tools**: PurgeCSS, cssnano, html-minifier-terser
- **Container**: Docker + nginx
- **Deployment**: Kubernetes on ARM64 cluster
- **Registry**: Harbor (registry.keyboardvagabond.com)
## 📦 Dependencies
```json
{
"devDependencies": {
"purgecss": "^6.0.0",
"cssnano": "^7.0.6",
"cssnano-cli": "^1.0.5",
"html-minifier-terser": "^7.2.0"
}
}
```
## 🚨 Troubleshooting
### Build Issues
```bash
# Clean rebuild
rm -rf dist/ node_modules/
npm install
npm run build
```
### Registry Access
```bash
# Login to registry
docker login registry.keyboardvagabond.com
# Verify access
docker pull registry.keyboardvagabond.com/library/keyboard-vagabond-web:latest
```
### Missing dist/ folder
The build script automatically creates `dist/` if missing. If you get errors:
```bash
# Manual dist creation
npm run dist-minified
```
## 📄 License
MIT License - See LICENSE file for details
## 🤝 Contributing
1. Fork the repository
2. Create feature branch
3. Make changes in `public/` directory
4. Test with `npm run dist-minified`
5. Submit pull request
---
**Ready to deploy the Keyboard Vagabond fediverse community! 🚀**

14
build.sh Normal file → Executable file
View File

@@ -3,10 +3,22 @@ set -e
REGISTRY="registry.keyboardvagabond.com" REGISTRY="registry.keyboardvagabond.com"
VERSION="latest" VERSION="latest"
PLATFORM="linux/arm64" PLATFORM="linux/arm64"
IMAGE_NAME="keyboard-vagabond-landing" IMAGE_NAME="keyboard-vagabond-web"
echo "Building Keyboard Vagabond Landing Page..." echo "Building Keyboard Vagabond Landing Page..."
# Ensure dist/ folder exists with optimized files
if [ ! -d "dist" ]; then
echo "⚠️ No dist/ folder found. Creating optimized build first..."
if [ -f "package.json" ]; then
npm run dist-minified
else
echo "❌ Error: No package.json found. Run 'npm install' first."
exit 1
fi
echo "✅ Optimized build created in dist/"
fi
docker build \ docker build \
--platform $PLATFORM \ --platform $PLATFORM \
--tag $REGISTRY/library/$IMAGE_NAME:$VERSION \ --tag $REGISTRY/library/$IMAGE_NAME:$VERSION \

298
minify-build.js Normal file
View File

@@ -0,0 +1,298 @@
#!/usr/bin/env node
/**
* Enhanced Build Script with Minification for Keyboard Vagabond
*
* This script:
* 1. Optimizes CSS with PurgeCSS
* 2. Minifies CSS with cssnano
* 3. Bundles and minifies JavaScript with Terser
* 4. Minifies HTML with html-minifier-terser
* 5. Creates a production-ready dist/ folder
*/
const { PurgeCSS } = require('purgecss');
const cssnano = require('cssnano');
const postcss = require('postcss');
const { minify: htmlMinify } = require('html-minifier-terser');
const { minify: jsMinify } = require('terser');
const fs = require('fs').promises;
const path = require('path');
async function createOptimizedBuild() {
console.log('🚀 Starting enhanced build with minification...');
// Clean and create dist directory
await fs.rm('dist', { recursive: true, force: true });
await fs.mkdir('dist', { recursive: true });
await fs.mkdir('dist/css', { recursive: true });
await fs.mkdir('dist/site-styles', { recursive: true });
await fs.mkdir('dist/assets', { recursive: true });
try {
// Step 1: CSS Optimization with PurgeCSS
console.log('📝 Step 1: Optimizing CSS with PurgeCSS...');
const purgeResults = await new PurgeCSS().purge({
content: [
'public/index.html',
'public/about.html',
'index.html'
],
css: [
'public/css/pico.green.min.css',
'public/css/pico.colors.min.css'
],
safelist: {
standard: [
'container',
'banner-container',
'banner',
'banner-title',
'banner-subtitle',
'video-container',
'photo-gallery',
'photo-item'
],
deep: [
/^--pico-/,
/\[data-theme/,
/data-theme/,
/:hover/,
/:focus/,
/:active/,
/:visited/
],
// Keep base typography selectors that are essential for proper font rendering
greedy: [
/^:where\(:root\)$/,
/^:where\(:host\)$/,
/^:root$/,
/^:host$/,
/^body$/,
/^html$/,
/^h[1-6]$/,
/^p$/,
/^ul$/,
/^li$/,
/^strong$/,
/^a$/
]
},
variables: true,
keyframes: true
});
// Step 1.5: Add essential font rule that PurgeCSS removes too aggressively
const fontRule = `:where(:host),:where(:root){background-color:var(--pico-background-color);color:var(--pico-color);font-weight:var(--pico-font-weight);font-size:var(--pico-font-size);line-height:var(--pico-line-height);font-family:var(--pico-font-family);}`;
// Step 2: CSS Minification with cssnano
console.log('🗜️ Step 2: Minifying CSS with cssnano...');
const combinedCSS = purgeResults.map(result => result.css).join('\n\n') + '\n' + fontRule;
const minifiedCSS = await postcss([
cssnano({
preset: ['default', {
discardComments: { removeAll: true },
normalizeWhitespace: true,
colormin: true,
minifySelectors: true,
minifyParams: true,
mergeLonghand: true,
mergeRules: true
}]
})
]).process(combinedCSS, { from: undefined });
await fs.writeFile('dist/css/styles.css', minifiedCSS.css);
// Also minify custom site styles
const customCSS = await fs.readFile('public/site-styles/style.css', 'utf8');
const minifiedCustomCSS = await postcss([
cssnano({
preset: ['default', {
discardComments: { removeAll: true },
normalizeWhitespace: true
}]
})
]).process(customCSS, { from: undefined });
await fs.writeFile('dist/site-styles/style.css', minifiedCustomCSS.css);
// Step 3: JavaScript Bundling and Minification
console.log('📦 Step 3: Bundling and minifying JavaScript...');
await bundleAndMinifyJS();
// Step 4: HTML Minification
console.log('📄 Step 4: Minifying HTML...');
await minifyHTMLFile('public/index.html', 'dist/index.html');
await minifyHTMLFile('public/about.html', 'dist/about.html');
// Step 5: Copy assets
console.log('📁 Step 5: Copying assets...');
try {
const assets = await fs.readdir('public/assets');
for (const asset of assets) {
await fs.copyFile(
path.join('public/assets', asset),
path.join('dist/assets', asset)
);
}
} catch (err) {
console.log(' No assets directory found, skipping...');
}
// Calculate compression results
const originalCSSSize = (await fs.stat('public/css/pico.green.min.css')).size +
(await fs.stat('public/css/pico.colors.min.css')).size;
const optimizedCSSSize = (await fs.stat('dist/css/styles.css')).size;
const cssReduction = ((originalCSSSize - optimizedCSSSize) / originalCSSSize * 100).toFixed(1);
const originalHTMLSize = (await fs.stat('public/index.html')).size +
(await fs.stat('public/about.html')).size;
const minifiedHTMLSize = (await fs.stat('dist/index.html')).size +
(await fs.stat('dist/about.html')).size;
const htmlReduction = ((originalHTMLSize - minifiedHTMLSize) / originalHTMLSize * 100).toFixed(1);
// Calculate JavaScript size if bundled
let jsInfo = '';
try {
const bundledJSSize = (await fs.stat('dist/scripts/scripts.js')).size;
jsInfo = `\n JavaScript: Bundled into ${(bundledJSSize / 1024).toFixed(1)}KB`;
} catch (err) {
// No JavaScript files were bundled
}
console.log('\n✨ Build complete with minification!');
console.log('\n📊 Optimization Results:');
console.log(` CSS: ${(originalCSSSize / 1024).toFixed(1)}KB → ${(optimizedCSSSize / 1024).toFixed(1)}KB (${cssReduction}% reduction)`);
console.log(` HTML: ${(originalHTMLSize / 1024).toFixed(1)}KB → ${(minifiedHTMLSize / 1024).toFixed(1)}KB (${htmlReduction}% reduction)`);
if (jsInfo) console.log(jsInfo);
console.log(` Total CSS+HTML savings: ${((originalCSSSize + originalHTMLSize - optimizedCSSSize - minifiedHTMLSize) / 1024).toFixed(1)}KB`);
console.log('\n📂 Production files created in dist/');
console.log('🚀 Ready for deployment!');
} catch (error) {
console.error('❌ Build failed:', error);
process.exit(1);
}
}
async function bundleAndMinifyJS() {
// Create scripts directory
await fs.mkdir('dist/scripts', { recursive: true });
// Find all JavaScript files in site-scripts directory
const jsFiles = [];
try {
const siteScriptsDir = 'public/site-scripts';
const files = await fs.readdir(siteScriptsDir);
for (const file of files) {
if (file.endsWith('.js')) {
jsFiles.push(path.join(siteScriptsDir, file));
}
}
} catch (err) {
console.log(' No site-scripts directory found, skipping...');
return;
}
if (jsFiles.length === 0) {
console.log(' No JavaScript files found to bundle');
return;
}
console.log(` Found ${jsFiles.length} JavaScript file(s) to bundle:`);
jsFiles.forEach(file => console.log(` - ${path.basename(file)}`));
// Read and concatenate all JavaScript files
let combinedJS = '';
for (const jsFile of jsFiles) {
const content = await fs.readFile(jsFile, 'utf8');
combinedJS += `\n// File: ${path.basename(jsFile)}\n`;
combinedJS += content;
combinedJS += '\n';
}
// Minify the combined JavaScript
try {
const minified = await jsMinify(combinedJS, {
compress: {
drop_console: false, // Keep console.log for debugging if needed
drop_debugger: true,
dead_code: true,
conditionals: true,
evaluate: true,
booleans: true,
loops: true,
unused: true,
hoist_funs: true,
keep_fargs: false,
hoist_vars: false,
if_return: true,
join_vars: true,
cascade: true,
side_effects: true
},
mangle: {
reserved: ['theme', 'toggle', 'icon'] // Preserve important function names
},
format: {
comments: false
}
});
await fs.writeFile('dist/scripts/bundle.js', minified.code);
// Calculate compression ratio
const originalSize = Buffer.byteLength(combinedJS, 'utf8');
const minifiedSize = Buffer.byteLength(minified.code, 'utf8');
const reduction = ((originalSize - minifiedSize) / originalSize * 100).toFixed(1);
console.log(` ✅ JavaScript bundled: ${(originalSize / 1024).toFixed(1)}KB → ${(minifiedSize / 1024).toFixed(1)}KB (${reduction}% reduction)`);
} catch (error) {
console.error(' ❌ JavaScript minification failed:', error);
// Fallback: write unminified version
await fs.writeFile('dist/scripts/bundle.js', combinedJS);
console.log(' ✅ JavaScript bundled (unminified fallback)');
}
}
async function minifyHTMLFile(inputPath, outputPath) {
const html = await fs.readFile(inputPath, 'utf8');
// Update CSS references for production and add script reference
let updatedHTML = html
.replace(/<link rel="stylesheet" href="css\/pico\.green\.min\.css">/g, '')
.replace(/<link rel="stylesheet" href="css\/pico\.colors\.min\.css">/g, '')
.replace(/<link rel="stylesheet" href="site-styles\/style\.css">/g,
'<link rel="stylesheet" href="css/styles.css"><link rel="stylesheet" href="site-styles/style.css">')
.replace(/<\/body>/g, '<script src="scripts/scripts.js"></script></body>');
const minified = await htmlMinify(updatedHTML, {
collapseWhitespace: true,
removeComments: true,
removeRedundantAttributes: true,
removeScriptTypeAttributes: true,
removeStyleLinkTypeAttributes: true,
minifyCSS: true,
minifyJS: true,
useShortDoctype: true,
removeAttributeQuotes: false, // Keep for readability
removeEmptyAttributes: true,
sortAttributes: true,
sortClassName: true
});
await fs.writeFile(outputPath, minified);
console.log(`${path.basename(inputPath)}${path.basename(outputPath)}`);
}
// Run if called directly
if (require.main === module) {
createOptimizedBuild();
}
module.exports = { createOptimizedBuild };

View File

@@ -1,6 +1,5 @@
user nginx;
worker_processes auto; worker_processes auto;
pid /var/run/nginx.pid; pid /tmp/nginx.pid;
events { events {
worker_connections 1024; worker_connections 1024;

22
package.json Normal file
View File

@@ -0,0 +1,22 @@
{
"name": "keyboard-vagabond-web",
"version": "1.0.0",
"description": "Landing page for Keyboard Vagabond fediverse community",
"main": "index.js",
"scripts": {
"dist-minified": "node minify-build.js",
"build": "npm run dist-minified && ./build.sh",
"build-basic": "npm run dist && ./build.sh",
"clean": "rm -rf dist/ public/css/optimized/ public/*-optimized.html public/*-minimal.html"
},
"keywords": ["fediverse", "static-site", "css-optimization"],
"author": "Keyboard Vagabond",
"license": "MIT",
"devDependencies": {
"purgecss": "^6.0.0",
"cssnano": "^7.0.6",
"cssnano-cli": "^1.0.5",
"html-minifier-terser": "^7.2.0",
"terser": "^5.24.0"
}
}

View File

@@ -0,0 +1,83 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="color-scheme" content="light dark">
<link rel="stylesheet" href="css/pico.green.min.css">
<link rel="stylesheet" href="css/pico.colors.min.css">
<link rel="stylesheet" href="site-styles/style.css">
<title>Keyboard Vagabond - About</title>
</head>
<body>
<header class="container">
<nav>
<ul>
<li><a target="_blank"href="https://mastodon.keyboardvagabond.com/public">Mastodon</a></li>
<li><a target="_blank" href="https://piefed.keyboardvagabond.com">Piefed</a></li>
<li><a target="_blank" href="https://pixelfed.keyboardvagabond.com">Pixelfed</a></li>
<li><a target="_blank" href="https://bookwyrm.keyboardvagabond.com">Bookwyrm</a></li>
<li><a target="_blank" href="https://blog.keyboardvagabond.com">Write Freely</a></li>
<li><a target="_blank" href="https://picsur.keyboardvagabond.com">Picsur</a></li>
</ul>
<ul>
<li><a href="index.html">Home</a></li>
<li>
<button id="theme-toggle" class="outline secondary" role="switch" aria-label="Toggle theme">
<span id="theme-icon">🌙</span>
</button>
</li>
</ul>
</nav>
</header>
<main class="container">
<h1>About Keyboard Vagabond</h1>
<p>Keyboard Vagabond is a place where nomads, travelers, backpacker, whoever, can come together in a digital space that is free of advertising and the attention economy to share information and experiences. It is a place of mutual respect, courtesy, and understanding not just for the members who join, but also for those people and places we encounter on our journeys.</p>
<h4>Why Keyboard Vagabond</h4>
<p>Keyboard Vagabond was made because I saw multiple instances of people saying that, while there are travel communities on different instances, there was a space specifically for nomads, so I thought I would make it.</p>
<h2>What to expect and commitments</h2>
<p><strong>Moderation style</strong> -
An online community of respect and courtesy that is simultaneously light on moderation and banning, yet firm on not tolerating bigotry, hatred, etc. Be kind and we'll all have a good time.</p>
<p><strong>Sign ups</strong> -
Sign ups require manual approval to prevent spam.</p>
<p><strong>Data protection</strong> -
Your data is yours and you can download it at any time through the apps.
The servers are run in a cluster with data redundancy across nodes + nightly and weekly backups to offline storage.</p>
<p><strong>Should shutdown happen</strong> -
There will be a 3 month announcement in advance, in accordance with the <a href="https://joinmastodon.org/covenant">Mastodon Server Covenant</a>.</p>
<p><strong>Funding</strong> -
Keyboard Mastodon is currently funded by the admin, for a cost of ~$40 - $45 per month. Donations may be opened in the future, but have not been set up at this time.</p>
<h1>The Dirty Technicals</h1>
<p>If you're not a mega-nerd, turn back now.</p>
<p>I warned you.</p>
<p>Keyboard Vagabond is run on a 3 node Kubernetes cluster running on 3x Arm VPSs hosted by NetCup in Amsterdam. I chose Amsterdam because I thought that Europe would be more centrally located for people who are traveling the world.</p>
<h4>The Specs</h4>
<p><strong>Servers</strong> - 3x 10 ARM vCPUs, 16GB Ram, 500GB (~50GB for Talos and the rest for Longhorn) storage running <a href="https://www.talos.dev">Talos</a> and Kubernetes.
<p><strong>Storage</strong> - Longhorn ensures that there are at least 2 copies across the nodes.</p>
<p><strong>Backups and Content</strong> - Backups and content are stored in S3 storage hosted by BackBlaze with CloudFlare providing CDN. I've already run through disaster recovery and restored database backups from S3.</p>
<p><strong>CDN</strong> - CloudFlare provides CDN and special rules have been set up to be sure that as much as possible is cached.</p>
<p><strong>Security</strong> - ports are closed off to the world and secured with CloudFlare tunnels and TailScale as the only means of access outside of website access.</p>
<p><strong>Observability and Logging</strong> - OpenObserve dashboards and log aggregation.</p>
<p><strong>Domain</strong> - domain is provided by CloudFlare</p>
<p><strong>Services</strong> - Typical arrangement for services is that web services get 2 instances and workers get 1 instance with autoscaling. Web pods scale horizontally and workers scale vertically, then horizontally.</p>
<p><strong>Source Code</strong> - If I get the source code to where I'm comfortable sharing, I'll post a link here. And if you're experienced in k8s, I'd always appreciate a review. :)</p>
<p><strong>Costs</strong><br />
VPS servers - 3x ~$13 / mth = $40/mth<br />
Domain name - $12/year<br />
Backblaze - $6/TB/mth = ~$2/mth<br />
Total: ~$45/mth</p>
</main>
<footer class="container">
<p>Contact: <a href="mailto:admin@keyboardvagabond.com">admin@keyboardvagabond.com</a>, any of the @sysadmin accounts on the instances</p>
<p></p>
<p>Copyright 2025 Keyboard Vagabond</p>
</footer>
<script src="scripts/bundle.js"></script>
</body>
</html>

View File

@@ -1,24 +1,41 @@
<!doctype html> <!doctype html>
<html lang="en" data-theme="light"> <html lang="en">
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="color-scheme" content="light"> <meta name="color-scheme" content="light dark">
<link rel="stylesheet" href="css/pico.green.min.css"> <link rel="stylesheet" href="css/pico.green.min.css">
<link rel="stylesheet" href="css/pico.colors.min.css"> <link rel="stylesheet" href="css/pico.colors.min.css">
<link rel="stylesheet" href="site-styles/style.css"> <link rel="stylesheet" href="site-styles/style.css">
<title>Hello world!</title> <title>Keyboard Vagabond</title>
</head> </head>
<body> <body>
<!-- Header --> <!-- Header -->
<header> <header class="container">
<nav>
<ul>
<li><a target="_blank"href="https://mastodon.keyboardvagabond.com/public">Mastodon</a></li>
<li><a target="_blank" href="https://piefed.keyboardvagabond.com">Piefed</a></li>
<li><a target="_blank" href="https://pixelfed.keyboardvagabond.com">Pixelfed</a></li>
<li><a target="_blank" href="https://bookwyrm.keyboardvagabond.com">Bookwyrm</a></li>
<li><a target="_blank" href="https://blog.keyboardvagabond.com">Write Freely</a></li>
<li><a target="_blank" href="https://picsur.keyboardvagabond.com">Picsur</a></li>
</ul>
<ul>
<li><a href="about.html">About</a></li>
<li>
<button id="theme-toggle" class="outline secondary" role="switch" aria-label="Toggle theme">
<span id="theme-icon">🌙</span>
</button>
</li>
</ul>
</nav>
</header> </header>
<div class="banner-container"> <div class="banner-container">
<img src="assets/banner.jpg" alt="Scenic mountain road with snow-capped peaks" class="banner"> <img src="https://picsur.michaeldileo.org/i/36516edf-1a67-4565-aa8e-c10dbe743fd6.jpg?width=2048" alt="Scenic mountain road with snow-capped peaks" class="banner">
<h1 class="banner-title">Welcome to Keyboard Vagabond</h1> <h1 class="banner-title">Welcome to Keyboard Vagabond</h1>
<p class="banner-subtitle">A space in the fediverse for travelers, nomads, and vagabonds of all kinds</p> <p class="banner-subtitle">A space in the fediverse for travelers, nomads, and vagabonds of all kinds</p>
</div> </div>
@@ -30,7 +47,7 @@
<p>&lt;mascot here? :D &gt; <p>&lt;mascot here? :D &gt;
<strong>mascot intro and why it was chosen</strong> <strong>mascot intro and why it was chosen</strong>
</p> </p>
<p><strong>Here, you are a member, not just a user</strong> - we want to create community in this space, being <p><strong>Here, you are a member, not just a user</strong> - We want to create community in this space, being
respectful of each other as well as the places go and the people we see. This space is what we make of it. respectful of each other as well as the places go and the people we see. This space is what we make of it.
</p> </p>
<h2>What you'll find here</h2> <h2>What you'll find here</h2>
@@ -67,12 +84,13 @@
<p>You can make a post on Mastodon, tag a community on Lemmy/Piefed and people there can respond. Someone can <p>You can make a post on Mastodon, tag a community on Lemmy/Piefed and people there can respond. Someone can
make a video on PeerTube (Youtube alternative) and discuss on Mastodon. Your Write Freely blog posts will, make a video on PeerTube (Youtube alternative) and discuss on Mastodon. Your Write Freely blog posts will,
if you enable it, be visible on Mastodon and people can follow your blog account.</p> if you enable it, be visible on Mastodon and people can follow your blog account.</p>
<p>Check out this amazing video by <a href="https://news.elenarossini.com">Elena Rossini</a>. You can follow her <p>Check out this amazing four minute video by <a href="https://news.elenarossini.com">Elena Rossini</a>. You can follow her
on Mastodon by searching @elena@aseachange.com.</p> on Mastodon by searching @elena@aseachange.com.</p>
<div class="video-container"> <div class="video-container">
<iframe title="Introducing the Fediverse: a New Era of Social Media" <iframe title="Introducing the Fediverse: a New Era of Social Media"
src="https://videos.elenarossini.com/videos/embed/64VuNCccZNrP4u9MfgbhkN" frameborder="0" allowfullscreen="" src="https://videos.elenarossini.com/videos/embed/64VuNCccZNrP4u9MfgbhkN" frameborder="0" allowfullscreen=""
sandbox="allow-same-origin allow-scripts allow-popups allow-forms"></iframe> sandbox="allow-same-origin allow-scripts allow-popups allow-forms"
loading="lazy"></iframe>
</div> </div>
<h2>How to get started in the Fediverse</h2> <h2>How to get started in the Fediverse</h2>
<p>The best way to see what's available in the fediverse is to start off on a larger instance, which will be <p>The best way to see what's available in the fediverse is to start off on a larger instance, which will be
@@ -84,7 +102,7 @@
<p><strong>Popular places to get started are</strong>:</p> <p><strong>Popular places to get started are</strong>:</p>
<strong><a href="https://lemmy.zip">Lemmy.zip</a></strong> for Lemmy or <strong><a <strong><a href="https://lemmy.zip">Lemmy.zip</a></strong> for Lemmy or <strong><a
href="https://piefed.social">Piefed.social</a></strong> for PieFed, which is the software that this href="https://piefed.social">Piefed.social</a></strong> for PieFed, which is the software that this
community runs. Like Reddit, you can browse community runs.
</p> </p>
<p><strong><a href="https://mastodon.social">Mastodon.social</a></strong> - the largest mastodon instance. <p><strong><a href="https://mastodon.social">Mastodon.social</a></strong> - the largest mastodon instance.
Search for hashtags or check out whats trending and follow them to create your timeline.</p> Search for hashtags or check out whats trending and follow them to create your timeline.</p>
@@ -101,18 +119,32 @@
<div class="photo-item"> <div class="photo-item">
<small>Check out that Explore button on the main page.</small> <small>Check out that Explore button on the main page.</small>
<img src="https://picsur.keyboardvagabond.com/i/48624639-e731-4e50-a166-f88cf9eccded.jpg?width=400" <img src="https://picsur.keyboardvagabond.com/i/48624639-e731-4e50-a166-f88cf9eccded.jpg?width=400"
alt="Explore button on main page"> alt="Explore button on main page" loading="lazy" />
</div> </div>
<div class="photo-item"> <div class="photo-item">
<small>Visit the Posts, Hashtags, and News tabs to see what's on the server</small> <small>Visit the Posts, Hashtags, and News tabs to see what's on the server</small>
<img src="https://picsur.keyboardvagabond.com/i/d05996c7-5c23-450e-9ca0-b7e532d1c700.jpg?width=700" <img src="https://picsur.keyboardvagabond.com/i/d05996c7-5c23-450e-9ca0-b7e532d1c700.jpg?width=700"
alt="Posts, Hashtags, and News tabs"> alt="Posts, Hashtags, and News tabs" loading="lazy" />
</div>
<div class="photo-item">
<small>Look at the local communities to see what communities have been created on this server specifically. The rest of the communities are ones that this server is following.
Following communities on other instances will result in them being shown here.
</small>
<img src="https://picsur.michaeldileo.org/i/e07dddbc-d264-4f12-8877-17eed896026a.jpg?width=700"
alt="Communities, Local Communities" loading="lazy" />
</div>
<div class="photo-item">
<small>Scroll all the way to the bottom to find local topics. Topics are groups of communities. You can suggest more in the Meta community.</small>
<img src="https://picsur.michaeldileo.org/i/5a977aa0-b9bb-4376-a348-f879c7162f63.jpg?width=700"
alt="Local topics" loading="lazy" />
</div> </div>
</div> </div>
</main> </main>
<!-- Footer --> <!-- Footer -->
<footer></footer> <footer></footer>
<script src="scripts/bundle.js"></script>
</body> </body>
</html> </html>

View File

@@ -0,0 +1,73 @@
(function() {
const themeToggle = document.getElementById('theme-toggle');
const themeIcon = document.getElementById('theme-icon');
function getStoredTheme() {
return localStorage.getItem('picoPreferredColorScheme') || 'auto';
}
function storeTheme(theme) {
localStorage.setItem('picoPreferredColorScheme', theme);
}
function getSystemTheme() {
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
}
function applyTheme() {
const storedTheme = getStoredTheme();
let actualTheme;
if (storedTheme === 'auto') {
actualTheme = getSystemTheme();
} else {
actualTheme = storedTheme;
}
document.documentElement.setAttribute('data-theme', actualTheme);
if (actualTheme === 'dark') {
themeIcon.textContent = '☀️';
themeToggle.setAttribute('aria-pressed', 'true');
} else {
themeIcon.textContent = '🌙';
themeToggle.setAttribute('aria-pressed', 'false');
}
}
function toggleTheme() {
const currentStored = getStoredTheme();
let nextTheme;
if (currentStored === 'auto') {
nextTheme = 'light';
} else if (currentStored === 'light') {
nextTheme = 'dark';
} else {
nextTheme = 'auto';
}
storeTheme(nextTheme);
applyTheme();
}
function init() {
if (themeToggle) {
applyTheme();
themeToggle.addEventListener('click', toggleTheme);
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', () => {
if (getStoredTheme() === 'auto') {
applyTheme();
}
});
}
}
if (document.readyState !== 'loading') {
init();
} else {
document.addEventListener('DOMContentLoaded', init);
}
})();

View File

@@ -17,8 +17,9 @@
.banner-title { .banner-title {
position: absolute; position: absolute;
top: var(--pico-typography-spacing-vertical); top: var(--pico-spacing);
left: var(--pico-spacing); left: var(--pico-spacing);
right: var(--pico-spacing);
color: white; /* Keep white for banner overlay readability */ color: white; /* Keep white for banner overlay readability */
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.7); text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.7);
margin: 0; margin: 0;
@@ -42,11 +43,47 @@
z-index: 10; z-index: 10;
} }
/* Responsive positioning to match Pico container */
@media (min-width: 576px) {
.banner-title {
left: 50%;
right: auto;
transform: translateX(-50%);
max-width: 510px;
width: 100%;
}
}
@media (min-width: 768px) {
.banner-title {
max-width: 700px;
}
}
@media (min-width: 1024px) {
.banner-title {
max-width: 950px;
}
}
@media (min-width: 1280px) {
.banner-title {
max-width: 1200px;
}
}
@media (min-width: 1536px) {
.banner-title {
max-width: 1450px;
}
}
@media (max-width: 768px) { @media (max-width: 768px) {
.banner-title { .banner-title {
font-size: 1.8rem; font-size: 1.8rem;
top: calc(var(--pico-typography-spacing-vertical) * 0.5); top: calc(var(--pico-typography-spacing-vertical) * 0.5);
left: var(--pico-spacing); left: var(--pico-spacing);
right: var(--pico-spacing);
} }
} }
@@ -151,8 +188,8 @@
} }
} }
/* Make headers use primary green color */ /* Make headers use primary green color - only for light theme and default */
:root { :root:not([data-theme="dark"]) {
--pico-h1-color: var(--pico-primary); --pico-h1-color: var(--pico-primary);
--pico-h2-color: var(--pico-primary); --pico-h2-color: var(--pico-primary);
--pico-h3-color: var(--pico-primary); --pico-h3-color: var(--pico-primary);
@@ -176,3 +213,25 @@
/* h1, h2, h3, h4, h5, h6 { /* h1, h2, h3, h4, h5, h6 {
color: #2d5016 !important; color: #2d5016 !important;
} */ } */
/* Theme toggle button styling */
#theme-toggle {
margin: 0;
padding: calc(var(--pico-spacing) * 0.5);
min-height: auto;
display: inline-flex;
align-items: center;
justify-content: center;
border-radius: var(--pico-border-radius);
font-size: 1.2rem;
transition: all 0.2s ease;
}
#theme-toggle:hover {
transform: scale(1.05);
}
#theme-toggle #theme-icon {
display: block;
line-height: 1;
}