first template launch (#2)
Co-authored-by: Michael DiLeo <michael.dileo@oakstreethealth.com> Reviewed-on: #2
This commit was merged in pull request #2.
This commit is contained in:
28
.gitignore
vendored
Normal file
28
.gitignore
vendored
Normal 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*
|
||||||
15
Dockerfile
15
Dockerfile
@@ -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
266
README.md
@@ -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
14
build.sh
Normal file → Executable 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
298
minify-build.js
Normal 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 };
|
||||||
@@ -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
22
package.json
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
@@ -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><mascot here? :D >
|
<p><mascot here? :D >
|
||||||
<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>
|
||||||
73
public/site-scripts/theme-toggle.js
Normal file
73
public/site-scripts/theme-toggle.js
Normal 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);
|
||||||
|
}
|
||||||
|
})();
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user