Single self-contained index.html (avatar + favicon inlined; only Google Fonts external; zero JS; dark-mode; responsive) served by a baked nginx:alpine image behind Traefik, built and published by CI on push to main and rolled out by Watchtower. Co-Authored-By: Claude Opus 4.8 <[email protected]> Claude-Session: https://claude.ai/code/session_01LbhPvfSERrnuY5jdhAdB7v
This commit is contained in:
commit
e3c7bf2e74
8 files changed
+719
No files matched your search
@@ -0,0 +1,85 @@
|
||||
name: build
|
||||
|
||||
# On push to main, build the self-contained nginx image and push it to the
|
||||
# build registry, then publish it so the deploy host can pull it over HTTPS
|
||||
# and Watchtower recreates the container. The registry address is injected
|
||||
# from a masked secret so it never appears in the repo or the logs.
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
paths:
|
||||
- 'index.html'
|
||||
- 'favicon.svg'
|
||||
- 'nginx.conf'
|
||||
- 'Dockerfile'
|
||||
- '.gitea/workflows/build.yml'
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
group: build-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: docker
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
with:
|
||||
# The build registry is plain HTTP — tell buildkit not to upgrade
|
||||
# the push to HTTPS.
|
||||
config-inline: |
|
||||
[registry."${{ secrets.REGISTRY_HOST }}"]
|
||||
http = true
|
||||
insecure = true
|
||||
|
||||
- name: Configure registry credentials for buildx
|
||||
env:
|
||||
REGISTRY_HOST: ${{ secrets.REGISTRY_HOST }}
|
||||
REGISTRY_USER: ${{ secrets.REGISTRY_USER }}
|
||||
REGISTRY_TOKEN: ${{ secrets.REGISTRY_TOKEN }}
|
||||
run: |
|
||||
mkdir -p ~/.docker
|
||||
AUTH=$(printf '%s:%s' "$REGISTRY_USER" "$REGISTRY_TOKEN" | base64 -w0)
|
||||
cat > ~/.docker/config.json <<EOF
|
||||
{"auths":{"$REGISTRY_HOST":{"auth":"$AUTH"}}}
|
||||
EOF
|
||||
|
||||
- name: Compute tags
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: ${{ secrets.REGISTRY_HOST }}/justin/jpaulio
|
||||
tags: |
|
||||
type=raw,value=latest
|
||||
type=raw,value=test-main
|
||||
type=sha,prefix=test-sha-,format=long
|
||||
labels: |
|
||||
org.opencontainers.image.url=https://jpaul.io
|
||||
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
platforms: linux/amd64
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
|
||||
- name: Publish package (link to repo)
|
||||
env:
|
||||
GITEA_TOKEN: ${{ secrets.REGISTRY_TOKEN }}
|
||||
run: |
|
||||
code=$(curl -s -o /tmp/link.out -w "%{http_code}" -X POST \
|
||||
-H "Authorization: token ${GITEA_TOKEN}" \
|
||||
"https://git.jpaul.io/api/v1/packages/justin/container/jpaulio/-/link/jpaulio")
|
||||
echo "link -> jpaulio: HTTP $code"
|
||||
case "$code" in
|
||||
201) echo "OK — newly linked" ;;
|
||||
400|409) echo "OK — already linked" ;;
|
||||
*) cat /tmp/link.out; exit 1 ;;
|
||||
esac
|
||||
@@ -0,0 +1,7 @@
|
||||
# local scratch — the avatar is inlined into index.html as a data URI, so these
|
||||
# downloaded source files aren't needed in the repo
|
||||
.assets/
|
||||
|
||||
# Claude Code local files (skill config with internal deploy notes, local
|
||||
# settings) stay out of the repo
|
||||
.claude/
|
||||
@@ -0,0 +1,7 @@
|
||||
# Self-contained image for the jpaul.io hub page: nginx:alpine with the site
|
||||
# and config baked in. No bind mounts, no inode surprises — the running
|
||||
# container always matches the image it was built from.
|
||||
FROM nginx:alpine
|
||||
|
||||
COPY nginx.conf /etc/nginx/conf.d/default.conf
|
||||
COPY index.html favicon.svg /usr/share/nginx/html/
|
||||
@@ -0,0 +1,33 @@
|
||||
# jpaul.io
|
||||
|
||||
The personal hub landing page at the root of [jpaul.io](https://jpaul.io) — who I am,
|
||||
what I make, and where to find me. Not a blog, not a portfolio: a hub.
|
||||
|
||||
## Contents
|
||||
|
||||
- **`index.html`** — the whole site. One self-contained file, no build step. The avatar
|
||||
and favicon are inlined as data URIs; the only external assets are the Google Fonts
|
||||
(Inter + JetBrains Mono). Zero JavaScript.
|
||||
- **`favicon.svg`** — green `$` / amber `_` cursor mark on the GitHub-dark-slate ground.
|
||||
- **`Dockerfile`** — bakes the site into an `nginx:alpine` image.
|
||||
- **`docker-compose.yml`** — runs that image behind Traefik.
|
||||
- **`.gitea/workflows/build.yml`** — CI: build the image and publish it on push to `main`.
|
||||
|
||||
## Design
|
||||
|
||||
Dark-mode only, GitHub-dark-slate palette, terminal-panel motifs. Matches the brand used
|
||||
across [jpaul.me](https://www.jpaul.me), the AI Workflow course cards, and the YouTube
|
||||
channel. Responsive; cards stack to one column below 720px; hero stays readable at 375px.
|
||||
|
||||
## Deploy — automated (push to main → live)
|
||||
|
||||
1. **Push to `main`** → Gitea Actions builds the `nginx:alpine` image and publishes it.
|
||||
2. The deploy host **pulls** the image over HTTPS.
|
||||
3. **Watchtower** recreates the container with the new image.
|
||||
|
||||
No bind mounts — the running container always matches what CI built. Runs behind Traefik:
|
||||
`websecure` entrypoint, an HTTP-01 cert, and `Host(jpaul.io) || Host(www.jpaul.io)` with
|
||||
the cert domains named explicitly (a compound rule won't auto-populate them).
|
||||
|
||||
First run on the host: `docker compose up -d`. After that, editing the site is just
|
||||
commit → push to `main`; CI and Watchtower do the rest.
|
||||
@@ -0,0 +1,31 @@
|
||||
# jpaul.io hub page — a single container behind Traefik. The site is baked into
|
||||
# the image by CI (see .gitea/workflows/build.yml + Dockerfile); Watchtower
|
||||
# recreates the container when a new image is published, so what's running
|
||||
# always matches what CI built.
|
||||
#
|
||||
# First run: docker compose up -d
|
||||
# Updates: automatic — push to main -> CI builds -> Watchtower redeploys.
|
||||
services:
|
||||
jpaulio:
|
||||
image: git.jpaul.io/justin/jpaulio:latest
|
||||
container_name: jpaulio
|
||||
restart: always
|
||||
pull_policy: always
|
||||
networks:
|
||||
- web
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
- "traefik.http.routers.jpaulio.rule=Host(`jpaul.io`) || Host(`www.jpaul.io`)"
|
||||
- "traefik.http.routers.jpaulio.entrypoints=websecure"
|
||||
- "traefik.http.routers.jpaulio.tls.certresolver=myresolver"
|
||||
# A compound (||) rule doesn't auto-populate the ACME domain list, so name
|
||||
# the cert domains explicitly: one cert for the apex + www SAN.
|
||||
- "traefik.http.routers.jpaulio.tls.domains[0].main=jpaul.io"
|
||||
- "traefik.http.routers.jpaulio.tls.domains[0].sans=www.jpaul.io"
|
||||
- "traefik.http.services.jpaulio.loadbalancer.server.port=80"
|
||||
# Watchtower auto-updates this container when a new image is published.
|
||||
- "com.centurylinklabs.watchtower.enable=true"
|
||||
|
||||
networks:
|
||||
web:
|
||||
external: true
|
||||
@@ -0,0 +1,6 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" role="img" aria-label="jpaul.io">
|
||||
<rect width="64" height="64" rx="14" fill="#0D1117"/>
|
||||
<rect x="1.75" y="1.75" width="60.5" height="60.5" rx="12.25" fill="none" stroke="#30363D" stroke-width="1.5"/>
|
||||
<text x="25" y="45" font-family="'JetBrains Mono', ui-monospace, 'SFMono-Regular', Menlo, monospace" font-size="42" font-weight="700" fill="#00D966" text-anchor="middle">$</text>
|
||||
<text x="41" y="45" font-family="'JetBrains Mono', ui-monospace, 'SFMono-Regular', Menlo, monospace" font-size="42" font-weight="700" fill="#FFB020" text-anchor="middle">_</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 633 B |
+527
@@ -0,0 +1,527 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Justin Paul — jpaul.io</title>
|
||||
<meta name="description" content="Justin Paul builds AI-native tools for infrastructure, agtech, and everyone tired of copy-pasting into a chat window. Drawbar founder, course creator, homelab operator.">
|
||||
<meta name="author" content="Justin Paul">
|
||||
<meta name="theme-color" content="#0D1117">
|
||||
|
||||
<!-- Open Graph -->
|
||||
<meta property="og:type" content="website">
|
||||
<meta property="og:title" content="Justin Paul — jpaul.io">
|
||||
<meta property="og:description" content="AI-native tools for infrastructure, agtech, and everyone tired of copy-pasting into a chat window.">
|
||||
<meta property="og:url" content="https://jpaul.io/">
|
||||
<meta name="twitter:card" content="summary">
|
||||
|
||||
<link rel="icon" href="data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2064%2064%22%20role%3D%22img%22%20aria-label%3D%22jpaul.io%22%3E%0A%20%20%3Crect%20width%3D%2264%22%20height%3D%2264%22%20rx%3D%2214%22%20fill%3D%22%230D1117%22%2F%3E%0A%20%20%3Crect%20x%3D%221.75%22%20y%3D%221.75%22%20width%3D%2260.5%22%20height%3D%2260.5%22%20rx%3D%2212.25%22%20fill%3D%22none%22%20stroke%3D%22%2330363D%22%20stroke-width%3D%221.5%22%2F%3E%0A%20%20%3Ctext%20x%3D%2225%22%20y%3D%2245%22%20font-family%3D%22%27JetBrains%20Mono%27%2C%20ui-monospace%2C%20%27SFMono-Regular%27%2C%20Menlo%2C%20monospace%22%20font-size%3D%2242%22%20font-weight%3D%22700%22%20fill%3D%22%2300D966%22%20text-anchor%3D%22middle%22%3E%24%3C%2Ftext%3E%0A%20%20%3Ctext%20x%3D%2241%22%20y%3D%2245%22%20font-family%3D%22%27JetBrains%20Mono%27%2C%20ui-monospace%2C%20%27SFMono-Regular%27%2C%20Menlo%2C%20monospace%22%20font-size%3D%2242%22%20font-weight%3D%22700%22%20fill%3D%22%23FFB020%22%20text-anchor%3D%22middle%22%3E_%3C%2Ftext%3E%0A%3C%2Fsvg%3E%0A" type="image/svg+xml">
|
||||
|
||||
<!-- Fonts -->
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link rel="preload" as="style" href="https://fonts.googleapis.com/css2?family=Inter:wght@600;700;800&family=JetBrains+Mono:wght@400;500;600;700&display=swap">
|
||||
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter:wght@600;700;800&family=JetBrains+Mono:wght@400;500;600;700&display=swap">
|
||||
|
||||
<style>
|
||||
:root {
|
||||
--bg: #0D1117;
|
||||
--panel: #161B22;
|
||||
--border: #30363D;
|
||||
--elevated: #21262D;
|
||||
--text: #c9d1d9;
|
||||
--high: #F0F6FC;
|
||||
--dim: #7D8590;
|
||||
--green: #00D966;
|
||||
--amber: #FFB020;
|
||||
--blue: #6a9dff;
|
||||
--code: #ffd580;
|
||||
|
||||
--mono: 'JetBrains Mono', ui-monospace, 'SFMono-Regular', Menlo, Consolas, monospace;
|
||||
--sans: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
|
||||
--maxw: 900px;
|
||||
--radius: 12px;
|
||||
}
|
||||
|
||||
* { box-sizing: border-box; }
|
||||
|
||||
html { -webkit-text-size-adjust: 100%; }
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
min-height: 100vh;
|
||||
font-family: var(--sans);
|
||||
font-weight: 600;
|
||||
color: var(--text);
|
||||
background-color: var(--bg);
|
||||
background-image: radial-gradient(ellipse 80% 60% at 50% 50%, #161B22 0%, #0D1117 100%);
|
||||
background-attachment: fixed;
|
||||
line-height: 1.6;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
text-rendering: optimizeLegibility;
|
||||
}
|
||||
|
||||
.wrap {
|
||||
max-width: var(--maxw);
|
||||
margin: 0 auto;
|
||||
padding: clamp(48px, 9vw, 96px) clamp(20px, 5vw, 40px) 64px;
|
||||
}
|
||||
|
||||
a { color: var(--blue); text-decoration: none; }
|
||||
a:hover { text-decoration: underline; text-underline-offset: 3px; }
|
||||
a:focus-visible {
|
||||
outline: 2px solid var(--blue);
|
||||
outline-offset: 2px;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.mono { font-family: var(--mono); }
|
||||
|
||||
/* Skip link */
|
||||
.skip {
|
||||
position: absolute; left: -9999px; top: 0;
|
||||
background: var(--elevated); color: var(--high);
|
||||
padding: 10px 16px; border-radius: 8px; z-index: 10;
|
||||
font-family: var(--mono); font-size: 13px;
|
||||
}
|
||||
.skip:focus { left: 12px; top: 12px; }
|
||||
|
||||
/* ---------- HERO ---------- */
|
||||
.hero {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
gap: 40px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.hero__lead {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: clamp(16px, 3vw, 24px);
|
||||
min-width: 0;
|
||||
flex: 0 1 auto;
|
||||
}
|
||||
|
||||
.hero__text { max-width: 380px; }
|
||||
|
||||
.avatar {
|
||||
width: 84px; height: 84px;
|
||||
border-radius: 50%;
|
||||
border: 1px solid var(--border);
|
||||
display: block;
|
||||
flex: none;
|
||||
background: var(--panel);
|
||||
transition: filter .2s ease;
|
||||
}
|
||||
.avatar:hover { filter: brightness(1.05); }
|
||||
|
||||
.eyebrow {
|
||||
font-family: var(--mono);
|
||||
font-weight: 500;
|
||||
font-size: 13px;
|
||||
color: var(--dim);
|
||||
letter-spacing: .01em;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
.eyebrow::before { content: '~/'; color: var(--green); }
|
||||
|
||||
h1 {
|
||||
font-family: var(--sans);
|
||||
font-weight: 800;
|
||||
font-size: clamp(34px, 6vw, 52px);
|
||||
line-height: 1.04;
|
||||
letter-spacing: -0.02em;
|
||||
color: var(--high);
|
||||
margin: 0 0 18px;
|
||||
}
|
||||
|
||||
.tagline {
|
||||
font-weight: 500;
|
||||
font-size: clamp(17px, 2.4vw, 21px);
|
||||
line-height: 1.5;
|
||||
color: var(--text);
|
||||
max-width: 30ch;
|
||||
margin: 0 0 14px;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
font-family: var(--mono);
|
||||
font-weight: 400;
|
||||
font-size: 13.5px;
|
||||
color: var(--dim);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* Hero terminal panel */
|
||||
.term {
|
||||
flex: none;
|
||||
width: 260px;
|
||||
background: var(--panel);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
overflow: hidden;
|
||||
box-shadow: 0 8px 24px -12px rgba(0,0,0,0.6);
|
||||
}
|
||||
.term__bar {
|
||||
display: flex; align-items: center; gap: 7px;
|
||||
padding: 10px 12px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
background: rgba(255,255,255,0.015);
|
||||
}
|
||||
.dot { width: 11px; height: 11px; border-radius: 50%; flex: none; }
|
||||
.dot--r { background: #ff5f56; }
|
||||
.dot--y { background: #ffbd2e; }
|
||||
.dot--g { background: #27c93f; }
|
||||
.term__bar .label {
|
||||
margin-left: auto;
|
||||
font-family: var(--mono);
|
||||
font-size: 11px;
|
||||
color: var(--dim);
|
||||
}
|
||||
.term__body {
|
||||
padding: 16px 16px 18px;
|
||||
font-family: var(--mono);
|
||||
font-size: 13px;
|
||||
line-height: 1.7;
|
||||
}
|
||||
.term__body .p { color: var(--green); }
|
||||
.term__body .out { color: var(--code); }
|
||||
.cursor {
|
||||
display: inline-block;
|
||||
width: 8px; height: 15px;
|
||||
vertical-align: -2px;
|
||||
background: var(--green);
|
||||
margin-left: 2px;
|
||||
}
|
||||
|
||||
/* ---------- generic section ---------- */
|
||||
section { margin-top: clamp(48px, 8vw, 72px); }
|
||||
|
||||
.sec-label {
|
||||
font-family: var(--mono);
|
||||
font-weight: 600;
|
||||
font-size: 13px;
|
||||
color: var(--dim);
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
.sec-label .p { color: var(--green); }
|
||||
|
||||
/* ---------- NOW ---------- */
|
||||
.now {
|
||||
background: var(--panel);
|
||||
border: 1px solid var(--border);
|
||||
border-left: 2px solid var(--green);
|
||||
border-radius: var(--radius);
|
||||
padding: 20px clamp(20px, 3vw, 26px) 22px;
|
||||
}
|
||||
.now__head {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: baseline;
|
||||
gap: 16px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.now__cmd {
|
||||
font-family: var(--mono);
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
color: var(--green);
|
||||
}
|
||||
.now__cmd::before { content: '$ '; color: var(--dim); }
|
||||
.now__date {
|
||||
font-family: var(--mono);
|
||||
font-weight: 400;
|
||||
font-size: 12px;
|
||||
color: var(--dim);
|
||||
white-space: nowrap;
|
||||
}
|
||||
.now__body {
|
||||
font-weight: 500;
|
||||
font-size: 15.5px;
|
||||
line-height: 1.65;
|
||||
color: var(--text);
|
||||
margin: 0;
|
||||
}
|
||||
.now__body strong { color: var(--high); font-weight: 700; }
|
||||
|
||||
/* ---------- CARDS ---------- */
|
||||
.cards {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 18px;
|
||||
}
|
||||
|
||||
.card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: var(--panel);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
overflow: hidden;
|
||||
transition: border-color .2s ease, transform .2s ease;
|
||||
}
|
||||
.card:hover { border-color: #3d4650; transform: translateY(-2px); }
|
||||
.card--new { border-color: rgba(0,217,102,0.35); }
|
||||
.card--new:hover { border-color: rgba(0,217,102,0.6); }
|
||||
|
||||
.card__bar {
|
||||
display: flex; align-items: center; gap: 7px;
|
||||
padding: 10px 14px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
background: rgba(255,255,255,0.015);
|
||||
}
|
||||
.card__kind {
|
||||
margin-left: auto;
|
||||
font-family: var(--mono);
|
||||
font-weight: 600;
|
||||
font-size: 11px;
|
||||
letter-spacing: 0.06em;
|
||||
color: var(--dim);
|
||||
}
|
||||
.tag-new { color: var(--green); }
|
||||
|
||||
.card__body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
padding: 20px clamp(18px, 2.4vw, 22px) 18px;
|
||||
}
|
||||
.card__title {
|
||||
font-family: var(--sans);
|
||||
font-weight: 800;
|
||||
font-size: 20px;
|
||||
letter-spacing: -0.01em;
|
||||
color: var(--high);
|
||||
margin: 0 0 8px;
|
||||
}
|
||||
.card__desc {
|
||||
font-weight: 500;
|
||||
font-size: 14px;
|
||||
line-height: 1.55;
|
||||
color: var(--text);
|
||||
margin: 0 0 20px;
|
||||
flex: 1;
|
||||
}
|
||||
.card__action {
|
||||
font-family: var(--mono);
|
||||
font-weight: 500;
|
||||
font-size: 13.5px;
|
||||
color: var(--blue);
|
||||
align-self: flex-start;
|
||||
}
|
||||
.card__action::before { content: '\2192\00a0'; }
|
||||
.card__action:hover { text-decoration: underline; text-underline-offset: 3px; }
|
||||
|
||||
/* ---------- ELSEWHERE ---------- */
|
||||
.pills {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
padding: 0;
|
||||
}
|
||||
.pill {
|
||||
font-family: var(--mono);
|
||||
font-weight: 400;
|
||||
font-size: 13px;
|
||||
color: var(--dim);
|
||||
background: var(--panel);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 999px;
|
||||
padding: 7px 14px;
|
||||
transition: color .18s ease, border-color .18s ease, background .18s ease;
|
||||
}
|
||||
.pill:hover {
|
||||
color: var(--high);
|
||||
border-color: #3d4650;
|
||||
background: var(--elevated);
|
||||
text-decoration: none;
|
||||
}
|
||||
.pill b { color: var(--text); font-weight: 500; }
|
||||
.pill:hover b { color: var(--high); }
|
||||
.pill .sep { color: var(--border); margin: 0 2px; }
|
||||
|
||||
/* ---------- FOOTER ---------- */
|
||||
footer {
|
||||
margin-top: clamp(56px, 9vw, 88px);
|
||||
padding-top: 24px;
|
||||
border-top: 1px solid var(--border);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
.footer__avatar {
|
||||
width: 26px; height: 26px;
|
||||
border-radius: 50%;
|
||||
border: 1px solid var(--border);
|
||||
flex: none;
|
||||
opacity: 0.85;
|
||||
}
|
||||
.footer__line {
|
||||
font-family: var(--mono);
|
||||
font-weight: 400;
|
||||
font-size: 12.5px;
|
||||
line-height: 1.7;
|
||||
color: var(--dim);
|
||||
margin: 0;
|
||||
}
|
||||
.footer__line a { color: var(--dim); text-decoration: underline; text-underline-offset: 2px; }
|
||||
.footer__line a:hover { color: var(--text); }
|
||||
.footer__line .sep { color: var(--border); }
|
||||
.footer__line .home { color: var(--green); }
|
||||
|
||||
/* ---------- RESPONSIVE ---------- */
|
||||
@media (max-width: 860px) {
|
||||
.term { display: none; }
|
||||
.hero { gap: 0; }
|
||||
}
|
||||
|
||||
@media (max-width: 720px) {
|
||||
.cards { grid-template-columns: 1fr; }
|
||||
}
|
||||
|
||||
@media (max-width: 440px) {
|
||||
.avatar { width: 64px; height: 64px; }
|
||||
.hero__lead { gap: 16px; }
|
||||
.now__head { flex-direction: column; gap: 4px; }
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
* { transition: none !important; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<a class="skip" href="#make">Skip to what I make</a>
|
||||
|
||||
<div class="wrap">
|
||||
|
||||
<!-- 1. HERO -->
|
||||
<header class="hero">
|
||||
<div class="hero__lead">
|
||||
<img class="avatar" src="data:image/jpeg;base64,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 truncated
|
||||
alt="Justin Paul avatar" fetchpriority="high" decoding="async">
|
||||
<div class="hero__text">
|
||||
<p class="eyebrow">jpaul.io</p>
|
||||
<h1>Justin Paul</h1>
|
||||
<p class="tagline">I build AI-native tools for infrastructure, agtech, and everyone who’s tired of copy-pasting into a chat window.</p>
|
||||
<p class="subtitle">Drawbar founder · course creator · homelab operator</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<aside class="term" aria-hidden="true">
|
||||
<div class="term__bar">
|
||||
<span class="dot dot--r"></span>
|
||||
<span class="dot dot--y"></span>
|
||||
<span class="dot dot--g"></span>
|
||||
<span class="label">zsh</span>
|
||||
</div>
|
||||
<div class="term__body">
|
||||
<div><span class="p">$</span> whoami</div>
|
||||
<div class="out">[email protected]</div>
|
||||
<div><span class="p">$</span> <span class="cursor"></span></div>
|
||||
</div>
|
||||
</aside>
|
||||
</header>
|
||||
|
||||
<main>
|
||||
<!-- 2. NOW -->
|
||||
<section aria-labelledby="now-h">
|
||||
<div class="now">
|
||||
<div class="now__head">
|
||||
<span class="now__cmd" id="now-h">now</span>
|
||||
<span class="now__date">updated 2026-07-02</span>
|
||||
</div>
|
||||
<p class="now__body">
|
||||
Publishing <strong>The Workflow</strong> — a free 27-module course on the engineering scaffolding around AI-assisted development. New post every Tuesday and Thursday on <a href="https://www.jpaul.me">jpaul.me</a> through August. <strong>Launching a <a href="https://www.youtube.com/@jpaulio">new YouTube channel</a></strong> in parallel: small AI-assisted homelab projects, weekly. Building the LMS it’ll all run on next — live on the same channel. Running <a href="https://drawbar.io">Drawbar</a> between takes.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 3. WHAT I MAKE -->
|
||||
<section id="make" aria-labelledby="make-h">
|
||||
<p class="sec-label" id="make-h"><span class="p">$</span> what I make</p>
|
||||
<div class="cards">
|
||||
|
||||
<!-- A: Course -->
|
||||
<article class="card">
|
||||
<div class="card__bar">
|
||||
<span class="dot dot--r"></span><span class="dot dot--y"></span><span class="dot dot--g"></span>
|
||||
<span class="card__kind">COURSE</span>
|
||||
</div>
|
||||
<div class="card__body">
|
||||
<h2 class="card__title">The Workflow</h2>
|
||||
<p class="card__desc">27 modules and a capstone on the engineering practice around AI-assisted coding. Free. Model- and vendor-neutral.</p>
|
||||
<a class="card__action" href="https://github.com/recklessop/ai-workflow-course/wiki">Read the wiki</a>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<!-- B: Product -->
|
||||
<article class="card">
|
||||
<div class="card__bar">
|
||||
<span class="dot dot--r"></span><span class="dot dot--y"></span><span class="dot dot--g"></span>
|
||||
<span class="card__kind">PRODUCT</span>
|
||||
</div>
|
||||
<div class="card__body">
|
||||
<h2 class="card__title">Drawbar</h2>
|
||||
<p class="card__desc">Agtech data platform. Cattle records, growth tracking, and a working set of MCPs so an AI can actually talk to your herd data.</p>
|
||||
<a class="card__action" href="https://drawbar.io">drawbar.io</a>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<!-- C: Channel (NEW) -->
|
||||
<article class="card card--new">
|
||||
<div class="card__bar">
|
||||
<span class="dot dot--r"></span><span class="dot dot--y"></span><span class="dot dot--g"></span>
|
||||
<span class="card__kind">CHANNEL <span class="tag-new">· NEW</span></span>
|
||||
</div>
|
||||
<div class="card__body">
|
||||
<h2 class="card__title">jpaul.io</h2>
|
||||
<p class="card__desc">Weekly builds — small AI-assisted projects on Raspberry Pis, self-hosted services, and homelab hardware. Every commit follows the <a href="https://www.jpaul.me">Workflow</a> playbook.</p>
|
||||
<a class="card__action" href="https://www.youtube.com/@jpaulio">Subscribe on YouTube</a>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<!-- D: Code -->
|
||||
<article class="card">
|
||||
<div class="card__bar">
|
||||
<span class="dot dot--r"></span><span class="dot dot--y"></span><span class="dot dot--g"></span>
|
||||
<span class="card__kind">CODE</span>
|
||||
</div>
|
||||
<div class="card__body">
|
||||
<h2 class="card__title">Public repos</h2>
|
||||
<p class="card__desc">Course source, MCP servers, homelab configs, small tools. Everything I write in public.</p>
|
||||
<a class="card__action" href="https://github.com/recklessop">github.com/recklessop</a>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 4. ELSEWHERE -->
|
||||
<section aria-labelledby="else-h">
|
||||
<p class="sec-label" id="else-h"><span class="p">$</span> elsewhere</p>
|
||||
<nav class="pills" aria-label="Find me elsewhere">
|
||||
<a class="pill" href="https://www.jpaul.me">blog<span class="sep"> · </span><b>jpaul.me</b></a>
|
||||
<a class="pill" href="https://www.youtube.com/@jpaulio">youtube<span class="sep"> · </span><b>@jpaulio</b></a>
|
||||
<a class="pill" href="https://git.jpaul.io">git<span class="sep"> · </span><b>git.jpaul.io</b></a>
|
||||
<a class="pill" href="https://github.com/recklessop">github<span class="sep"> · </span><b>recklessop</b></a>
|
||||
<a class="pill" href="https://www.linkedin.com/in/jpaul84/">linkedin<span class="sep"> · </span><b>in/jpaul84</b></a>
|
||||
<a class="pill" href="https://www.jpaul.me/feed">rss<span class="sep"> · </span><b>feed.xml</b></a>
|
||||
<a class="pill" href="mailto:[email protected]">email<span class="sep"> · </span><b>[email protected]</b></a>
|
||||
</nav>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<!-- 5. FOOTER -->
|
||||
<footer>
|
||||
<img class="footer__avatar" src="data:image/jpeg;base64,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 truncated
|
||||
<p class="footer__line">
|
||||
<span class="home">~/</span>jpaul.io <span class="sep">·</span> built in a weekend <span class="sep">·</span> powered by <a href="https://git.jpaul.io">self-hosted Gitea</a>, <a href="https://nginx.org">nginx</a> + <a href="https://traefik.io">Traefik</a> <span class="sep">·</span> © 2026
|
||||
</p>
|
||||
</footer>
|
||||
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
server {
|
||||
listen 80;
|
||||
server_name _;
|
||||
root /usr/share/nginx/html;
|
||||
index index.html;
|
||||
|
||||
# Compress the (mostly text) payload. The inlined base64 avatar doesn't
|
||||
# shrink much, but the HTML/SVG/CSS around it does.
|
||||
gzip on;
|
||||
gzip_comp_level 5;
|
||||
gzip_min_length 256;
|
||||
gzip_types text/html text/css application/javascript image/svg+xml application/json application/xml;
|
||||
|
||||
# It's a two-file site. Revalidate so copy edits (git pull, no restart)
|
||||
# go live on the next refresh instead of sticking in a cache.
|
||||
add_header Cache-Control "no-cache" always;
|
||||
add_header X-Content-Type-Options "nosniff" always;
|
||||
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
|
||||
|
||||
location / {
|
||||
try_files $uri $uri/ =404;
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user