diff --git a/.gitea/workflows/build-and-deploy.yaml b/.gitea/workflows/build-and-deploy.yaml
new file mode 100644
index 0000000..f582d0b
--- /dev/null
+++ b/.gitea/workflows/build-and-deploy.yaml
@@ -0,0 +1,102 @@
+name: Build and Deploy
+
+on:
+ push:
+ branches: [main]
+ pull_request:
+ branches: [main]
+
+env:
+ REGISTRY: registry.claw.jopdorp.nl
+ IMAGE_NAME: signalledger
+ NAMESPACE: openclaw-private
+
+jobs:
+ build-and-test:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+
+ - name: Set up Node
+ uses: actions/setup-node@v4
+ with:
+ node-version: '22'
+ cache: 'npm'
+
+ - name: Install dependencies
+ run: npm ci
+
+ - name: Run tests
+ run: npm test
+
+ - name: Build site
+ run: npm run build
+
+ - name: Upload build artifact
+ uses: actions/upload-artifact@v4
+ with:
+ name: dist
+ path: dist/
+
+ build-image:
+ runs-on: ubuntu-latest
+ needs: build-and-test
+ if: github.event_name == 'push' && github.ref == 'refs/heads/main'
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+
+ - name: Set up Docker Buildx
+ uses: docker/setup-buildx-action@v3
+
+ - name: Log in to registry
+ uses: docker/login-action@v3
+ with:
+ registry: ${{ env.REGISTRY }}
+ username: ${{ gitea.actor }}
+ password: ${{ secrets.REGISTRY_TOKEN }}
+
+ - name: Docker meta
+ id: meta
+ uses: docker/metadata-action@v5
+ with:
+ images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
+ tags: |
+ type=sha,prefix=,suffix=,format=short
+ type=raw,value=latest
+
+ - name: Build and push
+ uses: docker/build-push-action@v5
+ with:
+ context: .
+ push: true
+ tags: ${{ steps.meta.outputs.tags }}
+ labels: ${{ steps.meta.outputs.labels }}
+ cache-from: type=gha
+ cache-to: type=gha,mode=max
+
+ deploy:
+ runs-on: ubuntu-latest
+ needs: build-image
+ if: github.event_name == 'push' && github.ref == 'refs/heads/main'
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+
+ - name: Set up kubectl
+ uses: tale/kubectl-action@v1
+ with:
+ base64-kube-config: ${{ secrets.KUBECONFIG_BASE64 }}
+
+ - name: Update image tag in manifest
+ run: |
+ sed -i "s|image: .*signalledger.*|image: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:sha-$(git rev-parse --short HEAD)|" k8s/deployment.yaml
+
+ - name: Apply Kubernetes manifests
+ run: |
+ kubectl apply -f k8s/namespace.yaml
+ kubectl apply -f k8s/deployment.yaml
+ kubectl apply -f k8s/service.yaml
+ kubectl apply -f k8s/ingress.yaml
+ kubectl rollout status deployment/signalledger -n ${{ env.NAMESPACE }} --timeout=120s
diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 0000000..de3d7e4
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,38 @@
+# Build stage
+FROM node:22-alpine AS builder
+
+WORKDIR /app
+
+# Copy package files and install dependencies
+COPY package*.json ./
+RUN npm ci
+
+# Copy source and build
+COPY . .
+RUN npm run build
+
+# Production stage — nginx serving static files
+FROM nginx:alpine
+
+# Copy built site
+COPY --from=builder /app/dist /usr/share/nginx/html
+
+# Copy ads.txt if present at root
+COPY --from=builder /app/ads.txt /usr/share/nginx/html/ads.txt
+
+# Custom nginx config for SPA/history routing
+RUN echo 'server { \
+ listen 80; \
+ server_name localhost; \
+ root /usr/share/nginx/html; \
+ index index.html; \
+ location / { \
+ try_files $uri $uri/ /index.html; \
+ } \
+ location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ { \
+ expires 1y; \
+ add_header Cache-Control "public, immutable"; \
+ } \
+}' > /etc/nginx/conf.d/default.conf
+
+EXPOSE 80
diff --git a/build.js b/build.js
new file mode 100644
index 0000000..43d23f8
--- /dev/null
+++ b/build.js
@@ -0,0 +1,46 @@
+const fs = require('fs');
+const path = require('path');
+
+const srcDir = path.join(__dirname, 'src');
+const distDir = path.join(__dirname, 'dist');
+
+if (!fs.existsSync(distDir)) {
+ fs.mkdirSync(distDir, { recursive: true });
+}
+
+// Simple build: copy static assets and generate index.html
+const indexHtml = `
+
+
+
+
+ Signal Ledger
+
+
+
+
+
+ Welcome to Signal Ledger — a subsidiary of Jopdorp.
+
+
+
+
+`;
+
+fs.writeFileSync(path.join(distDir, 'index.html'), indexHtml);
+
+// Copy style.css if it exists
+const styleSrc = path.join(srcDir, 'style.css');
+const styleDest = path.join(distDir, 'style.css');
+if (fs.existsSync(styleSrc)) {
+ fs.copyFileSync(styleSrc, styleDest);
+} else {
+ fs.writeFileSync(styleDest, `body{font-family:system-ui,sans-serif;margin:0;padding:2rem;background:#0b0c10;color:#c5c6c7}header{border-bottom:1px solid #45a29e;padding-bottom:1rem;margin-bottom:2rem}h1{color:#66fcf1;margin:0}.tagline{color:#45a29e}footer{margin-top:4rem;padding-top:1rem;border-top:1px solid #45a29e;font-size:.875rem;color:#45a29e}`);
+}
+
+console.log('Build complete: dist/');
diff --git a/dist/index.html b/dist/index.html
new file mode 100644
index 0000000..df271e3
--- /dev/null
+++ b/dist/index.html
@@ -0,0 +1,21 @@
+
+
+
+
+
+ Signal Ledger
+
+
+
+
+
+ Welcome to Signal Ledger — a subsidiary of Jopdorp.
+
+
+
+
diff --git a/dist/style.css b/dist/style.css
new file mode 100644
index 0000000..07fd72d
--- /dev/null
+++ b/dist/style.css
@@ -0,0 +1 @@
+body{font-family:system-ui,sans-serif;margin:0;padding:2rem;background:#0b0c10;color:#c5c6c7}header{border-bottom:1px solid #45a29e;padding-bottom:1rem;margin-bottom:2rem}h1{color:#66fcf1;margin:0}.tagline{color:#45a29e}footer{margin-top:4rem;padding-top:1rem;border-top:1px solid #45a29e;font-size:.875rem;color:#45a29e}
\ No newline at end of file
diff --git a/k8s/deployment.yaml b/k8s/deployment.yaml
new file mode 100644
index 0000000..d3aa6b6
--- /dev/null
+++ b/k8s/deployment.yaml
@@ -0,0 +1,53 @@
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: signalledger
+ namespace: openclaw-private
+ labels:
+ app.kubernetes.io/name: signalledger
+ app.kubernetes.io/component: frontend
+ app.kubernetes.io/part-of: signalledger
+ app.kubernetes.io/managed-by: gitea-actions
+spec:
+ replicas: 2
+ strategy:
+ type: RollingUpdate
+ rollingUpdate:
+ maxSurge: 1
+ maxUnavailable: 0
+ selector:
+ matchLabels:
+ app.kubernetes.io/name: signalledger
+ template:
+ metadata:
+ labels:
+ app.kubernetes.io/name: signalledger
+ app.kubernetes.io/component: frontend
+ app.kubernetes.io/part-of: signalledger
+ spec:
+ containers:
+ - name: signalledger
+ image: registry.claw.jopdorp.nl/signalledger:latest
+ imagePullPolicy: Always
+ ports:
+ - containerPort: 80
+ name: http
+ resources:
+ requests:
+ memory: "32Mi"
+ cpu: "50m"
+ limits:
+ memory: "128Mi"
+ cpu: "200m"
+ livenessProbe:
+ httpGet:
+ path: /
+ port: 80
+ initialDelaySeconds: 10
+ periodSeconds: 10
+ readinessProbe:
+ httpGet:
+ path: /
+ port: 80
+ initialDelaySeconds: 5
+ periodSeconds: 5
diff --git a/k8s/ingress.yaml b/k8s/ingress.yaml
new file mode 100644
index 0000000..bad1381
--- /dev/null
+++ b/k8s/ingress.yaml
@@ -0,0 +1,40 @@
+apiVersion: networking.k8s.io/v1
+kind: Ingress
+metadata:
+ name: signalledger
+ namespace: openclaw-private
+ labels:
+ app.kubernetes.io/name: signalledger
+ app.kubernetes.io/component: frontend
+ app.kubernetes.io/part-of: signalledger
+ annotations:
+ cert-manager.io/cluster-issuer: "letsencrypt-prod"
+ nginx.ingress.kubernetes.io/ssl-redirect: "true"
+spec:
+ ingressClassName: nginx
+ tls:
+ - hosts:
+ - signalledger.nl
+ - www.signalledger.nl
+ secretName: signalledger-tls
+ rules:
+ - host: signalledger.nl
+ http:
+ paths:
+ - path: /
+ pathType: Prefix
+ backend:
+ service:
+ name: signalledger
+ port:
+ number: 80
+ - host: www.signalledger.nl
+ http:
+ paths:
+ - path: /
+ pathType: Prefix
+ backend:
+ service:
+ name: signalledger
+ port:
+ number: 80
diff --git a/k8s/namespace.yaml b/k8s/namespace.yaml
new file mode 100644
index 0000000..dcb847f
--- /dev/null
+++ b/k8s/namespace.yaml
@@ -0,0 +1,7 @@
+apiVersion: v1
+kind: Namespace
+metadata:
+ name: openclaw-private
+ labels:
+ app.kubernetes.io/name: signalledger
+ app.kubernetes.io/part-of: signalledger
diff --git a/k8s/service.yaml b/k8s/service.yaml
new file mode 100644
index 0000000..354dcef
--- /dev/null
+++ b/k8s/service.yaml
@@ -0,0 +1,17 @@
+apiVersion: v1
+kind: Service
+metadata:
+ name: signalledger
+ namespace: openclaw-private
+ labels:
+ app.kubernetes.io/name: signalledger
+ app.kubernetes.io/component: frontend
+ app.kubernetes.io/part-of: signalledger
+spec:
+ selector:
+ app.kubernetes.io/name: signalledger
+ ports:
+ - port: 80
+ targetPort: 80
+ name: http
+ type: ClusterIP
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..9a08128
--- /dev/null
+++ b/package.json
@@ -0,0 +1,10 @@
+{
+ "name": "signalledger.nl",
+ "version": "1.0.0",
+ "description": "Signal Ledger news site",
+ "scripts": {
+ "build": "node build.js",
+ "test": "node test.js"
+ },
+ "devDependencies": {}
+}
diff --git a/test.js b/test.js
new file mode 100644
index 0000000..6e810af
--- /dev/null
+++ b/test.js
@@ -0,0 +1,35 @@
+const fs = require('fs');
+const path = require('path');
+
+const distDir = path.join(__dirname, 'dist');
+
+// Run build first if dist doesn't exist
+if (!fs.existsSync(distDir)) {
+ require('./build.js');
+}
+
+const indexPath = path.join(distDir, 'index.html');
+if (!fs.existsSync(indexPath)) {
+ console.error('FAIL: dist/index.html not found');
+ process.exit(1);
+}
+
+const html = fs.readFileSync(indexPath, 'utf8');
+const checks = [
+ ['title', html.includes('Signal Ledger')],
+ ['header', html.includes('Signal Ledger
')],
+ ['footer', html.includes('Signal Ledger is a subsidiary of Jopdorp.')],
+];
+
+let failed = false;
+for (const [name, pass] of checks) {
+ if (pass) {
+ console.log(`PASS: ${name}`);
+ } else {
+ console.error(`FAIL: ${name}`);
+ failed = true;
+ }
+}
+
+if (failed) process.exit(1);
+console.log('All tests passed.');