Skip to content

Latest commit

 

History

15 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

🛰️ QLYNK PRODUCTION™ — Static Edge Pipeline

An educational, open-source demo of a GitHub Actions → Cloudflare Pages static-asset pipeline, with a branded 403 page, a live "edge dashboard," and full documentation of every moving part.

Made for Education Open Source PRs Welcome Status Built with


Important

⚠️ Read this before you clone/deploy anything

This repository is published strictly for learning purposes — to show, in full public view, how a small automation pipeline (GitHub Actions + Python + Cloudflare Pages) can be assembled. It is not a commercial product, it is not an official Cloudflare or GitHub project, and it makes no claim to be a real CDN (see § This Pipeline vs. an Actual CDN).

Deep Dey (the author) is not responsible for any misuse of this code — including but not limited to: deploying it in production without understanding it, using it to host or distribute content that violates GitHub's or Cloudflare's terms of service, bypassing rate limits, disguising malicious payloads as "static assets," or any other use that breaks the law or a third party's terms. You use this at your own risk, "as is," with no warranty. See § License & Liability.


📚 Table of Contents

1. What This Actually Is 8. ToS / Policy Links You Must Actually Read 15. Command Cheatsheet
2. Architecture Diagram 9. Environment, Secrets & Permissions 16. Troubleshooting
3. Repository Structure 10. Limits & Quotas 17. FAQ
4. Visual Design, Effects & Animations 11. The 403 Branded Page 18. This Pipeline vs. an Actual CDN
5. Step-by-Step Setup Guide 12. The Live Dashboard 19. Contributing / How to PR
6. The GitHub Action 13. config.json — The Ledger 20. License & Liability (Plain English)
7. Deployment Guide — Cloudflare Pages 14. The /dumb Quarantine Folder 21. Credits

1. What This Actually Is

This repo is a static-asset pipeline that sits in front of Cloudflare Pages, built from four independent pieces that run every time you push a commit:

Piece File Job
🔔 Trigger .github/workflows/normalize-static.yml Fires on every push, on every branch.
🐍 Worker .github/scripts/normalize_static.py Cleans filenames, quarantines oversized files, updates the ledger.
📒 Ledger config.json JSON record of every file currently published under /static.
🎨 Skin index.html + dashboard/index.html A fully custom, branded "edge network" 403 page and a password-gated file dashboard.

None of these pieces are Cloudflare's CDN. They are automation and presentation built on top of Cloudflare's real infrastructure (Cloudflare Pages / global edge network). That distinction is spelled out fully in §18 — worth reading before you tell anyone this "is a CDN," because it isn't one on its own.


2. Architecture Diagram

flowchart TD
    A["👤 You push a commit\n(any branch, any file)"] --> B["⚙️ GitHub Actions\nnormalize-static.yml\n(disposable runner VM)"]
    B --> C["🐍 normalize_static.py\n• walk /static\n• clean filenames\n• quarantine files > 25MB → /dumb\n• update config.json"]
    C --> D{changed == true?}
    D -- No --> E["🛑 Workflow ends\n(no commit, no noise)"]
    D -- Yes --> F["📝 git commit + push\n'chore: normalize static assets [skip ci]'"]
    F --> G["☁️ Cloudflare Pages build\n(separate system, Cloudflare-owned)"]
    G --> G2["🔑 node scripts/build.js\ninjects CONFIG + PASSWORD\nsecrets into dashboard/index.html"]
    G2 --> H["🧹 Cleanup deletes /dumb and /.github\n(keeps index.html, config.json,\nstatic/, dashboard/, scripts/)"]
    H --> I["🌍 Remaining files uploaded to\nCloudflare's real global edge network\n(THIS is the actual CDN)"]

    style A fill:#1c2430,stroke:#00f0c0,color:#fff
    style I fill:#0a5c4d,stroke:#00f0c0,color:#fff
    style E fill:#7a0f1f,stroke:#ff2b4d,color:#fff
Loading

Two systems, two owners, back-to-back:

  1. GitHub Actions (GitHub's infra) → does the bookkeeping: renaming, size-checking, metadata.
  2. Cloudflare Pages (Cloudflare's infra) → does the actual delivery: build, upload, edge caching, global distribution.

This repo's code only touches step 1. Step 2 is entirely Cloudflare's own infrastructure, triggered automatically once you connect the GitHub repo to a Cloudflare Pages project.


3. Repository Structure

your-repo/
├── .github/
│   ├── workflows/
│   │   └── normalize-static.yml      ← trigger (§6)
│   └── scripts/
│       └── normalize_static.py       ← worker (§6)
│
├── static/                            ← your real, public static assets
│   ├── logo.png
│   ├── banner-image.jpg
│   └── ...
│
├── dumb/                               ← auto-created quarantine for oversized files
│   └── (anything > 25MB lands here, never served)
│
├── dashboard/
│   └── index.html                     ← password-gated live file dashboard (§12)
│
├── config.json                        ← auto-maintained metadata ledger (§13)
├── index.html                          ← branded 403 "edge network" page (§11)
├── scripts/
│   └── build.js                        ← build-time secrets injector, preserved through cleanup (§7, §9)
├── .assetsignore                       ← Cloudflare Pages asset-ignore hints
└── README.md                           ← this file

Everything under .github/ and dumb/ is meant to stay out of the live deployment — enforced at the Cloudflare build-command layer, not by .gitignore (see §7).


4. Visual Design, Effects & Animations 🎬

The front-end pages (index.html, dashboard/index.html) are hand-built with pure CSS + vanilla JS — no framework. Here's every animated/interactive effect actually implemented, for anyone studying or reusing the techniques:

Effect Where How it works
Canvas particle field index.html A lightweight <canvas> loop (34 particles) drawn with requestAnimationFrame; pauses automatically when the tab is hidden to save CPU/battery.
CRT-style scanline + noise index.html @keyframes noiseShift / scanMove — an SVG feTurbulence filter animated via steps() for a retro terminal look.
Typewriter terminal text index.html @keyframes typing using steps(48, end) on a width transition, plus a separate caretBlink keyframe for the blinking cursor.
Glitch text layers index.html Two duplicated text layers (layer-r / layer-c) animated with mix-blend-mode:screen and offset steps() keyframes to fake a chromatic-aberration glitch.
Rotating shield rings index.html Two concentric SVG rings, spin / spinRev keyframes rotating opposite directions at different speeds (40s / 55s) around a shared transform-origin.
Animated dashed connection line index.html stroke-dashoffset animated via @keyframes dash to simulate a moving data packet along an SVG path.
Pulse rings / status dot index.html @keyframes pulseRing and pulseDot — scale + opacity fades that mimic a radar "ping."
Scrolling ticker index.html (footer) @keyframes tickerScroll — a transform:translateX() loop for the marquee-style status ticker.
Live network diagnostics index.html Real (not fake) call to Cloudflare's public /cdn-cgi/trace endpoint + a DNS-over-HTTPS lookup (cloudflare-dns.com/dns-query) to show your real IP / edge PoP / latency.
Reduced-motion & low-power fallback index.html @media (prefers-reduced-motion: reduce) and a JS lightMode feature-detect (touch / low-memory devices) disable all of the above automatically.
Password-gated dashboard reveal dashboard/index.html Simple gate before rendering; files rendered as cards (name, type badge, relative upload time converted to the viewer's local timezone).
File preview modal dashboard/index.html Clicking a card opens a modal; images/video/audio preview inline based on extension, everything else falls back to a generic icon + download link.
Live "Edge Status" strip dashboard/index.html Same /cdn-cgi/trace technique as the 403 page — visitor IP, edge location, latency, server vs. local clock, all genuinely live per-request data.

Performance notes (from the project's own changelog): custom cursors, cursor-trails, mouse-glow effects, and backdrop-filter: blur() were deliberately removed in a later pass because they caused jank on mobile/low-end devices — everything above only uses transform/opacity so the browser can composite it on the GPU without triggering layout thrashing.


5. Step-by-Step Setup Guide

  1. Fork or clone this repository into your own GitHub account.
  2. Create a static/ folder at the repo root (if it isn't already there) and drop your real public assets into it.
  3. Do nothing else in GitHub Actionsnormalize-static.yml already has permissions: contents: write and uses the auto-issued GITHUB_TOKEN, so no personal access token or secret is required for the normalizer itself.
  4. Push a commit. The workflow runs automatically, cleans filenames, quarantines anything over 25MB into /dumb, and updates config.json.
  5. Create a Cloudflare account (free tier is enough to start) and a Pages project, connecting it to this same GitHub repo. See Cloudflare's official Pages docs for the click-by-click flow.
  6. Add two Environment Variables (as Secrets) under Cloudflare Pages → Settings → Environment variables — see §9 for exactly what each one does.
  7. Set the Build command in Cloudflare Pages → Settings → Builds & deployments to the full node scripts/build.js && ... one-liner in §7 — this both injects your secrets into the dashboard and strips /dumb and .github/ before anything goes live.
  8. Set the Build output directory to / (repo root), since this project serves flat, no separate dist/ folder.
  9. (Optional) Point a custom domain at the Pages project, and update CDN_DOMAIN inside .github/scripts/normalize_static.py to match it, so config.json generates correct public URLs.
  10. Push again — Cloudflare will auto-deploy on every push once the connection is live. Watch the Actions tab and the Cloudflare Pages deployment log at the same time to see both halves of the pipeline run back-to-back.
  11. Verify using the checks in §16 Troubleshooting — confirm /dumb/ and /.github/ both 404 on the live domain, and that the dashboard/403 page load correctly.

6. The GitHub Action

.github/workflows/normalize-static.yml runs on every push, on every branch, with a concurrency guard so rapid pushes don't race each other, and only commits back to the repo when the Python worker actually changed something (if: steps.normalize.outputs.changed == 'true'). The worker itself (normalize_static.py) is fully idempotent — running it twice with nothing changed produces changed=false both times — uses atomic file moves (os.replace, never copy+delete), never makes a network call, and preserves each file's original, first-seen filename forever in config.json even after the on-disk name is cleaned/renamed many times over.


7. Deployment Guide — Cloudflare Pages

Cloudflare Pages → Settings → Builds & deployments → Build configuration → Build command:

node scripts/build.js && T0=$(date +%s.%N); FILES=$(find . -mindepth 1 -maxdepth 1 ! -name index.html ! -name config.json ! -name static ! -name dashboard ! -name scripts); if [ -n "$FILES" ]; then BYTES=$(du -cb $FILES 2>/dev/null | tail -1 | awk '{print $1}'); else BYTES=0; fi; COUNT=$(printf '%s\n' "$FILES" | grep -c .); MB=$(awk -v b="${BYTES:-0}" 'BEGIN{printf "%.2f", b/1048576}'); GB=$(awk -v b="${BYTES:-0}" 'BEGIN{printf "%.3f", b/1073741824}'); NAMES=$(printf '%s\n' "$FILES" | xargs -n1 basename 2>/dev/null | paste -sd, -); rm -rf $FILES; T1=$(date +%s.%N); DUR=$(awk -v a="$T0" -v b="$T1" 'BEGIN{printf "%.2f", b-a}'); echo "✅ Deleted $COUNT item(s) [$NAMES] — freed ${MB}MB (${GB}GB) in ${DUR}s"

This is two things chained with &&:

  1. node scripts/build.js runs first — a small secrets-injector (see §9 and §12) that reads the PASSWORD and CONFIG environment variables, hashes/encodes them, and writes the result into dashboard/index.html in place of two placeholder tokens (__PASSWORD_HASH__ and __CONFIG_SRC_B64__). If either variable is missing, this step fails loudly and the whole build stops — which is intentional, so a misconfigured project can't silently deploy with a broken dashboard.
  2. The rm -rf cleanup then runs inside Cloudflare's temporary, disposable build clone — never your real GitHub repo — deleting everything at the repo root except index.html, config.json, static/, dashboard/, and scripts/. That specifically preserves dashboard/ (so the file dashboard still deploys) while still wiping .github/ and /dumb before upload. Neither .gitignore (only affects new/untracked files) nor .assetsignore (only honored by Wrangler/Workers-style deploys, not the dashboard git-integration pipeline) reliably solves this on their own, which is why the build-command deletion exists.

⚠️ Order matters: build.js must run before the cleanup, since it writes into dashboard/index.html — reversing the order, or forgetting the &&, would either inject secrets after the folder no longer needs them or (if scripts/ weren't preserved) delete build.js before it ever runs.


8. ToS / Policy Links You Must Actually Read

This project runs entirely on top of two other companies' free-tier infrastructure — using it means you've agreed to their rules, not just this repo's README. Read these before deploying anything real:

Cloudflare's free CDN is meant for normal website assets (HTML/CSS/JS/images) — it explicitly restricts using the free CDN as a video-hosting or bulk-large-file service. This pipeline's 25MB quarantine step exists partly because of that policy, not despite it.


9. Environment, Secrets & Permissions

GitHub Actions side

  • No custom secrets required for the normalizer — it uses GitHub's own short-lived, auto-rotated GITHUB_TOKEN, scoped only to this repo, only for the duration of the run.
  • permissions: contents: write is explicitly escalated in the workflow because the default token is read-only; without it, the final git push fails with a GitHub-side 403 (nothing to do with Cloudflare).

Cloudflare Pages side — required Environment Variables

Set both of these under Cloudflare Pages → Settings → Environment variables, as Secret type (not plaintext), for whichever environment(s) you deploy (Production/Preview):

Type Name Value What it's for
🔒 Secret CONFIG The full URL of your deployed config.json (e.g. https://cdn.example.com/config.json) Read by scripts/build.js at build time, base64-encoded (obfuscated, not encryption — just kept off a plain view-source scan), and written into dashboard/index.html, where the dashboard atob()-decodes it at runtime to know where to fetch the file ledger from.
🔒 Secret PASSWORD Your real, plaintext dashboard password Read by scripts/build.js, hashed with SHA-256, and written into dashboard/index.html — only the hash ever lands in the deployed HTML, never the plaintext password.

Both show as Secret type with the value permanently masked (Value encrypted) in the Cloudflare dashboard once saved — there's no way to view a saved secret's plaintext again through the UI, only overwrite or delete it.

Because these are build-time-only variables consumed by node scripts/build.js (see §7), the build will fail on purpose if either CONFIG or PASSWORD is missing — this is a deliberate fail-safe so you can't accidentally ship a dashboard with a broken config source or no password gate at all.

Never commit the real values of CONFIG or PASSWORD into git, into .assetsignore, or into any comment/placeholder in dashboard/index.html — only the two placeholder tokens (__PASSWORD_HASH__, __CONFIG_SRC_B64__) should ever appear in the committed source; the real values live exclusively in Cloudflare's encrypted environment-variable store.


10. Limits & Quotas

Limit Value Source
Max single asset size (Cloudflare Pages) 25 MiB Cloudflare Pages limits
Files per site (Free plan) 20,000 Cloudflare Pages limits
Custom domains per project (Free plan) 100 Cloudflare Pages limits
GitHub Actions workflow runs Subject to plan-based minutes/concurrency caps GitHub Actions limits

This is also why the pipeline's MAX_BYTES constant is set to 25MB in normalize_static.py — it mirrors Cloudflare's own real ceiling, defined in exactly one place so it's a single-line change if that number ever changes.


11. The 403 Branded Page

index.html is a fully custom "edge network access denied" themed page (see §4 for every effect used inside it). It reads real, live data from Cloudflare's public /cdn-cgi/trace endpoint (visitor IP, edge PoP, latency) rather than hardcoding any of it — meaning two visitors from two different networks will see genuinely different values.


12. The Live Dashboard

dashboard/index.html is a password-gated viewer for whatever is currently tracked in config.json: it renders each file as a card (name, type badge, relative upload time converted into the viewer's local timezone), and clicking a card opens a preview modal (inline image/video/audio where applicable, a generic icon + download link otherwise). It shares the same live Cloudflare edge-diagnostics strip as the 403 page.

Before deploy, the committed source only contains two placeholder tokens — __PASSWORD_HASH__ and __CONFIG_SRC_B64__. Neither the real dashboard password nor the real config.json URL is ever committed to git. At build time, scripts/build.js (see §7 and §9) swaps those two tokens for a SHA-256 password hash and a base64-encoded config URL, respectively, and only that built version is what actually gets deployed to Cloudflare's edge.


13. config.json — The Ledger

Every tracked file gets one entry keyed by its cleaned filename:

{
  "logo.png": {
    "filename": "Logo (Final Final v2).PNG",
    "path": "/static/logo.png",
    "pushed_at": "22nd Aug 2026 | 8:16 pm",
    "uploaded_at": "1755856560",
    "url": "https://cdn.example.com/static/logo.png"
  }
}

filename is frozen at its first-seen value forever (a durable audit trail of the original messy upload name); path/url/timestamps update whenever the file actually changes. Entries that haven't changed are left byte-for-byte identical run over run, so git diff on this file always shows exactly what changed in a given push — nothing more.


14. The /dumb Quarantine Folder

Files over 25MB are moved, not deleted, into /dumb — atomically, so nothing is ever lost or duplicated mid-move. This happens on GitHub's infrastructure, before Cloudflare ever sees the file, so an oversized asset can't silently break an entire deploy. /dumb itself never reaches the live site because of the Cloudflare build-command cleanup in §7.


15. Command Cheatsheet

# Run the normalizer locally (no network calls, safe to test)
python3 .github/scripts/normalize_static.py

# Force the 25MB quarantine branch to fire, for testing
dd if=/dev/zero of=static/big-test-file.bin bs=1M count=26
python3 .github/scripts/normalize_static.py
ls dumb/      # big-test-file.bin should now be here

# Confirm the workflow triggers on a non-main branch
git checkout -b test-branch
echo "test" > static/probe-file.txt
git add static && git commit -m "test: probe workflow trigger"
git push -u origin test-branch

16. Troubleshooting

Symptom Likely cause Fix
git push step fails with 403 Missing permissions: contents: write Add it to the workflow YAML.
Deploy fails on Cloudflare with "file too large" A file slipped past normalization (e.g. added directly through the Cloudflare dashboard, not git) Re-run the normalizer, or manually move the file into /dumb.
Build fails immediately with ERROR: PASSWORD environment variable is not set (or CONFIG) One or both secrets are missing from Cloudflare Pages → Settings → Environment variables Add both as Secret type exactly as shown in §9, then retry the deployment.
Dashboard loads but never shows any files CONFIG secret points to the wrong URL, or config.json isn't actually reachable at that URL Confirm the CONFIG value resolves to your real, deployed config.json, not a local path.
config.json diff is huge/noisy every run Timestamps regenerating for untouched files Confirm you're on the version of the script that only rewrites entries when path/url actually changed.
/dumb/ or /.github/ are publicly reachable Build command cleanup step missing/misconfigured Re-check the exact Build command in Cloudflare Pages settings against §7.
Two files collide to the same clean name e.g. Photo 1.PNG and photo-1.png both clean to photo-1.png Known sharp edge — rename one of the source files before pushing; a hash-suffix fallback is on the roadmap.

17. FAQ

Is this an official Cloudflare or GitHub product? No. It's a third-party, independent, educational automation script built on top of their public infrastructure.

Can I use this in production? You can, but you're expected to read and understand every part of it first (that's the whole point of this README) and to comply with both platforms' terms linked in §8.

Does the normalizer ever talk to the network? No — it's pure local filesystem + JSON. Only the front-end pages (index.html, dashboard/index.html) make live calls, and only to Cloudflare's own public diagnostic endpoints.

What happens to a file over 25MB? It's moved (not deleted) to /dumb, and any config.json entry claiming it's still live under /static is removed in the same run.


18. This Pipeline vs. an Actual CDN

This repo An actual CDN (Cloudflare's network)
Owns physical edge servers? ❌ No ✅ Yes, globally distributed
Does request routing / edge caching? ❌ No ✅ Yes
Renames files, tracks metadata? ✅ Yes ❌ Not its job
What it actually is A pre-processing + bookkeeping layer that runs before your files reach Cloudflare The real delivery network that Cloudflare operates

In short: this project automates getting files ready for a CDN. It is not, and does not claim to be, the CDN itself.


19. Contributing / How to PR

This project is open source and welcomes contributions from anyone — bug fixes, new features, better docs, translated versions, accessibility improvements, all of it.

  1. Fork the repo.
  2. Create a branch: git checkout -b fix/short-description.
  3. Make your change. Keep the normalizer's design properties intact (idempotent, atomic, no network calls) unless your PR is specifically about changing that.
  4. Test locally using the Command Cheatsheet above.
  5. Open a Pull Request against main, describing what changed and why.
  6. Be patient — this is a community/education project, reviewed on a best-effort basis.

Issues, feature requests, and "this section of the README is unclear" reports are all welcome too — open a GitHub Issue.


20. License & Liability (Plain English)

This project is released under the MIT License (add/keep a LICENSE file with the full text in the repo root — see choosealicense.com/licenses/mit for the exact wording). In short:

  • ✅ You can use, copy, modify, merge, publish, and distribute this code, for free, including commercially.
  • ✅ You must keep the original copyright/credit notice in copies or substantial portions of the code.
  • ❌ It comes with no warranty of any kind. Use it at your own risk.
  • The author (Deep Dey) is not liable for any damages or consequences arising from the use, misuse, or inability to use this software — including violations of GitHub's or Cloudflare's own terms of service, data loss, downtime, or misuse for anything illegal or malicious.

This repository is shared strictly for educational and learning purposes — to demonstrate CI/CD automation, static-site pipelines, and front-end animation techniques in the open. It is not intended as a turnkey production CDN replacement, and it is not to be used to host, mirror, or distribute content that violates any applicable law or any third party's terms of service (including GitHub's and Cloudflare's, linked in §8).


21. Credits

Developed by Deep Dey.

Automation scripting, documentation drafting, and debugging assistance provided with Claude (Anthropic) — included here for transparency, not as a claim of authorship over the project's direction, branding, or purpose, all of which belong to Deep Dey.

Badges via shields.io (open source, free to use/embed in any README).


⭐ If this helped you learn something, consider starring the repo — and remember: education first, misuse never.

About

This repo gives an example how we can make free cdn server in cloudflare pages free plan. this is strictly for educational purposes so don't misuse this code, as Deep Dey will not responsible for it! also it opensource so you can make any update by PR, also you can use on your own workshop but make sure you give an credit of myself with insta link!

Topics

Resources

Stars

1 star

Watchers

1 watching

Forks

Releases

Sponsor this project

Contributors

Languages