commit 56b728dc492925af4948d46857fd54a7332b5d06 Author: rezayaghobi-dev Date: Mon Oct 19 10:19:26 2026 +0330 Build personal DevOps portfolio site Static HTML/CSS/JS site with local fonts and icons, certificate gallery, work section and homelab diagrams, ready for rezayaghobi.ir. 🤖 Generated with Codebuff Co-Authored-By: Codebuff diff --git a/README.md b/README.md new file mode 100644 index 0000000..883b802 --- /dev/null +++ b/README.md @@ -0,0 +1,112 @@ +# Reza Yaghobi — personal site + +A static, single-page portfolio for `rezayaghobi.ir`. Plain HTML, CSS and one small +JavaScript file. There is no build step and the page makes no external requests. + +## Preview locally + +From this directory: + +```bash +python3 -m http.server 8080 +``` + +Open . + +## Structure + +``` +index.html all content and both inline SVG diagrams +favicon.svg "RY" monogram +robots.txt +assets/style.css design tokens, layout, components, motion, responsive rules +assets/brand.css generated per-tile brand colours (used for tile hover borders) +assets/app.js progress line, section reveals, diagram line drawing +assets/boot.js adds .js to before render +assets/fonts/ two latin variable woff2 files +assets/icons/ 33 tool icons (simple-icons + neutral monograms) +assets/img/ screenshots, certificate images (WebP), og.png +tools/build-icons.mjs regenerates the icon set and assets/brand.css +tools/og.html template used to render the Open Graph image +``` + +## Fonts + +The fonts are copied from the Fontsource packages, latin variable weight files only: + +- `@fontsource-variable/bricolage-grotesque` → + `bricolage-grotesque-latin-wght-normal.woff2` +- `@fontsource-variable/instrument-sans` → + `instrument-sans-latin-wght-normal.woff2` + +They are declared with `font-display: swap` and a system fallback stack. Small +labels use the system monospace stack. + +## Icons + +Icons are generated from [`simple-icons`](https://simpleicons.org/) in a scratch +folder outside this site. Install it once: + +```bash +mkdir -p /tmp/ry-scratch +cd /tmp/ry-scratch +npm init -y +npm install simple-icons +``` + +Then, from this project: + +```bash +node tools/build-icons.mjs +# or point at another install: +SIMPLE_ICONS_DIR=/path/to/node_modules/simple-icons node tools/build-icons.mjs +``` + +The script reads each icon's slug, path and brand hex from `simple-icons`, then: + +- writes `assets/icons/.svg` with the brand colour as `fill`; +- lightens any brand colour that falls below 3:1 contrast on the `#0a0d14` + background (Helm and Elasticsearch needed this); +- writes `assets/brand.css` with one `--brand` custom property per tile; +- writes a neutral monogram tile for tools simple-icons does not include: + HAProxy, Keepalived, Argo CD, GitOps, Nexus Repository, Loki, Alloy, Zabbix, + C# and Microsoft SQL Server. These are plain letters, not imitations of logos. + +## Images + +`assets/img/pipeline.png` and `assets/img/dashboard.png` are copies of the supplied +screenshots. The page uses 1200 px wide WebP versions generated with Pillow: + +```python +from PIL import Image +im = Image.open("images/pipline-green.png").convert("RGB") +im.resize((1200, round(im.height * 1200 / im.width)), Image.LANCZOS).save( + "assets/img/pipeline-1200.webp", "WEBP", quality=82, method=6 +) +``` + +Repeat for `images/dashboard.png`. + +The three ArvanCloud certificate images are converted from `images/Associate.jpg`, +`images/professional.png` and `images/expert.png` to 1000 px wide WebP at quality +80: `cert-associate.webp`, `cert-professional.webp` and `cert-expert.webp`. The +full-size WebP opens in a new tab when a thumbnail is clicked. + +## Open Graph image + +`assets/img/og.png` is a 1200 × 630 render of `tools/og.html`. With the preview +server running: + +```bash +google-chrome --headless=new --hide-scrollbars --window-size=1200,630 \ + --screenshot=assets/img/og.png http://localhost:8080/tools/og.html +``` + +## Notes + +- Section reveals, hero fades and the staggered tile/row fades are gated behind + classes added by `app.js`, so the page remains fully visible if JavaScript is + disabled or fails. +- All motion is disabled under `prefers-reduced-motion: reduce`. +- The pipeline diagram uses the stage names from the devops-test repository + (`test`, `build`, `publish`, `deploy`) rather than invented steps. diff --git a/assets/app.js b/assets/app.js new file mode 100644 index 0000000..bdbab02 --- /dev/null +++ b/assets/app.js @@ -0,0 +1,63 @@ +/* Progress line, reveals, diagram draw. No dependencies. */ +(function () { + var root = document.documentElement; + root.classList.add("anims"); + var reduce = window.matchMedia("(prefers-reduced-motion: reduce)").matches; + var canObserve = "IntersectionObserver" in window; + + function reveal(target, threshold) { + var nodes = [].slice.call( + typeof target == "string" ? document.querySelectorAll(target) : target + ); + if (!canObserve || reduce) { + nodes.forEach(function (n) { n.classList.add("is-in"); }); + return; + } + var io = new IntersectionObserver(function (entries) { + entries.forEach(function (entry) { + if (!entry.isIntersecting) return; + entry.target.classList.add("is-in"); + io.unobserve(entry.target); + }); + }, { threshold: threshold, rootMargin: "0px 0px -6% 0px" }); + nodes.forEach(function (n) { io.observe(n); }); + } + + var bar = document.querySelector(".progress"); + var waiting = false; + var groups = document.querySelectorAll( + ".tiles, .chips, .ladder, .courses, .cert-gallery, .work-list, .timeline, .contact-rows" + ); + + function paint() { + waiting = false; + if (!bar) return; + var max = document.documentElement.scrollHeight - window.innerHeight; + var p = max > 0 ? window.scrollY / max : 0; + bar.style.transform = "scaleX(" + Math.min(1, Math.max(0, p)) + ")"; + } + + function queue() { + if (waiting) return; + waiting = true; + window.requestAnimationFrame(paint); + } + + try { + [].forEach.call(groups, function (group) { + [].forEach.call(group.children, function (item, i) { + item.style.setProperty("--i", Math.min(i, 6)); + }); + }); + reveal(".reveal", 0.08); + reveal(".diagram", 0.22); + reveal(groups, 0.12); + if (bar && !reduce) { + window.addEventListener("scroll", queue, { passive: true }); + window.addEventListener("resize", queue, { passive: true }); + paint(); + } + } catch (err) { + root.classList.remove("anims"); + } +})(); diff --git a/assets/boot.js b/assets/boot.js new file mode 100644 index 0000000..7555376 --- /dev/null +++ b/assets/boot.js @@ -0,0 +1 @@ +document.documentElement.classList.add("js"); diff --git a/assets/brand.css b/assets/brand.css new file mode 100644 index 0000000..7d68ffe --- /dev/null +++ b/assets/brand.css @@ -0,0 +1,35 @@ +/* Generated by tools/build-icons.mjs. Do not edit by hand. */ +/* Brand colours are lightened where needed to reach 3:1 on #0a0d14. */ +.brand--docker{--brand:#2496ed} +.brand--kubernetes{--brand:#326ce5} +.brand--rancher{--brand:#0075a8} +.brand--helm{--brand:#6267b2} +.brand--linux{--brand:#fcc624} +.brand--vmware{--brand:#607078} +.brand--traefik{--brand:#24a1c1} +.brand--nginx{--brand:#009639} +.brand--minio{--brand:#c72e49} +.brand--ceph{--brand:#ef5c55} +.brand--gitlab{--brand:#fc6d26} +.brand--gitlab-ci{--brand:#fc6d26} +.brand--git{--brand:#f03c2e} +.brand--ansible{--brand:#ee0000} +.brand--terraform{--brand:#844fba} +.brand--bash{--brand:#4eaa25} +.brand--prometheus{--brand:#e6522c} +.brand--grafana{--brand:#f46800} +.brand--elasticsearch{--brand:#1e6982} +.brand--python{--brand:#3776ab} +.brand--postgresql{--brand:#4169e1} +.brand--mysql{--brand:#4479a1} +.brand--keras{--brand:#d00000} +.brand--haproxy{--brand:#9aa7bc} +.brand--keepalived{--brand:#9aa7bc} +.brand--argocd{--brand:#9aa7bc} +.brand--gitops{--brand:#9aa7bc} +.brand--nexus{--brand:#9aa7bc} +.brand--loki{--brand:#9aa7bc} +.brand--alloy{--brand:#9aa7bc} +.brand--zabbix{--brand:#9aa7bc} +.brand--csharp{--brand:#9aa7bc} +.brand--mssql{--brand:#9aa7bc} diff --git a/assets/fonts/bricolage-grotesque-latin-wght-normal.woff2 b/assets/fonts/bricolage-grotesque-latin-wght-normal.woff2 new file mode 100644 index 0000000..82152a4 Binary files /dev/null and b/assets/fonts/bricolage-grotesque-latin-wght-normal.woff2 differ diff --git a/assets/fonts/instrument-sans-latin-wght-normal.woff2 b/assets/fonts/instrument-sans-latin-wght-normal.woff2 new file mode 100644 index 0000000..8611e41 Binary files /dev/null and b/assets/fonts/instrument-sans-latin-wght-normal.woff2 differ diff --git a/assets/icons/alloy.svg b/assets/icons/alloy.svg new file mode 100644 index 0000000..a5bad20 --- /dev/null +++ b/assets/icons/alloy.svg @@ -0,0 +1 @@ +AL diff --git a/assets/icons/ansible.svg b/assets/icons/ansible.svg new file mode 100644 index 0000000..78c64f5 --- /dev/null +++ b/assets/icons/ansible.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/argocd.svg b/assets/icons/argocd.svg new file mode 100644 index 0000000..bd7d5a6 --- /dev/null +++ b/assets/icons/argocd.svg @@ -0,0 +1 @@ +AC diff --git a/assets/icons/bash.svg b/assets/icons/bash.svg new file mode 100644 index 0000000..df3d089 --- /dev/null +++ b/assets/icons/bash.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/ceph.svg b/assets/icons/ceph.svg new file mode 100644 index 0000000..eca3ac9 --- /dev/null +++ b/assets/icons/ceph.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/csharp.svg b/assets/icons/csharp.svg new file mode 100644 index 0000000..ea45606 --- /dev/null +++ b/assets/icons/csharp.svg @@ -0,0 +1 @@ +C# diff --git a/assets/icons/docker.svg b/assets/icons/docker.svg new file mode 100644 index 0000000..f3194d1 --- /dev/null +++ b/assets/icons/docker.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/elasticsearch.svg b/assets/icons/elasticsearch.svg new file mode 100644 index 0000000..4cb9bf5 --- /dev/null +++ b/assets/icons/elasticsearch.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/git.svg b/assets/icons/git.svg new file mode 100644 index 0000000..bb362f4 --- /dev/null +++ b/assets/icons/git.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/gitlab-ci.svg b/assets/icons/gitlab-ci.svg new file mode 100644 index 0000000..f211ab1 --- /dev/null +++ b/assets/icons/gitlab-ci.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/gitlab.svg b/assets/icons/gitlab.svg new file mode 100644 index 0000000..f211ab1 --- /dev/null +++ b/assets/icons/gitlab.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/gitops.svg b/assets/icons/gitops.svg new file mode 100644 index 0000000..1a6c681 --- /dev/null +++ b/assets/icons/gitops.svg @@ -0,0 +1 @@ +GO diff --git a/assets/icons/grafana.svg b/assets/icons/grafana.svg new file mode 100644 index 0000000..0ca5f3f --- /dev/null +++ b/assets/icons/grafana.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/haproxy.svg b/assets/icons/haproxy.svg new file mode 100644 index 0000000..1aad475 --- /dev/null +++ b/assets/icons/haproxy.svg @@ -0,0 +1 @@ +HA diff --git a/assets/icons/helm.svg b/assets/icons/helm.svg new file mode 100644 index 0000000..aed160b --- /dev/null +++ b/assets/icons/helm.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/keepalived.svg b/assets/icons/keepalived.svg new file mode 100644 index 0000000..c99115f --- /dev/null +++ b/assets/icons/keepalived.svg @@ -0,0 +1 @@ +KP diff --git a/assets/icons/keras.svg b/assets/icons/keras.svg new file mode 100644 index 0000000..e9296da --- /dev/null +++ b/assets/icons/keras.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/kubernetes.svg b/assets/icons/kubernetes.svg new file mode 100644 index 0000000..b17bcd1 --- /dev/null +++ b/assets/icons/kubernetes.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/linux.svg b/assets/icons/linux.svg new file mode 100644 index 0000000..056c5fa --- /dev/null +++ b/assets/icons/linux.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/loki.svg b/assets/icons/loki.svg new file mode 100644 index 0000000..58922c9 --- /dev/null +++ b/assets/icons/loki.svg @@ -0,0 +1 @@ +LO diff --git a/assets/icons/minio.svg b/assets/icons/minio.svg new file mode 100644 index 0000000..aa77793 --- /dev/null +++ b/assets/icons/minio.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/mssql.svg b/assets/icons/mssql.svg new file mode 100644 index 0000000..2f8cac1 --- /dev/null +++ b/assets/icons/mssql.svg @@ -0,0 +1 @@ +SQL diff --git a/assets/icons/mysql.svg b/assets/icons/mysql.svg new file mode 100644 index 0000000..d8c79e5 --- /dev/null +++ b/assets/icons/mysql.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/nexus.svg b/assets/icons/nexus.svg new file mode 100644 index 0000000..d5c7c5b --- /dev/null +++ b/assets/icons/nexus.svg @@ -0,0 +1 @@ +NX diff --git a/assets/icons/nginx.svg b/assets/icons/nginx.svg new file mode 100644 index 0000000..a98706e --- /dev/null +++ b/assets/icons/nginx.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/postgresql.svg b/assets/icons/postgresql.svg new file mode 100644 index 0000000..05d8970 --- /dev/null +++ b/assets/icons/postgresql.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/prometheus.svg b/assets/icons/prometheus.svg new file mode 100644 index 0000000..da202da --- /dev/null +++ b/assets/icons/prometheus.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/python.svg b/assets/icons/python.svg new file mode 100644 index 0000000..bfca86b --- /dev/null +++ b/assets/icons/python.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/rancher.svg b/assets/icons/rancher.svg new file mode 100644 index 0000000..1421ded --- /dev/null +++ b/assets/icons/rancher.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/terraform.svg b/assets/icons/terraform.svg new file mode 100644 index 0000000..75748b6 --- /dev/null +++ b/assets/icons/terraform.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/traefik.svg b/assets/icons/traefik.svg new file mode 100644 index 0000000..a9875e1 --- /dev/null +++ b/assets/icons/traefik.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/vmware.svg b/assets/icons/vmware.svg new file mode 100644 index 0000000..9e9c928 --- /dev/null +++ b/assets/icons/vmware.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/zabbix.svg b/assets/icons/zabbix.svg new file mode 100644 index 0000000..210048f --- /dev/null +++ b/assets/icons/zabbix.svg @@ -0,0 +1 @@ +ZB diff --git a/assets/img/cert-associate.webp b/assets/img/cert-associate.webp new file mode 100644 index 0000000..d0632e0 Binary files /dev/null and b/assets/img/cert-associate.webp differ diff --git a/assets/img/cert-expert.webp b/assets/img/cert-expert.webp new file mode 100644 index 0000000..846d606 Binary files /dev/null and b/assets/img/cert-expert.webp differ diff --git a/assets/img/cert-professional.webp b/assets/img/cert-professional.webp new file mode 100644 index 0000000..98f2814 Binary files /dev/null and b/assets/img/cert-professional.webp differ diff --git a/assets/img/dashboard-1200.webp b/assets/img/dashboard-1200.webp new file mode 100644 index 0000000..001d961 Binary files /dev/null and b/assets/img/dashboard-1200.webp differ diff --git a/assets/img/dashboard.png b/assets/img/dashboard.png new file mode 100644 index 0000000..7f3eb5d Binary files /dev/null and b/assets/img/dashboard.png differ diff --git a/assets/img/og.png b/assets/img/og.png new file mode 100644 index 0000000..5bd0ba2 Binary files /dev/null and b/assets/img/og.png differ diff --git a/assets/img/pipeline-1200.webp b/assets/img/pipeline-1200.webp new file mode 100644 index 0000000..c14924e Binary files /dev/null and b/assets/img/pipeline-1200.webp differ diff --git a/assets/img/pipeline.png b/assets/img/pipeline.png new file mode 100644 index 0000000..8a8041a Binary files /dev/null and b/assets/img/pipeline.png differ diff --git a/assets/style.css b/assets/style.css new file mode 100644 index 0000000..054bf42 --- /dev/null +++ b/assets/style.css @@ -0,0 +1,1197 @@ +/* --------------------------------------------------------------- + Reza Yaghobi — personal site + Tokens, base, layout, components, motion, responsive. + --------------------------------------------------------------- */ + +@font-face { + font-family: "Bricolage Grotesque"; + src: url("fonts/bricolage-grotesque-latin-wght-normal.woff2") format("woff2"); + font-weight: 200 800; + font-style: normal; + font-display: swap; +} + +@font-face { + font-family: "Instrument Sans"; + src: url("fonts/instrument-sans-latin-wght-normal.woff2") format("woff2"); + font-weight: 400 700; + font-style: normal; + font-display: swap; +} + +:root { + color-scheme: dark; + + --bg: #0a0d14; + --surface: #0e131c; + --surface-2: #121926; + --line: #232b3a; + --line-soft: #1a2130; + --text: #eef2f8; + --muted: #9aa7bc; + --faint: #7d8aa0; + --accent: #6fe3b0; + --accent-dim: rgba(111, 227, 176, 0.4); + + --display: "Bricolage Grotesque", "Instrument Sans", system-ui, -apple-system, + "Segoe UI", Roboto, Helvetica, Arial, sans-serif; + --sans: "Instrument Sans", system-ui, -apple-system, "Segoe UI", Roboto, + Helvetica, Arial, sans-serif; + --mono: ui-monospace, "SFMono-Regular", Menlo, Consolas, "Liberation Mono", + monospace; + + --wrap: 1120px; + --gutter: clamp(20px, 5vw, 48px); + --radius: 14px; + --radius-sm: 10px; + --ease: cubic-bezier(0.2, 0.6, 0.2, 1); +} + +*, +*::before, +*::after { + box-sizing: border-box; +} + +html { + scroll-padding-top: 24px; + -webkit-text-size-adjust: 100%; +} + +body { + margin: 0; + background: var(--bg); + color: var(--text); + font: 400 16px/1.65 var(--sans); + -webkit-font-smoothing: antialiased; + text-rendering: optimizeLegibility; + overflow-x: hidden; +} + +img, +svg { + max-width: 100%; +} + +img { + height: auto; +} + +a { + color: var(--accent); + text-decoration-thickness: 1px; + text-underline-offset: 0.2em; +} + +a:hover { + text-decoration-color: var(--accent); +} + +:focus-visible { + outline: 2px solid var(--accent); + outline-offset: 3px; + border-radius: 2px; +} + +::selection { + background: var(--accent); + color: #04120c; +} + +h1, +h2, +h3, +h4, +p, +ul, +ol, +figure { + margin: 0; +} + +ul, +ol { + list-style: none; + padding: 0; +} + +.wrap { + width: 100%; + max-width: var(--wrap); + margin-inline: auto; + padding-inline: var(--gutter); +} + +.skip-link { + position: absolute; + top: -100px; + left: 12px; + z-index: 100; + padding: 12px 18px; + border-radius: 8px; + background: var(--accent); + color: #04120c; + font: 600 14px/1 var(--sans); + text-decoration: none; +} + +.skip-link:focus { + top: 12px; +} + +/* ---------- progress line (JS only) ---------- */ + +.progress { + display: none; +} + +html.anims .progress { + display: block; + position: fixed; + inset: 0 0 auto 0; + height: 2px; + background: var(--accent); + transform: scaleX(0); + transform-origin: 0 50%; + z-index: 60; + pointer-events: none; +} + +/* ---------- header ---------- */ + +.site-header { + border-bottom: 1px solid var(--line-soft); +} + +.site-header__inner { + display: flex; + flex-wrap: wrap; + align-items: center; + justify-content: space-between; + gap: 12px 28px; + padding-block: 20px; +} + +.brand { + display: inline-flex; + align-items: center; + gap: 10px; + color: var(--text); + font: 600 16px/1 var(--display); + letter-spacing: -0.01em; + text-decoration: none; +} + +.brand::before { + content: ""; + width: 7px; + height: 7px; + border-radius: 50%; + background: var(--accent); +} + +.site-nav { + display: flex; + flex-wrap: wrap; + gap: 8px 22px; +} + +.site-nav a { + position: relative; + color: var(--muted); + font: 500 13px/1 var(--sans); + letter-spacing: 0.01em; + text-decoration: none; +} + +.site-nav a::after { + content: ""; + position: absolute; + right: 0; + bottom: -7px; + left: 0; + height: 1px; + background: var(--accent); + transform: scaleX(0); + transform-origin: left; + transition: transform 0.22s var(--ease); +} + +.site-nav a:hover, +.site-nav a:focus-visible { + color: var(--text); +} + +.site-nav a:hover::after, +.site-nav a:focus-visible::after { + transform: scaleX(1); +} + +/* ---------- hero ---------- */ + +.hero { + padding-block: clamp(64px, 12vw, 130px) clamp(56px, 9vw, 100px); +} + +.hero__label { + display: flex; + align-items: center; + gap: 10px; + margin-bottom: clamp(18px, 3vw, 28px); + color: var(--accent); + font: 500 11.5px/1.4 var(--mono); + letter-spacing: 0.16em; + text-transform: uppercase; +} + +.hero__label::before { + content: ""; + width: 26px; + height: 1px; + background: var(--accent); +} + +.hero__title { + margin-bottom: clamp(24px, 4vw, 36px); + font: 650 clamp(3.2rem, 12.5vw, 8rem)/0.9 var(--display); + letter-spacing: -0.035em; +} + +.hero__lede { + max-width: 56ch; + color: #b7c2d3; + font-size: clamp(16px, 2.2vw, 19px); + line-height: 1.6; + text-wrap: pretty; +} + +.hero__lede strong { + color: var(--text); + font-weight: 600; +} + +.hero__actions { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); + gap: 10px; + margin-top: clamp(32px, 5vw, 48px); +} + +.action { + display: flex; + flex-direction: column; + gap: 3px; + padding: 16px 18px; + border: 1px solid var(--line); + border-radius: var(--radius-sm); + background: var(--surface); + color: var(--text); + text-decoration: none; + transition: border-color 0.16s ease, background-color 0.16s ease; +} + +.action:hover { + border-color: var(--accent); + background: var(--surface-2); +} + +.action__label { + font: 600 14px/1.3 var(--sans); +} + +.action__meta { + overflow-wrap: anywhere; + color: var(--faint); + font: 400 11.5px/1.4 var(--mono); +} + +/* ---------- sections ---------- */ + +.section { + padding-block: clamp(56px, 8vw, 88px); + border-top: 1px solid var(--line-soft); +} + +.section__head { + display: flex; + align-items: baseline; + justify-content: space-between; + gap: 24px; + margin-bottom: clamp(26px, 4vw, 40px); +} + +.section__title { + font: 600 clamp(1.9rem, 4.6vw, 2.9rem)/1.05 var(--display); + letter-spacing: -0.025em; +} + +.section__note { + color: var(--faint); + font: 400 11px/1.5 var(--mono); + text-align: right; +} + +.section--stack .section__title, +.section--certs .section__title { + font-size: clamp(2.3rem, 6vw, 3.8rem); + font-weight: 650; +} + +/* ---------- stack ---------- */ + +.group-title { + display: flex; + align-items: center; + gap: 14px; + margin: 38px 0 16px; + color: var(--faint); + font: 500 11px/1.4 var(--mono); + letter-spacing: 0.14em; + text-transform: uppercase; +} + +.group-title:first-of-type { + margin-top: 0; +} + +.group-title::after { + content: ""; + flex: 1; + height: 1px; + background: var(--line-soft); +} + +.tiles { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 10px; +} + +.tile { + display: flex; + flex-direction: column; + align-items: flex-start; + justify-content: space-between; + gap: 18px; + min-height: 124px; + padding: 20px 16px 15px; + border: 1px solid var(--line); + border-radius: var(--radius-sm); + background: var(--surface); + transition: border-color 0.16s ease, background-color 0.16s ease, + transform 0.18s var(--ease); +} + +.tile:hover { + border-color: var(--brand, var(--accent)); + background: var(--surface-2); + transform: translateY(-2px); +} + +.tile__icon { + width: 38px; + height: 38px; + flex: none; +} + +.tile__name { + font: 500 13px/1.35 var(--sans); + letter-spacing: -0.005em; + overflow-wrap: anywhere; +} + +.tile__note { + display: block; + color: var(--faint); + font: 400 10.5px/1.4 var(--mono); +} + +.chips { + display: flex; + flex-wrap: wrap; + gap: 8px; +} + +.chip { + padding: 7px 12px; + border: 1px solid var(--line); + border-radius: 999px; + color: var(--muted); + font: 400 12.5px/1.4 var(--sans); +} + +/* ---------- certificates ---------- */ + +.cert-ielts { + display: flex; + flex-wrap: wrap; + align-items: baseline; + justify-content: space-between; + gap: 8px 24px; + padding: 20px 22px; + margin-bottom: 12px; + border: 1px solid var(--line); + border-left: 3px solid var(--accent); + border-radius: var(--radius-sm); + background: var(--surface); + color: var(--text); + font: 600 clamp(1.25rem, 2.6vw, 1.7rem)/1.2 var(--display); + letter-spacing: -0.01em; +} + +.cert-ielts span { + display: block; + color: var(--muted); + font: 400 11px/1.5 var(--mono); +} + +.ladder { + display: grid; + gap: 10px; +} + +.cert-step { + position: relative; + display: flex; + flex-direction: column; + padding: 20px 20px 22px; + border: 1px solid var(--line); + border-top-width: 3px; + border-radius: var(--radius-sm); + background: linear-gradient(180deg, var(--surface-2), var(--surface)); +} + +.cert-step:nth-child(1) { + border-top-color: rgba(111, 227, 176, 0.35); +} + +.cert-step:nth-child(2) { + border-top-color: rgba(111, 227, 176, 0.65); +} + +.cert-step:nth-child(3) { + border-top-color: var(--accent); +} + +.cert-step__num { + margin-bottom: 22px; + color: var(--accent); + font: 500 11px/1 var(--mono); + letter-spacing: 0.16em; +} + +.cert-step:nth-child(1) .cert-step__num { + color: rgba(111, 227, 176, 0.55); +} + +.cert-step:nth-child(2) .cert-step__num { + color: rgba(111, 227, 176, 0.8); +} + +.cert-step__name { + margin-bottom: 8px; + font: 600 clamp(1.15rem, 2.3vw, 1.5rem)/1.25 var(--display); + letter-spacing: -0.015em; + text-wrap: balance; +} + +.cert-step__meta { + margin-top: auto; + color: var(--muted); + font: 400 11px/1.6 var(--mono); +} + +.cert-step__issuer { + margin-top: 2px; + color: var(--faint); + font: 400 12px/1.5 var(--sans); +} + +.certs-block { + margin-top: 44px; + padding-top: 30px; + border-top: 1px solid var(--line-soft); +} + +.certs-block .group-title { + margin-top: 0; +} + +.group-sub { + margin: -6px 0 18px; + color: var(--faint); + font: 400 12px/1.5 var(--sans); +} + +.courses { + display: grid; + grid-template-columns: 1fr; + gap: 8px; +} + +.course { + display: flex; + flex-wrap: wrap; + align-items: baseline; + justify-content: space-between; + gap: 4px 16px; + padding: 14px 16px; + border: 1px solid var(--line-soft); + border-radius: 9px; + background: transparent; +} + +.course__name { + color: #c9d2e1; + font: 500 14px/1.4 var(--sans); +} + +.course__meta { + color: var(--faint); + font: 400 11px/1.5 var(--mono); +} + +/* ---------- certificate images ---------- */ + +.cert-gallery { + display: grid; + gap: 18px; +} + +.cert-shot__frame { + position: relative; + display: block; + overflow: hidden; + border: 1px solid var(--line); + border-radius: var(--radius-sm); + background: var(--surface); + transition: border-color 0.2s ease; +} + +.cert-shot__frame img { + display: block; + width: 100%; + height: auto; + transition: transform 0.45s var(--ease); +} + +.cert-shot__frame::after { + content: "\2197"; + position: absolute; + right: 10px; + bottom: 10px; + display: grid; + place-items: center; + width: 28px; + height: 28px; + border: 1px solid var(--line); + border-radius: 7px; + background: rgba(10, 13, 20, 0.82); + color: var(--accent); + font: 400 13px/1 var(--mono); + transition: border-color 0.2s ease, background-color 0.2s ease; +} + +.cert-shot:hover .cert-shot__frame { + border-color: var(--accent-dim); +} + +.cert-shot:hover .cert-shot__frame img { + transform: scale(1.015); +} + +.cert-shot:hover .cert-shot__frame::after { + border-color: var(--accent); + background: rgba(10, 13, 20, 0.96); +} + +.cert-shot__caption { + margin-top: 10px; + color: var(--muted); + font: 400 12.5px/1.5 var(--sans); +} + +/* ---------- selected work ---------- */ + +.work-list { + border-top: 1px solid var(--line); +} + +.work-row { + display: grid; + gap: 6px 32px; + padding: 24px 0; + border-bottom: 1px solid var(--line-soft); +} + +.work-row__title { + font: 600 1.15rem/1.3 var(--display); + letter-spacing: -0.01em; +} + +.work-row__text { + max-width: 68ch; + color: var(--muted); + font-size: 14.5px; + text-wrap: pretty; +} + +.work-row__link { + align-self: start; + color: var(--accent); + font: 400 12px/1.5 var(--mono); + text-decoration: none; + white-space: nowrap; +} + +.work-row__link:hover { + text-decoration: underline; +} + +.work-more { + margin-top: 22px; + color: var(--faint); + font: 400 12px/1.8 var(--mono); +} + +.work-more a { + color: var(--muted); + text-decoration-color: transparent; +} + +.work-more a:hover { + color: var(--accent); + text-decoration-color: var(--accent); +} + +/* ---------- diagrams and screenshots ---------- */ + +.diagram-block { + margin-bottom: 54px; +} + +.diagram-block:last-child { + margin-bottom: 0; +} + +.diagram { + padding: 20px; + border: 1px solid var(--line-soft); + border-radius: var(--radius); + background: #0b1018; +} + +.diagram__title { + margin-bottom: 4px; + font: 600 1rem/1.4 var(--sans); +} + +.diagram__caption { + margin-top: 14px; + color: var(--faint); + font: 400 11.5px/1.6 var(--mono); +} + +.diagram__caption b { + color: var(--muted); + font-weight: 500; +} + +.diagram svg { + width: 100%; + height: auto; + overflow: visible; +} + +.diagram svg.diagram--wide { + display: block; +} + +.diagram svg.diagram--narrow { + display: none; + max-width: 420px; + margin-inline: auto; +} + +.dg-box { + fill: none; + stroke: #2c3648; + stroke-width: 1; +} + +.dg-box--accent { + stroke: var(--accent-dim); +} + +.dg-text { + fill: #c4cede; + font-family: var(--mono); + font-size: 12px; +} + +.dg-sub { + fill: var(--faint); + font-family: var(--mono); + font-size: 9.5px; + letter-spacing: 0.04em; +} + +.dg-line { + fill: none; + stroke: var(--accent); + stroke-width: 1.2; + stroke-linecap: round; + stroke-linejoin: round; +} + +.dg-soft { + stroke-opacity: 0.45; +} + +.dg-node { + fill: var(--accent); +} + +.dg-anchor { + fill: #0b1018; +} + +html.anims .diagram .draw { + stroke-dasharray: 1; + stroke-dashoffset: 1; +} + +html.anims .diagram.is-in .draw { + stroke-dashoffset: 0; + transition: stroke-dashoffset 1.3s var(--ease); +} + +html.anims .diagram .d2 { + transition-delay: 0.12s; +} + +html.anims .diagram .d3 { + transition-delay: 0.24s; +} + +html.anims .diagram .d4 { + transition-delay: 0.36s; +} + +html.anims .diagram .d5 { + transition-delay: 0.48s; +} + +html.anims .diagram .d6 { + transition-delay: 0.6s; +} + +.shot { + margin: 12px 0 0; +} + +.shot__frame { + overflow: hidden; + border: 1px solid var(--line-soft); + border-radius: var(--radius-sm); + background: var(--surface); +} + +.shot img { + display: block; + width: 100%; + height: auto; +} + +.shot figcaption { + margin-top: 9px; + color: var(--faint); + font: 400 11px/1.5 var(--mono); +} + +/* ---------- background ---------- */ + +.timeline { + border-top: 1px solid var(--line); +} + +.timeline__item { + display: grid; + gap: 4px 28px; + padding: 18px 0; + border-bottom: 1px solid var(--line-soft); +} + +.timeline__when { + color: var(--faint); + font: 400 11px/1.7 var(--mono); + letter-spacing: 0.02em; +} + +.timeline__what { + color: #ccd5e3; + font-size: 14.5px; + line-height: 1.6; +} + +/* ---------- contact ---------- */ + +.contact__lede { + max-width: 44ch; + margin-bottom: 28px; + color: var(--muted); + font-size: clamp(16px, 2vw, 18px); +} + +.contact-rows { + border-top: 1px solid var(--line); +} + +.contact-row { + display: flex; + align-items: baseline; + justify-content: space-between; + gap: 16px; + padding: 18px 0; + border-bottom: 1px solid var(--line-soft); + color: var(--text); + text-decoration: none; +} + +.contact-row__label { + color: var(--faint); + font: 500 11px/1.5 var(--mono); + letter-spacing: 0.12em; + text-transform: uppercase; +} + +.contact-row__value { + display: flex; + align-items: baseline; + gap: 14px; + overflow-wrap: anywhere; + font: 500 clamp(1.05rem, 2.6vw, 1.45rem)/1.3 var(--sans); + letter-spacing: -0.01em; + text-align: right; +} + +.contact-row__arrow { + color: var(--accent); + font: 400 1.1rem/1 var(--mono); + transition: transform 0.2s var(--ease); +} + +.contact-row:hover .contact-row__value { + color: var(--accent); +} + +.contact-row:hover .contact-row__arrow { + transform: translateX(4px); +} + +/* ---------- footer ---------- */ + +.site-footer { + padding-block: 28px; + border-top: 1px solid var(--line-soft); + color: var(--faint); + font: 400 11.5px/1.6 var(--mono); +} + +/* ---------- motion ---------- */ + +@keyframes hero-in { + from { + opacity: 0; + transform: translateY(16px); + } + to { + opacity: 1; + transform: none; + } +} + +@keyframes rise-in { + from { + opacity: 0; + transform: translateY(9px); + } + to { + opacity: 1; + transform: none; + } +} + +html.anims .reveal { + opacity: 0; + transform: translateY(14px); +} + +html.anims .reveal.is-in { + opacity: 1; + transform: none; + transition: opacity 0.5s var(--ease), transform 0.5s var(--ease); +} + +html.anims .hero__label, +html.anims .hero__title, +html.anims .hero__lede, +html.anims .hero__actions { + animation: hero-in 0.62s var(--ease) both; +} + +html.anims .hero__title { + animation-delay: 70ms; +} + +html.anims .hero__lede { + animation-delay: 140ms; +} + +html.anims .hero__actions { + animation-delay: 210ms; +} + +html.anims .tiles.is-in > .tile, +html.anims .chips.is-in > .chip, +html.anims .ladder.is-in > .cert-step, +html.anims .courses.is-in > .course, +html.anims .cert-gallery.is-in > .cert-shot, +html.anims .work-list.is-in > .work-row, +html.anims .timeline.is-in > .timeline__item, +html.anims .contact-rows.is-in > .contact-row { + animation: rise-in 0.5s var(--ease) backwards; + animation-delay: calc(var(--i, 0) * 40ms); +} + +/* ---------- wider screens ---------- */ + +@media (min-width: 560px) { + .tiles { + grid-template-columns: repeat(3, minmax(0, 1fr)); + } + + .courses { + grid-template-columns: repeat(3, minmax(0, 1fr)); + } +} + +@media (min-width: 700px) { + .site-header__inner { + flex-wrap: nowrap; + } + + .cert-gallery { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } + + .timeline__item { + grid-template-columns: 190px 1fr; + align-items: baseline; + } + + .work-row { + grid-template-columns: minmax(0, 220px) 1fr auto; + align-items: start; + } + + .work-row__title { + grid-column: 1; + } + + .work-row__text { + grid-column: 2; + } + + .work-row__link { + grid-column: 3; + } +} + +@media (min-width: 760px) { + .tiles { + grid-template-columns: repeat(4, minmax(0, 1fr)); + } + + .ladder { + grid-template-columns: repeat(3, minmax(0, 1fr)); + align-items: end; + position: relative; + padding-top: 54px; + } + + .ladder::before { + content: ""; + position: absolute; + top: 26px; + left: 8px; + right: 8px; + height: 1px; + background: linear-gradient( + 90deg, + rgba(111, 227, 176, 0.18), + rgba(111, 227, 176, 0.55) 55%, + rgba(111, 227, 176, 0.95) + ); + } + + .cert-step { + min-height: 176px; + } + + .cert-step:nth-child(2) { + min-height: 208px; + } + + .cert-step:nth-child(3) { + min-height: 240px; + } + + .cert-step__num { + position: absolute; + top: -54px; + left: 20px; + display: grid; + place-items: center; + width: 52px; + height: 52px; + margin: 0; + border: 1px solid var(--line); + border-radius: 50%; + background: var(--bg); + } + + .diagram { + padding: 28px; + } +} + +@media (min-width: 980px) { + .cert-gallery { + grid-template-columns: repeat(3, minmax(0, 1fr)); + } + + .tiles { + grid-template-columns: repeat(6, minmax(0, 1fr)); + } + + .tile { + min-height: 136px; + } +} + +@media (max-width: 799px) { + .diagram svg.diagram--wide { + display: none; + } + + .diagram svg.diagram--narrow { + display: block; + } +} + +@media (max-width: 699px) { + .site-nav { + width: 100%; + justify-content: flex-start; + gap: 10px 18px; + } + + .section__note { + display: none; + } + + .contact-row { + flex-direction: column; + gap: 6px; + } + + .contact-row__value { + text-align: left; + } +} + +@media (max-width: 559px) { + .site-header__inner { + padding-block: 16px; + } + + .tile { + min-height: 116px; + padding: 16px 14px 13px; + } + + .tile__icon { + width: 34px; + height: 34px; + } + + .cert-step { + padding: 18px 18px 20px; + } + + .cert-step__num { + margin-bottom: 16px; + } + + .diagram { + padding: 14px; + } +} + +/* ---------- reduced motion ---------- */ + +@media (prefers-reduced-motion: reduce) { + *, + *::before, + *::after { + animation-duration: 0.01ms !important; + animation-delay: 0s !important; + animation-iteration-count: 1 !important; + transition-duration: 0.01ms !important; + transition-delay: 0s !important; + } + + html.anims .reveal { + opacity: 1; + transform: none; + } + + .tile:hover, + .contact-row:hover .contact-row__arrow, + .cert-shot:hover .cert-shot__frame img { + transform: none; + } + + html.anims .diagram .draw { + stroke-dasharray: none; + stroke-dashoffset: 0; + } + + html.anims .progress { + display: none; + } +} + +@media print { + .site-header, + .progress, + .hero__actions { + display: none; + } + + html.anims .reveal { + opacity: 1; + transform: none; + } + + html.anims .diagram .draw { + stroke-dasharray: none; + stroke-dashoffset: 0; + } + + body { + background: #fff; + color: #000; + } +} diff --git a/favicon.svg b/favicon.svg new file mode 100644 index 0000000..1212751 --- /dev/null +++ b/favicon.svg @@ -0,0 +1,5 @@ + + + + RY + diff --git a/images/Associate.jpg b/images/Associate.jpg new file mode 100755 index 0000000..f650256 Binary files /dev/null and b/images/Associate.jpg differ diff --git a/images/dashboard.png b/images/dashboard.png new file mode 100644 index 0000000..7f3eb5d Binary files /dev/null and b/images/dashboard.png differ diff --git a/images/expert.png b/images/expert.png new file mode 100644 index 0000000..f45df46 Binary files /dev/null and b/images/expert.png differ diff --git a/images/pipline-green.png b/images/pipline-green.png new file mode 100644 index 0000000..8a8041a Binary files /dev/null and b/images/pipline-green.png differ diff --git a/images/professional.png b/images/professional.png new file mode 100755 index 0000000..01f902b Binary files /dev/null and b/images/professional.png differ diff --git a/index.html b/index.html new file mode 100644 index 0000000..a7960e1 --- /dev/null +++ b/index.html @@ -0,0 +1,595 @@ + + + + + + Reza Yaghobi · Junior DevOps engineer + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+

Junior DevOps engineer

+

Reza Yaghobi

+

I build and run containerized infrastructure: Docker, Kubernetes, GitLab CI/CD, Ansible and full observability with Prometheus and Grafana.

+ +
+
+ +
+
+
+

Stack

+
+ +

Platform

+
    +
  • + + Docker +
  • +
  • + + Kubernetes +
  • +
  • + + Rancher +
  • +
  • + + Helm +
  • +
  • + + Linux +
  • +
  • + + VMware ESXiserver virtualization +
  • +
  • + + Traefik +
  • +
  • + + nginx +
  • +
  • + + HAProxy +
  • +
  • + + Keepalived +
  • +
  • + + MinIO +
  • +
  • + + Ceph +
  • +
+ +

Automation and delivery

+
    +
  • + + GitLab +
  • +
  • + + GitLab CI/CD +
  • +
  • + + Git +
  • +
  • + + Ansible +
  • +
  • + + Terraform +
  • +
  • + + Argo CD +
  • +
  • + + GitOps +
  • +
  • + + Nexus Repository +
  • +
  • + + Shell scriptingBash +
  • +
+ +

Observability

+
    +
  • + + Prometheus +
  • +
  • + + Grafana +
  • +
  • + + Loki +
  • +
  • + + Alloy +
  • +
  • + + Elasticsearch +
  • +
  • + + Zabbix +
  • +
+ +

Languages and data

+
    +
  • + + Python +
  • +
  • + + C# +
  • +
  • + + PostgreSQL +
  • +
  • + + MySQL +
  • +
  • + + Microsoft SQL Server +
  • +
  • + + Kerasfor ML/DL +
  • +
+ +

Networking and Windows

+
    +
  • Routing
  • +
  • DNS administration
  • +
  • Active Directory
  • +
  • Windows Server
  • +
  • Network+ knowledge
  • +
+
+
+ +
+
+
+

Certificates

+
+ +

IELTS · Band 7.0 · 2024

+ +
    +
  1. + 01 +

    Associate DevOps Engineer

    +

    22 h · 2026

    +

    ArvanCloud

    +
  2. +
  3. + 02 +

    Professional DevOps Engineer

    +

    100 h · 2026

    +

    ArvanCloud

    +
  4. +
  5. + 03 +

    Expert DevOps Engineer

    +

    18 h · 2026

    +

    ArvanCloud

    +
  6. +
+ +
+

Completed courses

+

Course only, no certificate issued

+
    +
  • + LPIC-1 + 26 h · 2024 +
  • +
  • + Network+ + 72 h · 2024 +
  • +
  • + LPIC-2 +
  • +
+
+
+
+ +
+
+
+

ArvanCloud certificates

+
+ + +
+
+ +
+
+
+

Selected work

+
+ +
    +
  • +

    Homelab

    +

    Self-hosted Docker and K3s services behind Traefik, with a Prometheus, Grafana and Loki monitoring stack and CI/CD.

    + View on GitHub ↗ +
  • +
  • +

    devops-test

    +

    A DevOps assessment task for a company, covering a 6-stage GitLab CI/CD pipeline, firewalling with iptables, monitoring and a web app.

    + View on GitHub ↗ +
  • +
+ +

Also on GitHub: agents · ML portfolio · DL portfolio

+
+
+ +
+
+
+

Diagrams

+
+ +
+
+

Homelab architecture

+ + Homelab architecture + Internet traffic reaches Traefik, which proxies to Docker and K3s workloads. A monitoring group runs Prometheus, Grafana, Loki and Alloy, and GitLab CI/CD deploys the workloads. + + + + + + + + + + + + + + Internet + + + Traefik + reverse proxy + + + Docker + containers + + + K3s + cluster + + + MONITORING + + Prometheus + + Grafana + + Loki + + Alloy + + + GitLab · CI/CD + BUILD · TEST · DEPLOY + + METRICS · LOGS + DEPLOY + + + + Homelab architecture + Internet traffic reaches Traefik, which proxies to Docker and K3s workloads. A monitoring group runs Prometheus, Grafana, Loki and Alloy, and GitLab CI/CD deploys the workloads. + + + + + + + + + + + + + + + Internet + + + Traefik + reverse proxy + + + Docker + + + K3s + + + MONITORING + + Prometheus + + Grafana + + Loki + + Alloy + + + GitLab · CI/CD + BUILD · TEST · DEPLOY + DEPLOY TO WORKLOADS + +
+ +
+
+ Grafana dashboard showing a metrics graph +
+
Grafana dashboard
+
+
+ +
+
+

CI/CD pipeline

+ + CI/CD pipeline + A commit is pushed, then the pipeline runs the test, build, publish and deploy stages in order, and ends with a healthy container. + + + + + + + + + + + + + + git push + + + 01 + Test + pytest + + + 02 + Build + docker image + + + 03 + Publish + optional + + + 04 + Deploy + compose + + + healthy + + + + CI/CD pipeline + A commit is pushed, then the pipeline runs the test, build, publish and deploy stages in order, and ends with a healthy container. + + + + + + + + + + + + + + git push + + + 01 + Test + pytest + + + 02 + Build + docker image + + + 03 + Publish + optional + + + 04 + Deploy + compose + + + healthy + +
+ +
+
+ Green GitLab CI pipeline, all stages passed +
+
GitLab CI pipeline
+
+
+
+
+ +
+
+
+

Background

+
+
    +
  1. + 2025 to present +

    Bachelor's degree in Computer Networks, Shamsipoor, currently term 3

    +
  2. +
  3. + March to June 2026 +

    IT Help Desk, Jahad Sharif University

    +
  4. +
+
+
+ +
+
+
+

Contact

+
+

Open to junior and associate DevOps roles, on-site or remote.

+ +
+
+
+ +
+
© 2026 Reza Yaghobi · Hosted on my own homelab
+
+ + + + diff --git a/robots.txt b/robots.txt new file mode 100644 index 0000000..c2a49f4 --- /dev/null +++ b/robots.txt @@ -0,0 +1,2 @@ +User-agent: * +Allow: / diff --git a/tools/build-icons.mjs b/tools/build-icons.mjs new file mode 100644 index 0000000..26f1977 --- /dev/null +++ b/tools/build-icons.mjs @@ -0,0 +1,146 @@ +#!/usr/bin/env node +/** + * Regenerates assets/icons/*.svg and assets/brand.css from simple-icons. + * + * Setup (outside this project): + * mkdir -p /tmp/ry-scratch && cd /tmp/ry-scratch + * npm init -y && npm install simple-icons + * + * Run from the project root: + * node tools/build-icons.mjs + * SIMPLE_ICONS_DIR=/path/to/node_modules/simple-icons node tools/build-icons.mjs + * + * Brand colours below 3:1 contrast against the page background are lightened + * (blended towards white) until they reach 3:1, so every icon stays legible. + * Tools absent from simple-icons get a neutral monogram instead of a copied logo. + */ +import fs from "node:fs"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; + +const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); +const siDir = + process.env.SIMPLE_ICONS_DIR || + "/tmp/ry-scratch/node_modules/simple-icons"; +const outIcons = path.join(root, "assets", "icons"); +const outBrandCss = path.join(root, "assets", "brand.css"); +const BG = "#0a0d14"; + +const icons = [ + // group, file, simple-icons slug, display name + ["platform", "docker", "docker", "Docker"], + ["platform", "kubernetes", "kubernetes", "Kubernetes"], + ["platform", "rancher", "rancher", "Rancher"], + ["platform", "helm", "helm", "Helm"], + ["platform", "linux", "linux", "Linux"], + ["platform", "vmware", "vmware", "VMware ESXi"], + ["platform", "traefik", "traefikproxy", "Traefik"], + ["platform", "nginx", "nginx", "nginx"], + ["platform", "minio", "minio", "MinIO"], + ["platform", "ceph", "ceph", "Ceph"], + + ["automation", "gitlab", "gitlab", "GitLab"], + ["automation", "gitlab-ci", "gitlab", "GitLab CI/CD"], + ["automation", "git", "git", "Git"], + ["automation", "ansible", "ansible", "Ansible"], + ["automation", "terraform", "terraform", "Terraform"], + ["automation", "bash", "gnubash", "Shell scripting (Bash)"], + + ["observability", "prometheus", "prometheus", "Prometheus"], + ["observability", "grafana", "grafana", "Grafana"], + ["observability", "elasticsearch", "elasticsearch", "Elasticsearch"], + + ["data", "python", "python", "Python"], + ["data", "postgresql", "postgresql", "PostgreSQL"], + ["data", "mysql", "mysql", "MySQL"], + ["data", "keras", "keras", "Keras (for ML/DL)"], +]; + +const monograms = [ + ["platform", "haproxy", "HA", "HAProxy"], + ["platform", "keepalived", "KP", "Keepalived"], + ["automation", "argocd", "AC", "Argo CD"], + ["automation", "gitops", "GO", "GitOps"], + ["automation", "nexus", "NX", "Nexus Repository"], + ["observability", "loki", "LO", "Loki"], + ["observability", "alloy", "AL", "Alloy"], + ["observability", "zabbix", "ZB", "Zabbix"], + ["data", "csharp", "C#", "C#"], + ["data", "mssql", "SQL", "Microsoft SQL Server"], +]; + +const neutral = "#9aa7bc"; + +function hexToRgb(hex) { + const n = parseInt(hex.replace("#", ""), 16); + return [(n >> 16) & 255, (n >> 8) & 255, n & 255]; +} + +function luminance([r, g, b]) { + const f = (c) => { + const s = c / 255; + return s <= 0.03928 ? s / 12.92 : ((s + 0.055) / 1.055) ** 2.4; + }; + return 0.2126 * f(r) + 0.7152 * f(g) + 0.0722 * f(b); +} + +function contrast(a, b) { + const [hi, lo] = [luminance(a), luminance(b)].sort((x, y) => y - x); + return (hi + 0.05) / (lo + 0.05); +} + +const bg = hexToRgb(BG); + +function readable(hex) { + let rgb = hexToRgb(hex); + let t = 0; + while (contrast(rgb, bg) < 3 && t < 1) { + t += 0.04; + rgb = rgb.map((c) => Math.round(c + (255 - c) * t)); + } + return "#" + rgb.map((c) => c.toString(16).padStart(2, "0")).join(""); +} + +const toHex = (rgb) => "#" + rgb.map((c) => c.toString(16).padStart(2, "0")).join(""); + +const data = JSON.parse( + fs.readFileSync(path.join(siDir, "data", "simple-icons.json"), "utf8"), +); +const bySlug = new Map(data.map((i) => [i.slug, i])); + +fs.mkdirSync(outIcons, { recursive: true }); +const brandRules = []; + +for (const [group, file, slug, title] of icons) { + const meta = bySlug.get(slug); + if (!meta) throw new Error(`simple-icons has no slug "${slug}" (${title})`); + const rawPath = fs.readFileSync(path.join(siDir, "icons", `${slug}.svg`), "utf8"); + const d = rawPath.match(/]*\sd="([^"]+)"/)?.[1]; + if (!d) throw new Error(`no path found in ${slug}.svg`); + const colour = readable("#" + meta.hex); + fs.writeFileSync( + path.join(outIcons, `${file}.svg`), + `\n`, + ); + brandRules.push(`.brand--${file}{--brand:${colour}}`); +} + +for (const [group, file, label, title] of monograms) { + const small = label.length > 2; + fs.writeFileSync( + path.join(outIcons, `${file}.svg`), + `` + + `` + + `${label}\n`, + ); + brandRules.push(`.brand--${file}{--brand:${neutral}}`); +} + +const header = + "/* Generated by tools/build-icons.mjs. Do not edit by hand. */\n" + + "/* Brand colours are lightened where needed to reach 3:1 on #0a0d14. */\n"; +fs.writeFileSync(outBrandCss, header + brandRules.join("\n") + "\n"); + +console.log(`wrote ${icons.length + monograms.length} icons to assets/icons/`); +console.log(`wrote ${brandRules.length} brand rules to assets/brand.css`); +for (const r of brandRules) console.log(" " + r); diff --git a/tools/og.html b/tools/og.html new file mode 100644 index 0000000..db53905 --- /dev/null +++ b/tools/og.html @@ -0,0 +1,47 @@ + + + + +Open Graph card + + + +
Junior DevOps engineer
+

Reza
Yaghobi

+
+

Docker · Kubernetes · GitLab CI/CD · Ansible · Prometheus · Grafana

+ rezayaghobi.ir +
+ +