From feefd59b80f0c5096aaba183030211fa71dfd398 Mon Sep 17 00:00:00 2001 From: counterweight Date: Wed, 24 Dec 2025 10:22:13 +0100 Subject: [PATCH] building page --- .gitignore | 5 + README.md | 17 +++ build.py | 96 ++++++++++++++ config/services.json | 28 ++++ src/index.html.template | 36 ++++++ src/styles.css | 274 ++++++++++++++++++++++++++++++++++++++++ 6 files changed, 456 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100755 build.py create mode 100644 config/services.json create mode 100644 src/index.html.template create mode 100644 src/styles.css diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3ca3e45 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +dist/ +*.pyc +__pycache__/ +.DS_Store + diff --git a/README.md b/README.md new file mode 100644 index 0000000..0bec202 --- /dev/null +++ b/README.md @@ -0,0 +1,17 @@ +# Bitcoin Services Home + +Static site generator for sharing self-hosted service information. + +## Usage + +```bash +python3 build.py +``` + +Edit `config/services.json` to customize content. Output is generated in `dist/`. + +## Structure + +- `src/` - Template and styles +- `config/services.json` - Site configuration +- `dist/` - Generated output (gitignored) diff --git a/build.py b/build.py new file mode 100755 index 0000000..29307e9 --- /dev/null +++ b/build.py @@ -0,0 +1,96 @@ +#!/usr/bin/env python3 +""" +Simple build script for static site generation. +Replaces template variables with values from config/services.json +""" + +import json +import os +from datetime import datetime +from pathlib import Path + +# Paths +BASE_DIR = Path(__file__).parent +TEMPLATE_DIR = BASE_DIR / "src" +CONFIG_FILE = BASE_DIR / "config" / "services.json" +OUTPUT_DIR = BASE_DIR / "dist" + +def load_config(): + """Load configuration from JSON file""" + with open(CONFIG_FILE, 'r') as f: + return json.load(f) + +def generate_services_html(services): + """Generate HTML for services list""" + html = "" + for service in services: + # Handle URLs that may or may not have protocol + url = service['url'] + if not url.startswith(('http://', 'https://')): + href = f"https://{url}" + else: + href = url + # Display URL without protocol for cleaner look + url = url.replace('https://', '').replace('http://', '') + + html += f""" +
+
{service['name']}
+
{service['description']}
+
+ URL: {url} +
+
+ """ + return html.strip() + +def build_site(): + """Main build function""" + # Load config + config = load_config() + + # Read template + template_path = TEMPLATE_DIR / "index.html.template" + with open(template_path, 'r') as f: + template = f.read() + + # Generate services HTML + services_html = generate_services_html(config['services']) + + # Prepare replacements + replacements = { + '{{SITE_TITLE}}': config['site']['title'], + '{{SITE_SUBTITLE}}': config['site']['subtitle'], + '{{SITE_DESCRIPTION}}': config['site']['description'], + '{{SERVICES_LIST}}': services_html, + '{{FOOTER_TEXT}}': config['footer']['text'], + '{{BUILD_TIMESTAMP}}': datetime.now().strftime('%Y-%m-%d %H:%M:%S UTC') + } + + # Replace variables + output = template + for placeholder, value in replacements.items(): + output = output.replace(placeholder, value) + + # Create output directory + OUTPUT_DIR.mkdir(exist_ok=True) + + # Write output + output_path = OUTPUT_DIR / "index.html" + with open(output_path, 'w') as f: + f.write(output) + + # Copy CSS file + css_source = TEMPLATE_DIR / "styles.css" + css_dest = OUTPUT_DIR / "styles.css" + if css_source.exists(): + with open(css_source, 'r') as src, open(css_dest, 'w') as dst: + dst.write(src.read()) + + print(f"✓ Site built successfully!") + print(f"✓ Output: {output_path}") + print(f"✓ CSS copied to: {css_dest}") + +if __name__ == "__main__": + build_site() + diff --git a/config/services.json b/config/services.json new file mode 100644 index 0000000..a6d1b67 --- /dev/null +++ b/config/services.json @@ -0,0 +1,28 @@ +{ + "site": { + "title": "COUNTER BITCOIN SERVICES", + "subtitle": "Self-Hosted Infrastructure Available for the Public", + "description": "Welcome to my self-hosted services Bitcoin hub. I run this for myself, but you are welcome to use it as well free of cost.\nIf you feel like helping pay the bills, you can send some sats to infra@wallet.contrapeso.xyz." + }, + "services": [ + { + "name": "Bitcoin Node", + "description": "Full Bitcoin Knots node. You can add it as a peer to your own node.", + "url": "https://bitcoin.contrapeso.xyz:8333" + }, + { + "name": "Electrum Server", + "description": "Running Fulcrum. You can point your wallet to it. Anon logs, your IP and TXIDs won't be logged.", + "url": "https://electrum.contrapeso.xyz:50002" + }, + { + "name": "Mempool Explorer", + "description": "Mempool visualization and block explorer", + "url": "https://mempool.contrapeso.xyz" + } + ], + "footer": { + "text": "Code is law." + } +} + diff --git a/src/index.html.template b/src/index.html.template new file mode 100644 index 0000000..1c13000 --- /dev/null +++ b/src/index.html.template @@ -0,0 +1,36 @@ + + + + + + {{SITE_TITLE}} + + + +
+
+

[ {{SITE_TITLE}} ]

+

{{SITE_SUBTITLE}}

+
+ +
+
+

{{SITE_DESCRIPTION}}

+
+ +
+

[ SERVICES ]

+
+ {{SERVICES_LIST}} +
+
+
+ + +
+ + + diff --git a/src/styles.css b/src/styles.css new file mode 100644 index 0000000..0c96114 --- /dev/null +++ b/src/styles.css @@ -0,0 +1,274 @@ +@import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;700&display=swap'); + +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +:root { + --bg-primary: #0a0a0a; + --bg-secondary: #1a1a1a; + --text-primary: #FFD700; + --text-secondary: #FFA500; + --text-muted: #666666; + --accent: #FFD700; + --border: #333333; + --glow: rgba(255, 215, 0, 0.3); +} + +body { + font-family: 'JetBrains Mono', monospace; + background-color: var(--bg-primary); + color: var(--text-primary); + line-height: 1.6; + padding: 20px; + min-height: 100vh; +} + +.container { + max-width: 900px; + margin: 0 auto; + background-color: var(--bg-secondary); + border: 1px solid var(--border); + padding: 40px; + box-shadow: 0 0 20px var(--glow); +} + +header { + margin-bottom: 40px; +} + +.glitch { + font-size: 1.2em; + font-weight: 700; + text-transform: uppercase; + color: var(--accent); + letter-spacing: 2px; + margin-bottom: 20px; + border-bottom: 1px solid var(--border); + padding-bottom: 10px; + text-align: left; +} + +.subtitle { + color: var(--text-secondary); + font-size: 0.9em; + margin-top: 10px; + text-transform: uppercase; + letter-spacing: 2px; + text-align: left; +} + +.intro { + margin-bottom: 40px; +} + +.ascii-art { + color: var(--text-secondary); + font-size: 0.7em; + line-height: 1.2; + margin: 20px 0; + white-space: pre; + overflow-x: auto; +} + +.description { + color: var(--text-primary); + margin-top: 20px; + padding: 15px; + border-left: 3px solid var(--accent); + background-color: rgba(255, 215, 0, 0.05); +} + +section { + margin-bottom: 40px; +} + +h2 { + color: var(--accent); + font-size: 1.2em; + margin-bottom: 20px; + text-transform: uppercase; + letter-spacing: 2px; + border-bottom: 1px solid var(--border); + padding-bottom: 10px; +} + +.services { + padding: 20px; + background-color: var(--bg-primary); + border: 1px solid var(--border); +} + +.services-grid { + display: grid; + gap: 20px; +} + +.service-card { + background-color: var(--bg-primary); + border: 1px solid var(--border); + padding: 20px; + transition: all 0.3s ease; +} + +.service-card:hover { + border-color: var(--text-primary); + box-shadow: 0 0 15px var(--glow); + transform: translateX(5px); +} + +.service-name { + color: var(--accent); + font-weight: 700; + font-size: 1.1em; + margin-bottom: 10px; + text-transform: uppercase; +} + +.service-description { + color: var(--text-secondary); + margin-bottom: 15px; + font-size: 0.9em; +} + +.service-info { + font-family: 'JetBrains Mono', monospace; + font-size: 0.85em; +} + +.service-info code { + background-color: var(--bg-secondary); + padding: 2px 6px; + border: 1px solid var(--border); + color: var(--text-primary); +} + +.service-link { + color: var(--text-primary); + text-decoration: none; + border-bottom: 1px dotted var(--text-primary); + transition: color 0.3s ease; +} + +.service-link:hover { + color: var(--accent); + border-bottom-color: var(--accent); +} + +.contact { + padding: 20px; + background-color: var(--bg-primary); + border: 1px solid var(--border); +} + +.contact p { + color: var(--text-secondary); + font-size: 0.9em; +} + +footer { + margin-top: 40px; + padding-top: 20px; + border-top: 1px solid var(--border); + text-align: center; +} + +.footer-text { + color: var(--text-muted); + font-size: 0.8em; + margin-bottom: 10px; +} + +.timestamp { + color: var(--text-muted); + font-size: 0.75em; + font-style: italic; +} + +/* Responsive */ +@media (max-width: 768px) { + body { + padding: 10px; + } + + .container { + padding: 15px; + } + + header { + margin-bottom: 30px; + } + + .glitch { + font-size: 1.1em; + margin-bottom: 15px; + padding-bottom: 8px; + } + + .subtitle { + font-size: 0.8em; + margin-top: 8px; + } + + section { + margin-bottom: 30px; + } + + h2 { + font-size: 1em; + margin-bottom: 15px; + padding-bottom: 8px; + } + + .description { + margin-top: 15px; + padding: 12px; + font-size: 0.9em; + } + + .services { + padding: 15px; + } + + .services-grid { + gap: 15px; + } + + .service-card { + padding: 15px; + } + + .service-name { + font-size: 1em; + margin-bottom: 8px; + } + + .service-description { + font-size: 0.85em; + margin-bottom: 12px; + } + + .service-info { + font-size: 0.8em; + } + + .contact { + padding: 15px; + } + + .contact p { + font-size: 0.85em; + } + + footer { + margin-top: 30px; + padding-top: 15px; + } + + .ascii-art { + font-size: 0.6em; + } +} +