Compare commits

...
4 Commits
Author SHA1 Message Date
haopengzhan 053d19ce6a fix(analytics): prevent tracking on admin pages and local dev environments
Build and Push Docker Image / build (push) Successful in 19s
2026-09-16 23:41:33 -07:00
haopengzhan afc05deb91 chore(k8s): safeguard production secrets with example template and add Makefile k8s helpers
Build and Push Docker Image / build (push) Successful in 17s
2026-09-16 23:30:21 -07:00
haopengzhan 2502c2946e fix(k8s): add chmod 775 to init-content-perms container
Build and Push Docker Image / build (push) Successful in 39s
2026-09-16 23:18:27 -07:00
haopengzhan d865ee2d2b chore(k8s): add production kubernetes manifests for pengzhan-dev namespace
Build and Push Docker Image / build (push) Successful in 1m5s
2026-09-16 23:13:53 -07:00
9 changed files with 227 additions and 16 deletions
+5
View File
@@ -20,3 +20,8 @@ npm-debug.log*
# Raw WordPress XML Exports / Backups
*.WordPress.*.xml
*.wxr.xml
# Kubernetes Production Secrets
k8s/01-secret.yaml
k8s/*secret.yaml
!k8s/*.example.yaml
+38 -10
View File
@@ -1,4 +1,4 @@
.PHONY: help dev dev-persistent test lint clean docker-build docker-run
.PHONY: help dev dev-persistent test lint clean docker-build docker-run k8s-status k8s-logs k8s-restart k8s-sync-content
# Default target
all: help
@@ -8,15 +8,19 @@ help:
@echo " pengzhan.dev — Engineering Makefile"
@echo "========================================================================"
@echo "Available commands:"
@echo " make dev Start local server in Ephemeral Staging Mode"
@echo " (Safe by default: site.db is NEVER mutated)"
@echo " make dev-persistent Start local server in Persistent DB Mode"
@echo " (Changes to comments & logs write to site.db)"
@echo " make test Run complete automated test suite (in isolated temp DB)"
@echo " make lint Run comprehensive project linter (JS, EJS, CSS)"
@echo " make clean Remove temporary staging DBs and build caches"
@echo " make docker-build Build multi-stage production Docker image"
@echo " make docker-run Run production Docker container on port 3000"
@echo " make dev Start local server in Ephemeral Staging Mode"
@echo " (Safe by default: site.db is NEVER mutated)"
@echo " make dev-persistent Start local server in Persistent DB Mode"
@echo " (Changes to comments & logs write to site.db)"
@echo " make test Run complete automated test suite"
@echo " make lint Run comprehensive project linter (JS, EJS, CSS)"
@echo " make clean Remove temporary staging DBs and build caches"
@echo " make docker-build Build multi-stage production Docker image"
@echo " make docker-run Run production Docker container on port 3000"
@echo " make k8s-status Show pods, service, ingress, and PVC in k8s"
@echo " make k8s-logs Stream live logs from production pod"
@echo " make k8s-restart Trigger zero-downtime rolling restart"
@echo " make k8s-sync-content Sync local markdown/media into live cluster PVC"
@echo "========================================================================"
dev:
@@ -50,3 +54,27 @@ docker-build:
docker-run:
@echo "Running Docker container on http://localhost:3000..."
docker run --rm -p 3000:3000 -v $(PWD)/sample-content:/app/content pengzhan.dev:latest
k8s-status:
@echo "Checking Kubernetes status in namespace pengzhan-dev..."
kubectl get pods,svc,ingress,pvc -n pengzhan-dev -o wide
k8s-logs:
@echo "Streaming logs from pengzhan-web deployment..."
kubectl logs -f deployment/pengzhan-web -n pengzhan-dev
k8s-restart:
@echo "Triggering zero-downtime rolling restart of pengzhan-web..."
kubectl rollout restart deployment/pengzhan-web -n pengzhan-dev
kubectl rollout status deployment/pengzhan-web -n pengzhan-dev
k8s-sync-content:
@echo "Syncing local sample-content/ to Kubernetes /app/content/..."
@POD=$$(kubectl get pod -n pengzhan-dev -l app=pengzhan-web -o jsonpath='{.items[0].metadata.name}'); \
if [ -z "$$POD" ]; then \
echo "Error: No running pengzhan-web pod found."; exit 1; \
fi; \
echo "-> Target pod: $$POD"; \
kubectl cp sample-content/. pengzhan-dev/$$POD:/app/content/; \
kubectl exec -n pengzhan-dev $$POD -- chmod -R 775 /app/content; \
echo "-> Sync complete. Hot-reload watcher will index new content."
+6
View File
@@ -0,0 +1,6 @@
apiVersion: v1
kind: Namespace
metadata:
name: pengzhan-dev
labels:
app.kubernetes.io/name: pengzhan-dev
+10
View File
@@ -0,0 +1,10 @@
# Template Kubernetes secret for pengzhan.dev
# Copy this file to k8s/01-secret.yaml (gitignored) and customize before applying.
apiVersion: v1
kind: Secret
metadata:
name: pengzhan-secrets
namespace: pengzhan-dev
type: Opaque
stringData:
ADMIN_SECRET: "CHANGEME_ADMIN_SECRET"
+12
View File
@@ -0,0 +1,12 @@
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: pengzhan-content-pvc
namespace: pengzhan-dev
spec:
accessModes:
- ReadWriteOnce
storageClassName: local-path
resources:
requests:
storage: 5Gi
+88
View File
@@ -0,0 +1,88 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: pengzhan-web
namespace: pengzhan-dev
labels:
app: pengzhan-web
spec:
replicas: 1
revisionHistoryLimit: 5
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 1
maxUnavailable: 0
selector:
matchLabels:
app: pengzhan-web
template:
metadata:
labels:
app: pengzhan-web
spec:
securityContext:
fsGroup: 1000
fsGroupChangePolicy: "OnRootMismatch"
initContainers:
- name: init-content-perms
image: busybox:1.36
imagePullPolicy: IfNotPresent
command: ["sh", "-c"]
args:
- |
mkdir -p /app/content/data
chown -R 1000:1000 /app/content || true
chmod -R 775 /app/content || true
volumeMounts:
- name: content-data
mountPath: /app/content
containers:
- name: pengzhan-web
image: git.pengzhan.dev/haopengzhan/pengzhan.dev:latest
imagePullPolicy: Always
ports:
- containerPort: 3000
name: http
env:
- name: NODE_ENV
value: "production"
- name: PORT
value: "3000"
- name: CONTENT_DIR
value: "/app/content"
- name: ADMIN_SECRET
valueFrom:
secretKeyRef:
name: pengzhan-secrets
key: ADMIN_SECRET
volumeMounts:
- name: content-data
mountPath: /app/content
livenessProbe:
httpGet:
path: /
port: 3000
initialDelaySeconds: 15
periodSeconds: 30
timeoutSeconds: 5
failureThreshold: 3
readinessProbe:
httpGet:
path: /
port: 3000
initialDelaySeconds: 10
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 2
resources:
requests:
cpu: 50m
memory: 128Mi
limits:
cpu: 500m
memory: 512Mi
volumes:
- name: content-data
persistentVolumeClaim:
claimName: pengzhan-content-pvc
+15
View File
@@ -0,0 +1,15 @@
apiVersion: v1
kind: Service
metadata:
name: pengzhan-web
namespace: pengzhan-dev
labels:
app: pengzhan-web
spec:
type: ClusterIP
selector:
app: pengzhan-web
ports:
- name: http
port: 80
targetPort: 3000
+24
View File
@@ -0,0 +1,24 @@
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: pengzhan-ingress
namespace: pengzhan-dev
annotations:
cert-manager.io/cluster-issuer: letsencrypt-prod
spec:
ingressClassName: traefik
rules:
- host: pengzhan.dev
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: pengzhan-web
port:
number: 80
tls:
- hosts:
- pengzhan.dev
secretName: pengzhan-dev-tls
@@ -1,17 +1,40 @@
module.exports = {
injectHead({ pluginConfig }) {
injectHead({ pluginConfig, pageType }) {
const trackingId = pluginConfig.tracking_id;
if (!trackingId) return '';
// 1. Never inject analytics on administrative moderation pages
if (pageType === 'admin') {
return '';
}
// 2. Do not inject in development, staging, or testing modes (unless explicitly enabled)
const isProd = process.env.NODE_ENV === 'production' && process.env.APP_ENV !== 'staging';
if (!isProd && !pluginConfig.enable_in_dev) {
return '';
}
return `
<!-- Google Analytics (GA4 via Plugin) -->
<script async src="https://www.googletagmanager.com/gtag/js?id=${trackingId}"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', '${trackingId}');
(function() {
// 3. Client-side domain safeguard: only track on production pengzhan.dev domains
var host = window.location.hostname;
if (host !== 'pengzhan.dev' && !host.endsWith('.pengzhan.dev')) {
window['ga-disable-${trackingId}'] = true;
window.dataLayer = window.dataLayer || [];
window.gtag = function(){};
return;
}
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
window.gtag = gtag;
gtag('js', new Date());
gtag('config', '${trackingId}');
})();
</script>
`;
}
};