Rewrite landing page in Rust + WebAssembly

- Complete rewrite from HTML/CSS/JS to Rust + WASM
- Uses vanilla wasm-bindgen with web-sys (no framework)
- Minimal bundle size: ~19KB WASM + 9KB JS
- Modern, clean redesign with dark theme
- Local builds, dist files committed for fast CI

Technical changes:
- Added Cargo.toml with wasm-bindgen, web-sys dependencies
- Implemented landing page in src/lib.rs using web_sys DOM API
- Created minimal index.html shell for WASM loading
- Designed modern CSS with responsive layout
- Archived old HTML files to html-old/
- Updated Dockerfile to copy dist/ instead of html/
- Updated build process in README.md
- Documented migration in CHANGELOG.md

Build process:
1. cargo build --release --target wasm32-unknown-unknown
2. wasm-bindgen --out-dir dist --target web
3. Copy static assets to dist/
4. Commit dist/ to git for fast CI deployment

Benefits:
- Near-native performance
- Type safety from Rust
- Smaller bundle size
- Modern web technology stack
This commit is contained in:
rail 2025-10-25 23:14:46 +03:00
parent ad2a586de2
commit 15a18e45b4
17 changed files with 1338 additions and 18 deletions

5
.cargo/config.toml Normal file
View File

@ -0,0 +1,5 @@
[build]
target = "wasm32-unknown-unknown"
[target.wasm32-unknown-unknown]
rustflags = ["-C", "link-arg=-s"]

18
.gitignore vendored
View File

@ -1,2 +1,20 @@
# Environment files
.env .env
.env.bak .env.bak
# Rust build artifacts
target/
Cargo.lock
# Editor files
.vscode/
.idea/
*.swp
*.swo
*~
# OS files
.DS_Store
Thumbs.db
# Note: We commit dist/ since we build locally for faster CI

28
Cargo.toml Normal file
View File

@ -0,0 +1,28 @@
[package]
name = "railwayka-landing"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib"]
[dependencies]
wasm-bindgen = "0.2.104"
wasm-bindgen-futures = "0.4.54"
[dependencies.web-sys]
version = "0.3.81"
features = [
"console",
"Document",
"Element",
"HtmlElement",
"Node",
"Window",
"Location",
]
[profile.release]
opt-level = "z"
lto = true
codegen-units = 1

View File

@ -3,4 +3,4 @@ ARG IMAGE_VER=1.27-alpine
FROM --platform=$BUILDPLATFORM ${IMAGE}:${IMAGE_VER} FROM --platform=$BUILDPLATFORM ${IMAGE}:${IMAGE_VER}
COPY html /usr/share/nginx/html COPY dist /usr/share/nginx/html

View File

@ -1,18 +1,22 @@
# railwayka.ru Landing Page # railwayka.ru Landing Page
Business card website for railwayka.ru infrastructure, deployed via [dcape](https://github.com/dopos/dcape) GitOps platform. Modern business card website for railwayka.ru infrastructure, built with **Rust + WebAssembly** and deployed via [dcape](https://github.com/dopos/dcape) GitOps platform.
## Features ## Features
- Modern dark theme with animated stars background - 🦀 **Rust + WebAssembly** - High-performance, compiled to WASM
- Modern dark theme with clean design
- Service cards showcasing current and planned services - Service cards showcasing current and planned services
- Technology stack descriptions - Technology stack descriptions
- GitOps workflow visualization - Fully responsive layout
- Konami code easter egg - Minimal bundle size (~19KB WASM)
- Responsive design
## Technology Stack ## Technology Stack
- **Rust**: Core application logic
- **WebAssembly**: Fast, portable binary format
- **wasm-bindgen**: Rust-WASM bindings
- **web-sys**: Web API bindings for Rust
- **Nginx**: Alpine-based web server - **Nginx**: Alpine-based web server
- **Docker**: Containerized deployment - **Docker**: Containerized deployment
- **Traefik**: Automatic HTTPS via Let's Encrypt - **Traefik**: Automatic HTTPS via Let's Encrypt
@ -44,32 +48,75 @@ make docker-build
make up make up
``` ```
## Local Development ## Building from Source
To run locally: ### Prerequisites
- Rust (latest stable) - install from [rustup.rs](https://rustup.rs/)
- wasm-bindgen-cli - `cargo install wasm-bindgen-cli`
- wasm32 target - `rustup target add wasm32-unknown-unknown`
### Build Steps
```bash ```bash
# Serve files with any web server # 1. Build Rust to WASM
cd html cargo build --release --target wasm32-unknown-unknown
# 2. Generate JS bindings
wasm-bindgen --out-dir dist --target web \
target/wasm32-unknown-unknown/release/railwayka_landing.wasm
# 3. Copy static assets
cp index.html style.css dist/
# 4. Serve locally
cd dist
python3 -m http.server 8000 python3 -m http.server 8000
# Visit http://localhost:8000 # Visit http://localhost:8000
``` ```
## Structure ### Quick Build Script
For convenience, you can use this one-liner:
```bash
cargo build --release --target wasm32-unknown-unknown && \
wasm-bindgen --out-dir dist --target web target/wasm32-unknown-unknown/release/railwayka_landing.wasm && \
cp index.html style.css dist/
```
## Project Structure
``` ```
. .
├── Makefile # Dcape app Makefile ├── Cargo.toml # Rust project manifest
├── docker-compose.yml # Service definition ├── .cargo/
├── Dockerfile # Custom nginx image │ └── config.toml # Rust build configuration
├── .woodpecker.yml # CI/CD pipeline ├── src/
├── html/ # Static website files │ └── lib.rs # Main Rust/WASM code
├── index.html # HTML shell
├── style.css # Styling
├── dist/ # Built artifacts (committed to git)
│ ├── index.html │ ├── index.html
│ ├── style.css │ ├── style.css
│ └── script.js │ ├── railwayka_landing.js
└── README.md │ ├── railwayka_landing_bg.wasm
│ └── *.d.ts
├── html-old/ # Archived old HTML version
├── Makefile # Dcape app Makefile
├── docker-compose.yml # Service definition
├── Dockerfile # Nginx deployment
└── .woodpecker.yml # CI/CD pipeline
``` ```
## Why Rust + WASM?
- **Performance**: Near-native execution speed
- **Size**: Optimized bundle (~19KB WASM + 9KB JS)
- **Type Safety**: Rust's type system prevents common bugs
- **Modern**: Cutting-edge web technology
- **Learning**: Great opportunity to explore WASM ecosystem
## License ## License
MIT MIT

35
dist/index.html vendored Normal file
View File

@ -0,0 +1,35 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="railwayka.ru - Modern self-hosted infrastructure powered by GitOps">
<title>railwayka.ru</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<div id="loading">
<div class="loader">Loading...</div>
</div>
<script type="module">
import init from './railwayka_landing.js';
async function run() {
try {
await init();
// Remove loading indicator
const loading = document.getElementById('loading');
if (loading) {
loading.remove();
}
} catch (error) {
console.error('Failed to initialize WASM:', error);
document.body.innerHTML = '<div class="error">Failed to load application. Please refresh the page.</div>';
}
}
run();
</script>
</body>
</html>

33
dist/railwayka_landing.d.ts vendored Normal file
View File

@ -0,0 +1,33 @@
/* tslint:disable */
/* eslint-disable */
export function main(): void;
export type InitInput = RequestInfo | URL | Response | BufferSource | WebAssembly.Module;
export interface InitOutput {
readonly memory: WebAssembly.Memory;
readonly main: () => void;
readonly __wbindgen_export_0: (a: number) => void;
readonly __wbindgen_start: () => void;
}
export type SyncInitInput = BufferSource | WebAssembly.Module;
/**
* Instantiates the given `module`, which can either be bytes or
* a precompiled `WebAssembly.Module`.
*
* @param {{ module: SyncInitInput }} module - Passing `SyncInitInput` directly is deprecated.
*
* @returns {InitOutput}
*/
export function initSync(module: { module: SyncInitInput } | SyncInitInput): InitOutput;
/**
* If `module_or_path` is {RequestInfo} or {URL}, makes a request and
* for everything else, calls `WebAssembly.instantiate` directly.
*
* @param {{ module_or_path: InitInput | Promise<InitInput> }} module_or_path - Passing `InitInput` directly is deprecated.
*
* @returns {Promise<InitOutput>}
*/
export default function __wbg_init (module_or_path?: { module_or_path: InitInput | Promise<InitInput> } | InitInput | Promise<InitInput>): Promise<InitOutput>;

275
dist/railwayka_landing.js vendored Normal file
View File

@ -0,0 +1,275 @@
let wasm;
let heap = new Array(128).fill(undefined);
heap.push(undefined, null, true, false);
function getObject(idx) { return heap[idx]; }
let heap_next = heap.length;
function addHeapObject(obj) {
if (heap_next === heap.length) heap.push(heap.length + 1);
const idx = heap_next;
heap_next = heap[idx];
heap[idx] = obj;
return idx;
}
function handleError(f, args) {
try {
return f.apply(this, args);
} catch (e) {
wasm.__wbindgen_export_0(addHeapObject(e));
}
}
function isLikeNone(x) {
return x === undefined || x === null;
}
let cachedUint8ArrayMemory0 = null;
function getUint8ArrayMemory0() {
if (cachedUint8ArrayMemory0 === null || cachedUint8ArrayMemory0.byteLength === 0) {
cachedUint8ArrayMemory0 = new Uint8Array(wasm.memory.buffer);
}
return cachedUint8ArrayMemory0;
}
let cachedTextDecoder = new TextDecoder('utf-8', { ignoreBOM: true, fatal: true });
cachedTextDecoder.decode();
const MAX_SAFARI_DECODE_BYTES = 2146435072;
let numBytesDecoded = 0;
function decodeText(ptr, len) {
numBytesDecoded += len;
if (numBytesDecoded >= MAX_SAFARI_DECODE_BYTES) {
cachedTextDecoder = new TextDecoder('utf-8', { ignoreBOM: true, fatal: true });
cachedTextDecoder.decode();
numBytesDecoded = len;
}
return cachedTextDecoder.decode(getUint8ArrayMemory0().subarray(ptr, ptr + len));
}
function getStringFromWasm0(ptr, len) {
ptr = ptr >>> 0;
return decodeText(ptr, len);
}
function dropObject(idx) {
if (idx < 132) return;
heap[idx] = heap_next;
heap_next = idx;
}
function takeObject(idx) {
const ret = getObject(idx);
dropObject(idx);
return ret;
}
export function main() {
wasm.main();
}
const EXPECTED_RESPONSE_TYPES = new Set(['basic', 'cors', 'default']);
async function __wbg_load(module, imports) {
if (typeof Response === 'function' && module instanceof Response) {
if (typeof WebAssembly.instantiateStreaming === 'function') {
try {
return await WebAssembly.instantiateStreaming(module, imports);
} catch (e) {
const validResponse = module.ok && EXPECTED_RESPONSE_TYPES.has(module.type);
if (validResponse && module.headers.get('Content-Type') !== 'application/wasm') {
console.warn("`WebAssembly.instantiateStreaming` failed because your server does not serve Wasm with `application/wasm` MIME type. Falling back to `WebAssembly.instantiate` which is slower. Original error:\n", e);
} else {
throw e;
}
}
}
const bytes = await module.arrayBuffer();
return await WebAssembly.instantiate(bytes, imports);
} else {
const instance = await WebAssembly.instantiate(module, imports);
if (instance instanceof WebAssembly.Instance) {
return { instance, module };
} else {
return instance;
}
}
}
function __wbg_get_imports() {
const imports = {};
imports.wbg = {};
imports.wbg.__wbg_appendChild_87a6cc0aeb132c06 = function() { return handleError(function (arg0, arg1) {
const ret = getObject(arg0).appendChild(getObject(arg1));
return addHeapObject(ret);
}, arguments) };
imports.wbg.__wbg_body_8822ca55cb3730d2 = function(arg0) {
const ret = getObject(arg0).body;
return isLikeNone(ret) ? 0 : addHeapObject(ret);
};
imports.wbg.__wbg_call_13410aac570ffff7 = function() { return handleError(function (arg0, arg1) {
const ret = getObject(arg0).call(getObject(arg1));
return addHeapObject(ret);
}, arguments) };
imports.wbg.__wbg_createElement_4909dfa2011f2abe = function() { return handleError(function (arg0, arg1, arg2) {
const ret = getObject(arg0).createElement(getStringFromWasm0(arg1, arg2));
return addHeapObject(ret);
}, arguments) };
imports.wbg.__wbg_document_7d29d139bd619045 = function(arg0) {
const ret = getObject(arg0).document;
return isLikeNone(ret) ? 0 : addHeapObject(ret);
};
imports.wbg.__wbg_instanceof_Window_12d20d558ef92592 = function(arg0) {
let result;
try {
result = getObject(arg0) instanceof Window;
} catch (_) {
result = false;
}
const ret = result;
return ret;
};
imports.wbg.__wbg_log_6c7b5f4f00b8ce3f = function(arg0) {
console.log(getObject(arg0));
};
imports.wbg.__wbg_newnoargs_254190557c45b4ec = function(arg0, arg1) {
const ret = new Function(getStringFromWasm0(arg0, arg1));
return addHeapObject(ret);
};
imports.wbg.__wbg_setAttribute_d1baf9023ad5696f = function() { return handleError(function (arg0, arg1, arg2, arg3, arg4) {
getObject(arg0).setAttribute(getStringFromWasm0(arg1, arg2), getStringFromWasm0(arg3, arg4));
}, arguments) };
imports.wbg.__wbg_setclassName_c8bccad917b973f4 = function(arg0, arg1, arg2) {
getObject(arg0).className = getStringFromWasm0(arg1, arg2);
};
imports.wbg.__wbg_setinnerHTML_34e240d6b8e8260c = function(arg0, arg1, arg2) {
getObject(arg0).innerHTML = getStringFromWasm0(arg1, arg2);
};
imports.wbg.__wbg_settextContent_b55fe2f5f1399466 = function(arg0, arg1, arg2) {
getObject(arg0).textContent = arg1 === 0 ? undefined : getStringFromWasm0(arg1, arg2);
};
imports.wbg.__wbg_static_accessor_GLOBAL_8921f820c2ce3f12 = function() {
const ret = typeof global === 'undefined' ? null : global;
return isLikeNone(ret) ? 0 : addHeapObject(ret);
};
imports.wbg.__wbg_static_accessor_GLOBAL_THIS_f0a4409105898184 = function() {
const ret = typeof globalThis === 'undefined' ? null : globalThis;
return isLikeNone(ret) ? 0 : addHeapObject(ret);
};
imports.wbg.__wbg_static_accessor_SELF_995b214ae681ff99 = function() {
const ret = typeof self === 'undefined' ? null : self;
return isLikeNone(ret) ? 0 : addHeapObject(ret);
};
imports.wbg.__wbg_static_accessor_WINDOW_cde3890479c675ea = function() {
const ret = typeof window === 'undefined' ? null : window;
return isLikeNone(ret) ? 0 : addHeapObject(ret);
};
imports.wbg.__wbg_wbindgenisundefined_c4b71d073b92f3c5 = function(arg0) {
const ret = getObject(arg0) === undefined;
return ret;
};
imports.wbg.__wbg_wbindgenrethrow_01815c9239d70cc2 = function(arg0) {
throw takeObject(arg0);
};
imports.wbg.__wbg_wbindgenthrow_451ec1a8469d7eb6 = function(arg0, arg1) {
throw new Error(getStringFromWasm0(arg0, arg1));
};
imports.wbg.__wbindgen_cast_2241b6af4c4b2941 = function(arg0, arg1) {
// Cast intrinsic for `Ref(String) -> Externref`.
const ret = getStringFromWasm0(arg0, arg1);
return addHeapObject(ret);
};
imports.wbg.__wbindgen_object_clone_ref = function(arg0) {
const ret = getObject(arg0);
return addHeapObject(ret);
};
imports.wbg.__wbindgen_object_drop_ref = function(arg0) {
takeObject(arg0);
};
return imports;
}
function __wbg_init_memory(imports, memory) {
}
function __wbg_finalize_init(instance, module) {
wasm = instance.exports;
__wbg_init.__wbindgen_wasm_module = module;
cachedUint8ArrayMemory0 = null;
wasm.__wbindgen_start();
return wasm;
}
function initSync(module) {
if (wasm !== undefined) return wasm;
if (typeof module !== 'undefined') {
if (Object.getPrototypeOf(module) === Object.prototype) {
({module} = module)
} else {
console.warn('using deprecated parameters for `initSync()`; pass a single object instead')
}
}
const imports = __wbg_get_imports();
__wbg_init_memory(imports);
if (!(module instanceof WebAssembly.Module)) {
module = new WebAssembly.Module(module);
}
const instance = new WebAssembly.Instance(module, imports);
return __wbg_finalize_init(instance, module);
}
async function __wbg_init(module_or_path) {
if (wasm !== undefined) return wasm;
if (typeof module_or_path !== 'undefined') {
if (Object.getPrototypeOf(module_or_path) === Object.prototype) {
({module_or_path} = module_or_path)
} else {
console.warn('using deprecated parameters for the initialization function; pass a single object instead')
}
}
if (typeof module_or_path === 'undefined') {
module_or_path = new URL('railwayka_landing_bg.wasm', import.meta.url);
}
const imports = __wbg_get_imports();
if (typeof module_or_path === 'string' || (typeof Request === 'function' && module_or_path instanceof Request) || (typeof URL === 'function' && module_or_path instanceof URL)) {
module_or_path = fetch(module_or_path);
}
__wbg_init_memory(imports);
const { instance, module } = await __wbg_load(await module_or_path, imports);
return __wbg_finalize_init(instance, module);
}
export { initSync };
export default __wbg_init;

BIN
dist/railwayka_landing_bg.wasm vendored Normal file

Binary file not shown.

6
dist/railwayka_landing_bg.wasm.d.ts vendored Normal file
View File

@ -0,0 +1,6 @@
/* tslint:disable */
/* eslint-disable */
export const memory: WebAssembly.Memory;
export const main: () => void;
export const __wbindgen_export_0: (a: number) => void;
export const __wbindgen_start: () => void;

299
dist/style.css vendored Normal file
View File

@ -0,0 +1,299 @@
/* Reset and base styles */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
:root {
--bg-dark: #0a0e17;
--bg-card: #1a1f2e;
--bg-card-hover: #252a3a;
--text-primary: #e0e6ed;
--text-secondary: #9ca3af;
--accent: #3b82f6;
--accent-hover: #2563eb;
--rust: #f74c00;
--border: #2d3748;
--success: #10b981;
--planned: #6366f1;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', sans-serif;
background: var(--bg-dark);
color: var(--text-primary);
line-height: 1.6;
min-height: 100vh;
}
/* Loading screen */
#loading {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: var(--bg-dark);
display: flex;
align-items: center;
justify-content: center;
z-index: 9999;
}
.loader {
font-size: 1.2rem;
color: var(--text-secondary);
animation: pulse 2s ease-in-out infinite;
}
@keyframes pulse {
0%, 100% { opacity: 0.5; }
50% { opacity: 1; }
}
.error {
padding: 2rem;
text-align: center;
color: var(--rust);
}
/* Container */
.container {
max-width: 1200px;
margin: 0 auto;
padding: 0 2rem;
}
/* Hero Section */
.hero {
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
text-align: center;
background: linear-gradient(135deg, #0a0e17 0%, #1a1f2e 100%);
position: relative;
overflow: hidden;
}
.hero::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: radial-gradient(circle at 50% 50%, rgba(59, 130, 246, 0.1) 0%, transparent 50%);
}
.hero .container {
position: relative;
z-index: 1;
}
.hero-title {
font-size: clamp(3rem, 8vw, 6rem);
font-weight: 700;
margin-bottom: 1rem;
background: linear-gradient(135deg, var(--accent) 0%, var(--planned) 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.hero-subtitle {
font-size: clamp(1.2rem, 3vw, 1.5rem);
color: var(--text-secondary);
margin-bottom: 2rem;
max-width: 600px;
margin-left: auto;
margin-right: auto;
}
.hero-badge {
display: inline-block;
padding: 0.75rem 1.5rem;
background: var(--bg-card);
border: 1px solid var(--border);
border-radius: 2rem;
font-size: 1rem;
color: var(--text-primary);
animation: float 3s ease-in-out infinite;
}
@keyframes float {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-10px); }
}
/* Section Styles */
section {
padding: 5rem 0;
}
.section-title {
font-size: 2.5rem;
margin-bottom: 3rem;
text-align: center;
color: var(--text-primary);
}
/* Services Section */
.services {
background: var(--bg-dark);
}
.services-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 1.5rem;
}
.service-card {
background: var(--bg-card);
border: 1px solid var(--border);
border-radius: 0.75rem;
padding: 1.5rem;
transition: all 0.3s ease;
text-decoration: none;
color: inherit;
display: block;
}
.service-card:hover {
background: var(--bg-card-hover);
border-color: var(--accent);
transform: translateY(-4px);
box-shadow: 0 10px 25px rgba(59, 130, 246, 0.2);
}
.service-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 0.75rem;
}
.service-card h3 {
font-size: 1.25rem;
color: var(--text-primary);
flex: 1;
}
.status-badge {
font-size: 1.5rem;
flex-shrink: 0;
margin-left: 0.5rem;
}
.service-description {
color: var(--text-secondary);
font-size: 0.95rem;
line-height: 1.6;
}
/* Tech Section */
.tech {
background: linear-gradient(180deg, var(--bg-dark) 0%, var(--bg-card) 100%);
}
.tech-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 2rem;
}
.tech-item {
text-align: center;
padding: 2rem 1rem;
}
.tech-item h3 {
font-size: 1.5rem;
margin-bottom: 0.75rem;
color: var(--accent);
}
.tech-item p {
color: var(--text-secondary);
font-size: 0.95rem;
}
/* Footer */
.footer {
background: var(--bg-card);
border-top: 1px solid var(--border);
padding: 3rem 0;
text-align: center;
}
.footer p {
color: var(--text-secondary);
margin-bottom: 1.5rem;
}
.rust-love {
color: var(--rust);
font-weight: 600;
}
.footer-links {
display: flex;
justify-content: center;
gap: 2rem;
flex-wrap: wrap;
}
.footer-links a {
color: var(--accent);
text-decoration: none;
transition: color 0.3s ease;
font-size: 0.95rem;
}
.footer-links a:hover {
color: var(--accent-hover);
text-decoration: underline;
}
/* Responsive adjustments */
@media (max-width: 768px) {
.container {
padding: 0 1.5rem;
}
section {
padding: 3rem 0;
}
.services-grid,
.tech-grid {
grid-template-columns: 1fr;
}
.hero {
min-height: 80vh;
}
.footer-links {
flex-direction: column;
gap: 1rem;
}
}
/* Smooth scrolling */
html {
scroll-behavior: smooth;
}
/* Selection color */
::selection {
background: var(--accent);
color: white;
}
::-moz-selection {
background: var(--accent);
color: white;
}

35
index.html Normal file
View File

@ -0,0 +1,35 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="railwayka.ru - Modern self-hosted infrastructure powered by GitOps">
<title>railwayka.ru</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<div id="loading">
<div class="loader">Loading...</div>
</div>
<script type="module">
import init from './railwayka_landing.js';
async function run() {
try {
await init();
// Remove loading indicator
const loading = document.getElementById('loading');
if (loading) {
loading.remove();
}
} catch (error) {
console.error('Failed to initialize WASM:', error);
document.body.innerHTML = '<div class="error">Failed to load application. Please refresh the page.</div>';
}
}
run();
</script>
</body>
</html>

240
src/lib.rs Normal file
View File

@ -0,0 +1,240 @@
use wasm_bindgen::prelude::*;
use web_sys::{console, Document, Element, HtmlElement, Window};
#[wasm_bindgen(start)]
pub fn main() -> Result<(), JsValue> {
console::log_1(&"🚀 railwayka.ru WASM landing page initializing...".into());
let window = web_sys::window().expect("no global window exists");
let document = window.document().expect("should have a document on window");
// Clear body and build page
if let Some(body) = document.body() {
body.set_inner_html("");
build_page(&document, &body)?;
}
console::log_1(&"✅ Landing page ready!".into());
Ok(())
}
fn build_page(document: &Document, body: &HtmlElement) -> Result<(), JsValue> {
// Hero Section
let hero = create_hero(document)?;
body.append_child(&hero)?;
// Services Section
let services = create_services_section(document)?;
body.append_child(&services)?;
// Tech Stack Section
let tech = create_tech_section(document)?;
body.append_child(&tech)?;
// Footer
let footer = create_footer(document)?;
body.append_child(&footer)?;
Ok(())
}
fn create_hero(document: &Document) -> Result<Element, JsValue> {
let section = document.create_element("section")?;
section.set_class_name("hero");
let container = document.create_element("div")?;
container.set_class_name("container");
let title = document.create_element("h1")?;
title.set_text_content(Some("railwayka.ru"));
title.set_class_name("hero-title");
let subtitle = document.create_element("p")?;
subtitle.set_text_content(Some("Modern self-hosted infrastructure powered by GitOps"));
subtitle.set_class_name("hero-subtitle");
let badge = document.create_element("div")?;
badge.set_class_name("hero-badge");
badge.set_text_content(Some("🦀 Powered by Rust + WebAssembly"));
container.append_child(&title)?;
container.append_child(&subtitle)?;
container.append_child(&badge)?;
section.append_child(&container)?;
Ok(section)
}
fn create_services_section(document: &Document) -> Result<Element, JsValue> {
let section = document.create_element("section")?;
section.set_class_name("services");
let container = document.create_element("div")?;
container.set_class_name("container");
let heading = document.create_element("h2")?;
heading.set_text_content(Some("Services"));
heading.set_class_name("section-title");
container.append_child(&heading)?;
let grid = document.create_element("div")?;
grid.set_class_name("services-grid");
// Live services
let services_live = vec![
("Git Repository", "Gitea - Self-hosted Git server with web UI", "🔴", "https://git.dc.railwayka.ru"),
("CI/CD", "Woodpecker CI - Automated builds and deployments", "🔴", "https://cicd.dc.railwayka.ru"),
("Container Management", "Portainer - Docker management interface", "🔴", "https://port.dc.railwayka.ru"),
];
for (name, desc, status, url) in services_live {
let card = create_service_card(document, name, desc, status, Some(url))?;
grid.append_child(&card)?;
}
// Planned services
let services_planned = vec![
("Matrix Server", "Decentralized messaging and communication", "🔵"),
("Blog", "Technical writing and knowledge sharing", "🔵"),
("Password Manager", "Vaultwarden - Self-hosted password vault", "🔵"),
("Cloud Storage", "Nextcloud - File sync and sharing", "🔵"),
("Monitoring", "Grafana + Prometheus - Infrastructure monitoring", "🔵"),
];
for (name, desc, status) in services_planned {
let card = create_service_card(document, name, desc, status, None)?;
grid.append_child(&card)?;
}
container.append_child(&grid)?;
section.append_child(&container)?;
Ok(section)
}
fn create_service_card(
document: &Document,
name: &str,
description: &str,
status: &str,
url: Option<&str>,
) -> Result<Element, JsValue> {
let card = if let Some(link) = url {
let a = document.create_element("a")?;
a.set_attribute("href", link)?;
a.set_attribute("target", "_blank")?;
a.set_attribute("rel", "noopener noreferrer")?;
a
} else {
document.create_element("div")?
};
card.set_class_name("service-card");
let header = document.create_element("div")?;
header.set_class_name("service-header");
let title = document.create_element("h3")?;
title.set_text_content(Some(name));
let badge = document.create_element("span")?;
badge.set_class_name("status-badge");
badge.set_text_content(Some(status));
header.append_child(&title)?;
header.append_child(&badge)?;
let desc = document.create_element("p")?;
desc.set_text_content(Some(description));
desc.set_class_name("service-description");
card.append_child(&header)?;
card.append_child(&desc)?;
Ok(card)
}
fn create_tech_section(document: &Document) -> Result<Element, JsValue> {
let section = document.create_element("section")?;
section.set_class_name("tech");
let container = document.create_element("div")?;
container.set_class_name("container");
let heading = document.create_element("h2")?;
heading.set_text_content(Some("Technology Stack"));
heading.set_class_name("section-title");
container.append_child(&heading)?;
let grid = document.create_element("div")?;
grid.set_class_name("tech-grid");
let technologies = vec![
("Dcape", "GitOps platform for containerized applications"),
("Docker", "Containerization and orchestration"),
("Traefik", "Automatic HTTPS and reverse proxy"),
("PowerDNS", "Authoritative DNS server"),
("PostgreSQL", "Shared database infrastructure"),
("Rust + WASM", "High-performance web applications"),
];
for (name, desc) in technologies {
let item = create_tech_item(document, name, desc)?;
grid.append_child(&item)?;
}
container.append_child(&grid)?;
section.append_child(&container)?;
Ok(section)
}
fn create_tech_item(document: &Document, name: &str, description: &str) -> Result<Element, JsValue> {
let item = document.create_element("div")?;
item.set_class_name("tech-item");
let title = document.create_element("h3")?;
title.set_text_content(Some(name));
let desc = document.create_element("p")?;
desc.set_text_content(Some(description));
item.append_child(&title)?;
item.append_child(&desc)?;
Ok(item)
}
fn create_footer(document: &Document) -> Result<Element, JsValue> {
let footer = document.create_element("footer")?;
footer.set_class_name("footer");
let container = document.create_element("div")?;
container.set_class_name("container");
let text = document.create_element("p")?;
text.set_inner_html("Built with <span class=\"rust-love\">🦀 Rust</span> + WebAssembly • Deployed via GitOps");
let links = document.create_element("div")?;
links.set_class_name("footer-links");
let link_data = vec![
("Traefik Dashboard", "https://dc.railwayka.ru/dashboard/"),
("Gitea", "https://git.dc.railwayka.ru"),
("Woodpecker CI", "https://cicd.dc.railwayka.ru"),
];
for (name, url) in link_data {
let link = document.create_element("a")?;
link.set_attribute("href", url)?;
link.set_attribute("target", "_blank")?;
link.set_attribute("rel", "noopener noreferrer")?;
link.set_text_content(Some(name));
links.append_child(&link)?;
}
container.append_child(&text)?;
container.append_child(&links)?;
footer.append_child(&container)?;
Ok(footer)
}

299
style.css Normal file
View File

@ -0,0 +1,299 @@
/* Reset and base styles */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
:root {
--bg-dark: #0a0e17;
--bg-card: #1a1f2e;
--bg-card-hover: #252a3a;
--text-primary: #e0e6ed;
--text-secondary: #9ca3af;
--accent: #3b82f6;
--accent-hover: #2563eb;
--rust: #f74c00;
--border: #2d3748;
--success: #10b981;
--planned: #6366f1;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', sans-serif;
background: var(--bg-dark);
color: var(--text-primary);
line-height: 1.6;
min-height: 100vh;
}
/* Loading screen */
#loading {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: var(--bg-dark);
display: flex;
align-items: center;
justify-content: center;
z-index: 9999;
}
.loader {
font-size: 1.2rem;
color: var(--text-secondary);
animation: pulse 2s ease-in-out infinite;
}
@keyframes pulse {
0%, 100% { opacity: 0.5; }
50% { opacity: 1; }
}
.error {
padding: 2rem;
text-align: center;
color: var(--rust);
}
/* Container */
.container {
max-width: 1200px;
margin: 0 auto;
padding: 0 2rem;
}
/* Hero Section */
.hero {
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
text-align: center;
background: linear-gradient(135deg, #0a0e17 0%, #1a1f2e 100%);
position: relative;
overflow: hidden;
}
.hero::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: radial-gradient(circle at 50% 50%, rgba(59, 130, 246, 0.1) 0%, transparent 50%);
}
.hero .container {
position: relative;
z-index: 1;
}
.hero-title {
font-size: clamp(3rem, 8vw, 6rem);
font-weight: 700;
margin-bottom: 1rem;
background: linear-gradient(135deg, var(--accent) 0%, var(--planned) 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.hero-subtitle {
font-size: clamp(1.2rem, 3vw, 1.5rem);
color: var(--text-secondary);
margin-bottom: 2rem;
max-width: 600px;
margin-left: auto;
margin-right: auto;
}
.hero-badge {
display: inline-block;
padding: 0.75rem 1.5rem;
background: var(--bg-card);
border: 1px solid var(--border);
border-radius: 2rem;
font-size: 1rem;
color: var(--text-primary);
animation: float 3s ease-in-out infinite;
}
@keyframes float {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-10px); }
}
/* Section Styles */
section {
padding: 5rem 0;
}
.section-title {
font-size: 2.5rem;
margin-bottom: 3rem;
text-align: center;
color: var(--text-primary);
}
/* Services Section */
.services {
background: var(--bg-dark);
}
.services-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 1.5rem;
}
.service-card {
background: var(--bg-card);
border: 1px solid var(--border);
border-radius: 0.75rem;
padding: 1.5rem;
transition: all 0.3s ease;
text-decoration: none;
color: inherit;
display: block;
}
.service-card:hover {
background: var(--bg-card-hover);
border-color: var(--accent);
transform: translateY(-4px);
box-shadow: 0 10px 25px rgba(59, 130, 246, 0.2);
}
.service-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 0.75rem;
}
.service-card h3 {
font-size: 1.25rem;
color: var(--text-primary);
flex: 1;
}
.status-badge {
font-size: 1.5rem;
flex-shrink: 0;
margin-left: 0.5rem;
}
.service-description {
color: var(--text-secondary);
font-size: 0.95rem;
line-height: 1.6;
}
/* Tech Section */
.tech {
background: linear-gradient(180deg, var(--bg-dark) 0%, var(--bg-card) 100%);
}
.tech-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 2rem;
}
.tech-item {
text-align: center;
padding: 2rem 1rem;
}
.tech-item h3 {
font-size: 1.5rem;
margin-bottom: 0.75rem;
color: var(--accent);
}
.tech-item p {
color: var(--text-secondary);
font-size: 0.95rem;
}
/* Footer */
.footer {
background: var(--bg-card);
border-top: 1px solid var(--border);
padding: 3rem 0;
text-align: center;
}
.footer p {
color: var(--text-secondary);
margin-bottom: 1.5rem;
}
.rust-love {
color: var(--rust);
font-weight: 600;
}
.footer-links {
display: flex;
justify-content: center;
gap: 2rem;
flex-wrap: wrap;
}
.footer-links a {
color: var(--accent);
text-decoration: none;
transition: color 0.3s ease;
font-size: 0.95rem;
}
.footer-links a:hover {
color: var(--accent-hover);
text-decoration: underline;
}
/* Responsive adjustments */
@media (max-width: 768px) {
.container {
padding: 0 1.5rem;
}
section {
padding: 3rem 0;
}
.services-grid,
.tech-grid {
grid-template-columns: 1fr;
}
.hero {
min-height: 80vh;
}
.footer-links {
flex-direction: column;
gap: 1rem;
}
}
/* Smooth scrolling */
html {
scroll-behavior: smooth;
}
/* Selection color */
::selection {
background: var(--accent);
color: white;
}
::-moz-selection {
background: var(--accent);
color: white;
}