building page
This commit is contained in:
commit
feefd59b80
6 changed files with 456 additions and 0 deletions
5
.gitignore
vendored
Normal file
5
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
dist/
|
||||||
|
*.pyc
|
||||||
|
__pycache__/
|
||||||
|
.DS_Store
|
||||||
|
|
||||||
17
README.md
Normal file
17
README.md
Normal file
|
|
@ -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)
|
||||||
96
build.py
Executable file
96
build.py
Executable file
|
|
@ -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"""
|
||||||
|
<div class="service-card">
|
||||||
|
<div class="service-name">{service['name']}</div>
|
||||||
|
<div class="service-description">{service['description']}</div>
|
||||||
|
<div class="service-info">
|
||||||
|
<strong>URL:</strong> <a href="{href}" class="service-link" target="_blank">{url}</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
"""
|
||||||
|
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()
|
||||||
|
|
||||||
28
config/services.json
Normal file
28
config/services.json
Normal file
|
|
@ -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."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
36
src/index.html.template
Normal file
36
src/index.html.template
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>{{SITE_TITLE}}</title>
|
||||||
|
<link rel="stylesheet" href="styles.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<header>
|
||||||
|
<h1 class="glitch" data-text="{{SITE_TITLE}}">[ {{SITE_TITLE}} ]</h1>
|
||||||
|
<p class="subtitle">{{SITE_SUBTITLE}}</p>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main>
|
||||||
|
<section class="intro">
|
||||||
|
<p class="description">{{SITE_DESCRIPTION}}</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="services">
|
||||||
|
<h2>[ SERVICES ]</h2>
|
||||||
|
<div class="services-grid">
|
||||||
|
{{SERVICES_LIST}}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<footer>
|
||||||
|
<p class="footer-text">{{FOOTER_TEXT}}</p>
|
||||||
|
<p class="timestamp">Last updated: {{BUILD_TIMESTAMP}}</p>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
||||||
274
src/styles.css
Normal file
274
src/styles.css
Normal file
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue