From f23531dd70ee31d438afef98c7c4b04e82b33b9a Mon Sep 17 00:00:00 2001 From: Michael DiLeo Date: Wed, 13 Aug 2025 19:10:31 -0500 Subject: [PATCH 01/18] first template launch --- .gitignore | 28 +++ Dockerfile | 15 +- README.md | 266 ++++++++++++++++++++++++- build.sh | 14 +- minify-build.js | 298 ++++++++++++++++++++++++++++ nginx.conf | 3 +- package.json | 22 ++ public/about.html | 83 ++++++++ public/index.html | 56 ++++-- public/site-scripts/theme-toggle.js | 73 +++++++ public/site-styles/style.css | 67 ++++++- 11 files changed, 902 insertions(+), 23 deletions(-) create mode 100644 .gitignore mode change 100644 => 100755 build.sh create mode 100644 minify-build.js create mode 100644 package.json create mode 100644 public/site-scripts/theme-toggle.js diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..15aeb6b --- /dev/null +++ b/.gitignore @@ -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* diff --git a/Dockerfile b/Dockerfile index 0aae48a..798b6ad 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,13 +1,24 @@ 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 public/ /usr/share/nginx/html/ +COPY dist/ /usr/share/nginx/html/ RUN chown -R nginx:nginx /usr/share/nginx/html \ && chmod -R 755 /usr/share/nginx/html - HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \ CMD wget --no-verbose --tries=1 --spider http://localhost/ || exit 1 diff --git a/README.md b/README.md index da7fbe1..84d0cb7 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,265 @@ -# Keyboard-Vagabond-Web +# Keyboard Vagabond Web -static website landing page for Keyboard Vagabond \ No newline at end of file +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 +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! πŸš€** \ No newline at end of file diff --git a/build.sh b/build.sh old mode 100644 new mode 100755 index 4a139f0..ed28778 --- a/build.sh +++ b/build.sh @@ -3,10 +3,22 @@ set -e REGISTRY="registry.keyboardvagabond.com" VERSION="latest" PLATFORM="linux/arm64" -IMAGE_NAME="keyboard-vagabond-landing" +IMAGE_NAME="keyboard-vagabond-web" 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 \ --platform $PLATFORM \ --tag $REGISTRY/library/$IMAGE_NAME:$VERSION \ diff --git a/minify-build.js b/minify-build.js new file mode 100644 index 0000000..b2ab660 --- /dev/null +++ b/minify-build.js @@ -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(//g, '') + .replace(//g, '') + .replace(//g, + '') + .replace(/<\/body>/g, ''); + + 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 }; diff --git a/nginx.conf b/nginx.conf index 2b62a85..6569dcf 100644 --- a/nginx.conf +++ b/nginx.conf @@ -1,6 +1,5 @@ -user nginx; worker_processes auto; -pid /var/run/nginx.pid; +pid /tmp/nginx.pid; events { worker_connections 1024; diff --git a/package.json b/package.json new file mode 100644 index 0000000..b79d111 --- /dev/null +++ b/package.json @@ -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" + } +} diff --git a/public/about.html b/public/about.html index e69de29..b5cc577 100644 --- a/public/about.html +++ b/public/about.html @@ -0,0 +1,83 @@ + + + + + + + + + + Keyboard Vagabond - About + + +
+ +
+ + +
+

About Keyboard Vagabond

+

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.

+ +

Why Keyboard Vagabond

+

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.

+ +

What to expect and commitments

+

Moderation style - + 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.

+

Sign ups - + Sign ups require manual approval to prevent spam.

+

Data protection - + 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.

+

Should shutdown happen - + There will be a 3 month announcement in advance, in accordance with the Mastodon Server Covenant.

+

Funding - + 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.

+

The Dirty Technicals

+

If you're not a mega-nerd, turn back now.

+

I warned you.

+

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.

+

The Specs

+

Servers - 3x 10 ARM vCPUs, 16GB Ram, 500GB (~50GB for Talos and the rest for Longhorn) storage running Talos and Kubernetes. +

Storage - Longhorn ensures that there are at least 2 copies across the nodes.

+

Backups and Content - 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.

+

CDN - CloudFlare provides CDN and special rules have been set up to be sure that as much as possible is cached.

+

Security - ports are closed off to the world and secured with CloudFlare tunnels and TailScale as the only means of access outside of website access.

+

Observability and Logging - OpenObserve dashboards and log aggregation.

+

Domain - domain is provided by CloudFlare

+

Services - 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.

+

Source Code - 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. :)

+

Costs
+ VPS servers - 3x ~$13 / mth = $40/mth
+ Domain name - $12/year
+ Backblaze - $6/TB/mth = ~$2/mth
+ Total: ~$45/mth

+
+ + + + + + \ No newline at end of file diff --git a/public/index.html b/public/index.html index cfd52fd..ac5a6d6 100644 --- a/public/index.html +++ b/public/index.html @@ -1,24 +1,41 @@ - + - + - Hello world! + Keyboard Vagabond -
- +
+
@@ -30,7 +47,7 @@

<mascot here? :D > mascot intro and why it was chosen

-

Here, you are a member, not just a user - we want to create community in this space, being +

Here, you are a member, not just a user - 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.

What you'll find here

@@ -67,12 +84,13 @@

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, if you enable it, be visible on Mastodon and people can follow your blog account.

-

Check out this amazing video by Elena Rossini. You can follow her +

Check out this amazing four minute video by Elena Rossini. You can follow her on Mastodon by searching @elena@aseachange.com.

+ sandbox="allow-same-origin allow-scripts allow-popups allow-forms" + loading="lazy">

How to get started in the Fediverse

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 @@

Popular places to get started are:

Lemmy.zip for Lemmy or Piefed.social for PieFed, which is the software that this - community runs. Like Reddit, you can browse + community runs.

Mastodon.social - the largest mastodon instance. Search for hashtags or check out whats trending and follow them to create your timeline.

@@ -101,18 +119,32 @@
Check out that Explore button on the main page. Explore button on main page + alt="Explore button on main page" loading="lazy" />
Visit the Posts, Hashtags, and News tabs to see what's on the server Posts, Hashtags, and News tabs + alt="Posts, Hashtags, and News tabs" loading="lazy" /> +
+
+ 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. + + Communities, Local Communities +
+
+ Scroll all the way to the bottom to find local topics. Topics are groups of communities. You can suggest more in the Meta community. + Local topics
+ + \ No newline at end of file diff --git a/public/site-scripts/theme-toggle.js b/public/site-scripts/theme-toggle.js new file mode 100644 index 0000000..202b7b2 --- /dev/null +++ b/public/site-scripts/theme-toggle.js @@ -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); + } +})(); \ No newline at end of file diff --git a/public/site-styles/style.css b/public/site-styles/style.css index 4d5b563..0af26de 100644 --- a/public/site-styles/style.css +++ b/public/site-styles/style.css @@ -17,8 +17,9 @@ .banner-title { position: absolute; - top: var(--pico-typography-spacing-vertical); + top: var(--pico-spacing); left: var(--pico-spacing); + right: var(--pico-spacing); color: white; /* Keep white for banner overlay readability */ text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.7); margin: 0; @@ -42,11 +43,47 @@ 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) { .banner-title { font-size: 1.8rem; top: calc(var(--pico-typography-spacing-vertical) * 0.5); left: var(--pico-spacing); + right: var(--pico-spacing); } } @@ -151,8 +188,8 @@ } } -/* Make headers use primary green color */ -:root { +/* Make headers use primary green color - only for light theme and default */ +:root:not([data-theme="dark"]) { --pico-h1-color: var(--pico-primary); --pico-h2-color: var(--pico-primary); --pico-h3-color: var(--pico-primary); @@ -175,4 +212,26 @@ /* Test: Force green color on ALL headings */ /* h1, h2, h3, h4, h5, h6 { color: #2d5016 !important; -} */ \ No newline at end of file +} */ + +/* 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; +} \ No newline at end of file From f739f87ce42e3b2075a6720e4c9b681814f14823 Mon Sep 17 00:00:00 2001 From: Michael DiLeo Date: Thu, 14 Aug 2025 09:11:44 -0500 Subject: [PATCH 02/18] swap to jade theme, fix missing anchor tags --- minify-build.js | 26 ++++++++++++++++---------- public/site-styles/style.css | 12 +++++++++++- 2 files changed, 27 insertions(+), 11 deletions(-) diff --git a/minify-build.js b/minify-build.js index b2ab660..22ecbee 100644 --- a/minify-build.js +++ b/minify-build.js @@ -35,12 +35,11 @@ async function createOptimizedBuild() { const purgeResults = await new PurgeCSS().purge({ content: [ 'public/index.html', - 'public/about.html', - 'index.html' + 'public/about.html' ], css: [ - 'public/css/pico.green.min.css', - 'public/css/pico.colors.min.css' + 'public/css/pico.jade.css' + // 'public/css/pico.colors.min.css' ], safelist: { standard: [ @@ -51,7 +50,8 @@ async function createOptimizedBuild() { 'banner-subtitle', 'video-container', 'photo-gallery', - 'photo-item' + 'photo-item', + 'dark' ], deep: [ /^--pico-/, @@ -75,19 +75,25 @@ async function createOptimizedBuild() { /^ul$/, /^li$/, /^strong$/, - /^a$/ + /^a$/, + /^a:/, // Add anchor pseudo-classes like a:hover, a:focus + /^a\./, // Add anchor with classes like a.secondary + /^a\[/ // Add anchor with attributes like a[role=button] ] }, variables: true, keyframes: true }); - // Step 1.5: Add essential font rule that PurgeCSS removes too aggressively + // Step 1.5: Add essential rules 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);}`; + + // Add missing anchor tag primary color rule + const anchorRule = `a:not([role=button]){--pico-color:var(--pico-primary);--pico-underline:var(--pico-primary-underline);background-color:transparent;color:var(--pico-color);text-decoration:var(--pico-text-decoration);text-decoration-color:var(--pico-underline);text-underline-offset:.125em;transition:background-color var(--pico-transition),color var(--pico-transition),text-decoration var(--pico-transition),box-shadow var(--pico-transition);}a:not([role=button]):is(:hover,:active,:focus){--pico-color:var(--pico-primary-hover);--pico-underline:var(--pico-primary-hover-underline);text-decoration:underline;}`; // 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 combinedCSS = purgeResults.map(result => result.css).join('\n\n') + '\n' + fontRule + '\n' + anchorRule; const minifiedCSS = await postcss([ cssnano({ @@ -156,7 +162,7 @@ async function createOptimizedBuild() { // Calculate JavaScript size if bundled let jsInfo = ''; try { - const bundledJSSize = (await fs.stat('dist/scripts/scripts.js')).size; + const bundledJSSize = (await fs.stat('dist/scripts/bundle.js')).size; jsInfo = `\n JavaScript: Bundled into ${(bundledJSSize / 1024).toFixed(1)}KB`; } catch (err) { // No JavaScript files were bundled @@ -269,7 +275,7 @@ async function minifyHTMLFile(inputPath, outputPath) { .replace(//g, '') .replace(//g, '') - .replace(/<\/body>/g, ''); + .replace(/<\/body>/g, ''); const minified = await htmlMinify(updatedHTML, { collapseWhitespace: true, diff --git a/public/site-styles/style.css b/public/site-styles/style.css index 0af26de..bc3b9c1 100644 --- a/public/site-styles/style.css +++ b/public/site-styles/style.css @@ -188,7 +188,7 @@ } } -/* Make headers use primary green color - only for light theme and default */ +/* Make headers use primary green color for both themes */ :root:not([data-theme="dark"]) { --pico-h1-color: var(--pico-primary); --pico-h2-color: var(--pico-primary); @@ -198,6 +198,16 @@ --pico-h6-color: var(--pico-primary); } +/* Make headers use primary color in dark theme too */ +[data-theme="dark"] { + --pico-h1-color: var(--pico-primary); + --pico-h2-color: var(--pico-primary); + --pico-h3-color: var(--pico-primary); + --pico-h4-color: var(--pico-primary); + --pico-h5-color: var(--pico-primary); + --pico-h6-color: var(--pico-primary); +} + /* Custom header colors for green theme - light mode */ /* [data-theme="light"], :root:not([data-theme="dark"]) { From 4e22a6c687af64f8e3eb6c5d5c8e14602604009a Mon Sep 17 00:00:00 2001 From: Michael DiLeo Date: Thu, 14 Aug 2025 09:45:19 -0500 Subject: [PATCH 03/18] disable caching - should lower cached value later --- nginx.conf | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/nginx.conf b/nginx.conf index 6569dcf..8e605a8 100644 --- a/nginx.conf +++ b/nginx.conf @@ -40,14 +40,21 @@ http { add_header X-XSS-Protection "1; mode=block" always; add_header Referrer-Policy "strict-origin-when-cross-origin" always; + # Temporary: Disable caching during development location ~* \.(css|js|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ { - expires 1w; - add_header Cache-Control "public, immutable"; + # expires 1w; + # add_header Cache-Control "public, immutable"; + add_header Cache-Control "no-cache, no-store, must-revalidate"; + add_header Pragma "no-cache"; + add_header Expires "0"; } location ~* \.html$ { - expires 1h; - add_header Cache-Control "public"; + # expires 1h; + # add_header Cache-Control "public"; + add_header Cache-Control "no-cache, no-store, must-revalidate"; + add_header Pragma "no-cache"; + add_header Expires "0"; } location /health { From b8d1c816207d4d8d3004576524014c2a561dfdcf Mon Sep 17 00:00:00 2001 From: Michael DiLeo Date: Thu, 14 Aug 2025 09:53:09 -0500 Subject: [PATCH 04/18] switch to single bundle file --- minify-build.js | 24 ++++++++++++------------ public/index.html | 3 +-- 2 files changed, 13 insertions(+), 14 deletions(-) diff --git a/minify-build.js b/minify-build.js index 22ecbee..c8cc359 100644 --- a/minify-build.js +++ b/minify-build.js @@ -109,20 +109,21 @@ async function createOptimizedBuild() { }) ]).process(combinedCSS, { from: undefined }); - await fs.writeFile('dist/css/styles.css', minifiedCSS.css); - - // Also minify custom site styles + // Combine with custom site styles const customCSS = await fs.readFile('public/site-styles/style.css', 'utf8'); - const minifiedCustomCSS = await postcss([ + const combinedWithCustomCSS = minifiedCSS.css + '\n' + customCSS; + + // Minify the combined CSS bundle + const finalMinifiedCSS = await postcss([ cssnano({ preset: ['default', { discardComments: { removeAll: true }, normalizeWhitespace: true }] }) - ]).process(customCSS, { from: undefined }); + ]).process(combinedWithCustomCSS, { from: undefined }); - await fs.writeFile('dist/site-styles/style.css', minifiedCustomCSS.css); + await fs.writeFile('dist/css/bundle.css', finalMinifiedCSS.css); // Step 3: JavaScript Bundling and Minification console.log('πŸ“¦ Step 3: Bundling and minifying JavaScript...'); @@ -148,9 +149,9 @@ async function createOptimizedBuild() { } // 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 originalCSSSize = (await fs.stat('public/css/pico.jade.min.css')).size + + (await fs.stat('public/css/pico.min.css')).size; + const optimizedCSSSize = (await fs.stat('dist/css/bundle.css')).size; const cssReduction = ((originalCSSSize - optimizedCSSSize) / originalCSSSize * 100).toFixed(1); const originalHTMLSize = (await fs.stat('public/index.html')).size + @@ -271,10 +272,9 @@ async function minifyHTMLFile(inputPath, outputPath) { // Update CSS references for production and add script reference let updatedHTML = html - .replace(//g, '') - .replace(//g, '') + .replace(//g, '') .replace(//g, - '') + '') .replace(/<\/body>/g, ''); const minified = await htmlMinify(updatedHTML, { diff --git a/public/index.html b/public/index.html index ac5a6d6..4b50dcd 100644 --- a/public/index.html +++ b/public/index.html @@ -5,8 +5,7 @@ - - + Keyboard Vagabond From 77b43080a0feba7ac2c9470c4914dfa86f9d97ab Mon Sep 17 00:00:00 2001 From: Michael DiLeo Date: Thu, 14 Aug 2025 10:47:48 -0500 Subject: [PATCH 05/18] about page gets the stylesheets --- public/about.html | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/public/about.html b/public/about.html index b5cc577..1842af5 100644 --- a/public/about.html +++ b/public/about.html @@ -4,8 +4,7 @@ - - + Keyboard Vagabond - About From f316a049a6f9edc8f2862b43fa614a0241a961a1 Mon Sep 17 00:00:00 2001 From: Michael DiLeo Date: Thu, 14 Aug 2025 14:41:40 -0500 Subject: [PATCH 06/18] collapse nav items on smaller screens --- minify-build.js | 27 ++++++++++-- public/about.html | 22 +++++++++- public/index.html | 23 +++++++++- public/site-styles/style.css | 83 ++++++++++++++++++++++++++++++++++++ 4 files changed, 147 insertions(+), 8 deletions(-) diff --git a/minify-build.js b/minify-build.js index c8cc359..bd51d3d 100644 --- a/minify-build.js +++ b/minify-build.js @@ -51,7 +51,14 @@ async function createOptimizedBuild() { 'video-container', 'photo-gallery', 'photo-item', - 'dark' + 'dark', + // Responsive navigation classes + 'desktop-nav', + 'mobile-nav', + 'dropdown', + // Button classes + 'secondary', + 'outline' ], deep: [ /^--pico-/, @@ -60,7 +67,12 @@ async function createOptimizedBuild() { /:hover/, /:focus/, /:active/, - /:visited/ + /:visited/, + // Dropdown and interactive states + /\[open\]/, + /\[role=/, + /details/, + /summary/ ], // Keep base typography selectors that are essential for proper font rendering greedy: [ @@ -78,7 +90,14 @@ async function createOptimizedBuild() { /^a$/, /^a:/, // Add anchor pseudo-classes like a:hover, a:focus /^a\./, // Add anchor with classes like a.secondary - /^a\[/ // Add anchor with attributes like a[role=button] + /^a\[/, // Add anchor with attributes like a[role=button] + // Dropdown and button elements + /^details$/, + /^summary$/, + /^button$/, + /^button:/, + /^button\./, + /^button\[/ ] }, variables: true, @@ -239,7 +258,7 @@ async function bundleAndMinifyJS() { hoist_vars: false, if_return: true, join_vars: true, - cascade: true, + side_effects: true }, mangle: { diff --git a/public/about.html b/public/about.html index 1842af5..b305bfd 100644 --- a/public/about.html +++ b/public/about.html @@ -11,14 +11,32 @@