浏览代码

Merge branch 'feature/home-page' of bajdev/home into main

bajdev 3 小时之前
父节点
当前提交
c2a397760c

+ 118 - 1
README.md

@@ -1,3 +1,120 @@
 # home
 
-Home page for monoexp.com. Showcase linking to different projects.
+Home page for monoexp.com. Showcase linking to different projects.
+
+Plain HTML and CSS, no build step and no JavaScript.
+
+## Layout
+
+```
+public/            what gets deployed (the site root)
+  index.html       the page: about section, then project cards
+  style.css        all styles, light and dark via prefers-color-scheme
+  favicon.svg
+nginx/
+  monoexp.com.conf reference copy of the server's vhost file
+pull-releases/     one record per pull request, with screenshots
+```
+
+## Adding a project
+
+Copy one `<li class="project">` block in `public/index.html` and change:
+
+- the title in `<h3>`
+- the tag (language or kind), e.g. `Go`
+- the one- or two-sentence description
+- the **Visit** link to `https://<name>.monoexp.com/` (leave it out for source-only projects)
+- the **Source** link to `https://git.monoexp.com/bajdev/<repo>`
+
+Open a PR; merging it deploys the page once the site is on the pipeline.
+
+## Hosting
+
+| | |
+|---|---|
+| Canonical URL | `https://monoexp.com` |
+| `www.monoexp.com` | 301 redirect to the apex |
+| Site root | `/srv/www/vhosts/monoexp.com` |
+| nginx file | `/etc/nginx/vhosts.d/monoexp.com.conf` |
+| Certificate | `monoexp-home` (covers `monoexp.com` and `www.monoexp.com`) |
+
+DNS needs no change: the `@` and `*` A records at Hover already point here.
+
+## First-time setup (as the Admin over SSH)
+
+1. **Certificate.** The `issue-cert` wrapper only accepts subdomains, so the
+   apex goes through certbot directly:
+
+   ```bash
+   sudo certbot certonly --webroot -w /srv/www/acme --cert-name monoexp-home \
+     -d monoexp.com -d www.monoexp.com --deploy-hook 'systemctl reload nginx'
+   ```
+
+   Port 80 already serves `/.well-known/acme-challenge/` for every
+   `.monoexp.com` name, the apex included.
+
+2. **Site folder and first deploy.**
+
+   Create the folder group-writable. It inherits the `webops` group from
+   `/srv/www/vhosts`, and mode `2775` lets every `webops` member, Claude
+   included, write to it:
+
+   ```bash
+   mkdir -m 2775 /srv/www/vhosts/monoexp.com
+   ```
+
+   Then do the first deploy, **either** yourself:
+
+   ```bash
+   git clone git@git.monoexp.com:bajdev/home.git ~/src/home
+   rsync -a --delete ~/src/home/public/ /srv/www/vhosts/monoexp.com/
+   ```
+
+   **or** by asking Claude, which runs
+   `deploy-static monoexp.com ~/src/home/public` from its own account. Pass
+   the source folder explicitly, because the script's default would look for
+   `~/src/monoexp/public`.
+
+   Pick one, not both: whoever deploys first owns the files, and the other
+   account then can't update them. Merge-to-deploy hands them to the `deploy`
+   user afterwards either way.
+
+3. **nginx.**
+
+   ```bash
+   sudo install -o root -g root -m 644 ~/src/home/nginx/monoexp.com.conf /etc/nginx/vhosts.d/
+   sudo nginx -t && sudo systemctl reload nginx
+   ```
+
+4. **Check.**
+
+   ```bash
+   curl -sI https://monoexp.com | head -1          # HTTP/2 200
+   curl -sI https://www.monoexp.com | grep -i location   # https://monoexp.com/
+   curl -sI http://monoexp.com | grep -i location        # https://monoexp.com/
+   ```
+
+## Merge-to-deploy
+
+Same steps as any other site on the pipeline (see the VPS setup plan, Phase 7):
+
+1. In `bajdev/devops`, add `bajdev/home` to `sites.json` through a PR, as a
+   static site:
+
+   ```json
+   "bajdev/home": { "kind": "static", "host": "monoexp.com" }
+   ```
+
+   A static site publishes the repo's `public/` folder by default. To publish
+   a different folder, add `"dir": "<folder>"`. Merge, then reinstall the
+   file:
+   `sudo install -o root -g root -m 644 sites.json /etc/deployd/sites.json`
+2. Hand the deployed files to the deploy user:
+   `sudo chown -R deploy /srv/www/vhosts/monoexp.com`
+3. In Gogs, on `bajdev/home`: add `deploy` (and `claude`, if Claude will work
+   on it) as Write collaborators, turn on branch protection for `main`, and
+   add a webhook: payload URL `http://127.0.0.1:9000/deploy`, content type
+   `application/json`, the secret from `/etc/deployd/secret.env`, and only the
+   **Pull Request** event.
+4. Merge a small test PR and check for a `deploy/…` tag and a line in
+   `/var/log/deploys.log`.

+ 37 - 0
nginx/monoexp.com.conf

@@ -0,0 +1,37 @@
+# /etc/nginx/vhosts.d/monoexp.com.conf
+#
+# monoexp.com is canonical; www.monoexp.com redirects to it.
+# Port 80 (ACME challenges and the HTTP->HTTPS redirect) is already handled
+# by the `.monoexp.com` block in 00-default.conf, which matches the apex too.
+#
+# Certificate: one cert named "monoexp-home" covering both names. The
+# issue-cert wrapper only accepts subdomains, so issue it directly as bajdev:
+#   sudo certbot certonly --webroot -w /srv/www/acme --cert-name monoexp-home \
+#     -d monoexp.com -d www.monoexp.com --deploy-hook 'systemctl reload nginx'
+
+# www -> apex
+server {
+    listen 443 ssl; listen [::]:443 ssl;
+    server_name www.monoexp.com;
+    ssl_certificate     /etc/letsencrypt/live/monoexp-home/fullchain.pem;
+    ssl_certificate_key /etc/letsencrypt/live/monoexp-home/privkey.pem;
+    include snippets/tls-common.conf;
+    return 301 https://monoexp.com$request_uri;
+}
+
+# apex: the home page
+server {
+    listen 443 ssl; listen [::]:443 ssl;
+    server_name monoexp.com;
+    ssl_certificate     /etc/letsencrypt/live/monoexp-home/fullchain.pem;
+    ssl_certificate_key /etc/letsencrypt/live/monoexp-home/privkey.pem;
+    include snippets/tls-common.conf;
+
+    root /srv/www/vhosts/monoexp.com;
+    index index.html;
+
+    location / { try_files $uri $uri/ =404; }
+
+    # Short cache for the stylesheet and icon; HTML keeps nginx's defaults.
+    location ~* \.(css|svg)$ { expires 1h; }
+}

+ 12 - 0
public/favicon.svg

@@ -0,0 +1,12 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
+  <style>
+    rect { fill: #1c1c1a; }
+    path { stroke: #fafaf9; }
+    @media (prefers-color-scheme: dark) {
+      rect { fill: #ececea; }
+      path { stroke: #121211; }
+    }
+  </style>
+  <rect width="32" height="32" rx="7"/>
+  <path d="M8 23V10l8 8 8-8v13" fill="none" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/>
+</svg>

+ 83 - 0
public/index.html

@@ -0,0 +1,83 @@
+<!doctype html>
+<html lang="en">
+<head>
+  <meta charset="utf-8">
+  <meta name="viewport" content="width=device-width, initial-scale=1">
+  <title>Mono Experiments</title>
+  <meta name="description" content="Mono Experiments: a small self-hosted workshop for side projects. Live sites and their source.">
+  <meta name="color-scheme" content="light dark">
+  <link rel="icon" href="/favicon.svg" type="image/svg+xml">
+  <link rel="stylesheet" href="/style.css">
+</head>
+<body>
+  <header class="site-header">
+    <div class="wrap header-inner">
+      <a class="wordmark" href="/">mono<span>exp</span></a>
+      <nav aria-label="Main">
+        <a href="#projects">Projects</a>
+        <a href="https://git.monoexp.com/">Git</a>
+      </nav>
+    </div>
+  </header>
+
+  <main class="wrap">
+    <section id="about" class="about" aria-labelledby="about-title">
+      <h1 id="about-title">Mono Experiments</h1>
+      <p class="lede">
+        A small, self-hosted workshop for side projects.
+      </p>
+      <p>
+        Everything here runs on a single server. Code lives in a self-hosted Git
+        service, and merging a pull request builds and deploys it automatically.
+        Some projects are finished; most are works in progress. Each one below
+        links to the live site and to its source.
+      </p>
+    </section>
+
+    <section id="projects" aria-labelledby="projects-title">
+      <h2 id="projects-title">Projects</h2>
+
+      <ul class="projects">
+        <!--
+          To add a project, copy one <li class="project"> block.
+          Leave out the "Visit" link for source-only projects.
+        -->
+        <li class="project">
+          <div class="project-head">
+            <h3>Posts</h3>
+            <span class="tag">Go</span>
+          </div>
+          <p>
+            A simple blog for write-ups. Posts are written in lightly formatted
+            Markdown and served as plain HTML with basic navigation.
+          </p>
+          <div class="links">
+            <a href="https://posts.monoexp.com/">Visit <span aria-hidden="true">&rarr;</span><span class="sr-only"> Posts</span></a>
+            <a href="https://git.monoexp.com/bajdev/posts">Source<span class="sr-only"> for Posts</span></a>
+          </div>
+        </li>
+
+        <li class="project">
+          <div class="project-head">
+            <h3>DevOps</h3>
+            <span class="tag">Go &middot; Bash</span>
+          </div>
+          <p>
+            The tooling behind this server's merge-to-deploy pipeline: a webhook
+            receiver, a release script and a certificate helper.
+          </p>
+          <div class="links">
+            <a href="https://git.monoexp.com/bajdev/devops">Source<span class="sr-only"> for DevOps</span></a>
+          </div>
+        </li>
+      </ul>
+    </section>
+  </main>
+
+  <footer class="site-footer">
+    <div class="wrap">
+      <p>&copy; 2026 Mono Experiments &middot; <a href="https://git.monoexp.com/bajdev/home">Source for this page</a></p>
+    </div>
+  </footer>
+</body>
+</html>

+ 150 - 0
public/style.css

@@ -0,0 +1,150 @@
+/* monoexp.com — one stylesheet, no framework */
+
+:root {
+  --bg: #fafaf9;
+  --surface: #ffffff;
+  --text: #1c1c1a;
+  --muted: #5f5f5a;
+  --border: #e4e4e0;
+  --accent: #1c1c1a;
+  --tag-bg: #f0f0ed;
+  --focus: #2f6fdb;
+
+  --sans: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
+  --mono: ui-monospace, "SF Mono", Menlo, Consolas, "Liberation Mono", monospace;
+  --radius: 10px;
+  --max: 760px;
+}
+
+@media (prefers-color-scheme: dark) {
+  :root {
+    --bg: #121211;
+    --surface: #1a1a19;
+    --text: #ececea;
+    --muted: #a3a39e;
+    --border: #2c2c2a;
+    --accent: #ececea;
+    --tag-bg: #252524;
+    --focus: #7aa7f5;
+  }
+}
+
+*,
+*::before,
+*::after { box-sizing: border-box; }
+
+html { -webkit-text-size-adjust: 100%; scroll-behavior: smooth; }
+
+body {
+  margin: 0;
+  background: var(--bg);
+  color: var(--text);
+  font: 17px/1.6 var(--sans);
+  -webkit-font-smoothing: antialiased;
+}
+
+a { color: inherit; text-underline-offset: 3px; }
+a:focus-visible { outline: 2px solid var(--focus); outline-offset: 3px; border-radius: 4px; }
+
+.wrap {
+  max-width: var(--max);
+  margin: 0 auto;
+  padding: 0 20px;
+}
+
+.sr-only {
+  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
+  overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
+}
+
+/* Header */
+.site-header { border-bottom: 1px solid var(--border); }
+.header-inner {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  height: 64px;
+}
+.wordmark {
+  font: 600 18px/1 var(--mono);
+  letter-spacing: -0.02em;
+  text-decoration: none;
+}
+.wordmark span { color: var(--muted); }
+nav { display: flex; gap: 20px; }
+nav a {
+  font-size: 15px;
+  color: var(--muted);
+  text-decoration: none;
+}
+nav a:hover { color: var(--text); }
+
+/* About */
+.about { padding: 56px 0 40px; }
+.about h1 {
+  margin: 0 0 12px;
+  font-size: clamp(30px, 6vw, 42px);
+  line-height: 1.15;
+  letter-spacing: -0.03em;
+}
+.about p { margin: 0 0 14px; color: var(--muted); max-width: 62ch; }
+.about .lede { font-size: 20px; color: var(--text); }
+
+/* Projects */
+#projects { padding-bottom: 64px; scroll-margin-top: 16px; }
+#projects h2 {
+  margin: 0 0 16px;
+  font: 600 13px/1 var(--mono);
+  text-transform: uppercase;
+  letter-spacing: 0.08em;
+  color: var(--muted);
+}
+.projects {
+  list-style: none;
+  margin: 0;
+  padding: 0;
+  display: grid;
+  grid-template-columns: 1fr;
+  gap: 16px;
+}
+@media (min-width: 640px) {
+  .projects { grid-template-columns: 1fr 1fr; }
+}
+.project {
+  display: flex;
+  flex-direction: column;
+  background: var(--surface);
+  border: 1px solid var(--border);
+  border-radius: var(--radius);
+  padding: 20px;
+}
+.project-head {
+  display: flex;
+  align-items: baseline;
+  justify-content: space-between;
+  gap: 12px;
+  margin-bottom: 8px;
+}
+.project h3 { margin: 0; font-size: 19px; letter-spacing: -0.01em; }
+.tag {
+  flex-shrink: 0;
+  font: 12px/1 var(--mono);
+  color: var(--muted);
+  background: var(--tag-bg);
+  padding: 5px 8px;
+  border-radius: 6px;
+}
+.project p { margin: 0 0 18px; color: var(--muted); font-size: 15.5px; }
+.links {
+  margin-top: auto;
+  display: flex;
+  gap: 18px;
+  font-size: 15px;
+  font-weight: 500;
+}
+.links a { text-decoration: none; border-bottom: 1px solid var(--border); padding-bottom: 1px; }
+.links a:hover { border-color: var(--accent); }
+
+/* Footer */
+.site-footer { border-top: 1px solid var(--border); }
+.site-footer p { margin: 0; padding: 24px 0 32px; font-size: 14px; color: var(--muted); }

+ 42 - 0
pull-releases/001-home-page/README.md

@@ -0,0 +1,42 @@
+# Add static home page, nginx vhost and setup README
+
+Branch `feature/home-page`
+
+## What changed
+
+- **Home page (`public/`):** a static showcase for monoexp.com in plain HTML and CSS, with no JavaScript and no build step. It has an about section, then project cards for **Posts** (Visit and Source links) and **DevOps** (Source only).
+- **Light and dark themes:** both follow the visitor's system setting through `prefers-color-scheme`. The favicon switches too.
+- **Accessibility:** link text names its project for screen readers, the nav is labelled, and keyboard focus shows a visible outline.
+- **nginx vhost (`nginx/monoexp.com.conf`):** a reference copy for the server. `www.monoexp.com` redirects to `https://monoexp.com`, and one certificate, `monoexp-home`, covers both names. The stylesheet and icon get a one-hour cache.
+- **README:** layout, how to add a project card, hosting details, first-time setup (certificate, site folder and first deploy, nginx, checks), and onboarding to the merge-to-deploy pipeline.
+- **Release records:** this `pull-releases/` folder, following the posts repo's convention.
+- **LICENSE:** unchanged.
+
+## Notes
+
+- **Nothing is deployed by this PR.** The certificate, site folder, nginx file and pipeline onboarding are manual steps for the Admin, described in the README.
+- **First deploy:** the site folder is created with `mkdir -m 2775` so it's group-writable for `webops`. The first deploy is either the Admin's `rsync` or Claude's `deploy-static`, not both, because whoever deploys first owns the files.
+- **Pipeline entry:** the README gives the actual `sites.json` entry for a static site: `"bajdev/home": { "kind": "static", "host": "monoexp.com" }`. Static sites publish `public/` by default.
+- **Certificate:** `issue-cert` only accepts subdomains, so the apex certificate is issued with certbot directly, as the README shows.
+
+## Testing
+
+- Served `public/` locally: `/`, `/style.css` and `/favicon.svg` return 200 with the right content types.
+- Every link on the page returns 200: Posts, the three Gogs repos, the Gogs home, and the page's own assets.
+- Checked against the server: DNS for `monoexp.com` and `www.monoexp.com` points here, and the existing `.monoexp.com` port-80 block also covers the apex, so the certificate's challenge will be served. The `sites.json` entry parses alongside the installed file. `mkdir -m 2775` gives `drwxrwsr-x` regardless of umask.
+- Not tested: the nginx vhost itself. It can't be checked with `nginx -t` until the `monoexp-home` certificate exists.
+- Viewed desktop, mobile and dark-mode captures: the layout holds at phone width, with the cards stacking into one column.
+
+## Screenshots
+
+From the branch's `public/` folder served locally.
+
+| Desktop | Mobile |
+|---|---|
+| [![Desktop](desktop.png)](desktop.png) | [![Mobile](mobile.png)](mobile.png) |
+
+| Dark mode |
+|---|
+| [![Dark mode](dark.png)](dark.png) |
+
+🤖 Generated with [Claude Code](https://claude.com/claude-code)

二进制
pull-releases/001-home-page/dark.png


二进制
pull-releases/001-home-page/desktop.png


二进制
pull-releases/001-home-page/mobile.png


+ 36 - 0
pull-releases/README.md

@@ -0,0 +1,36 @@
+# Pull releases
+
+One folder per pull request, so each change has a written record with
+screenshots. The record is added on the PR's own branch, so it is reviewed and
+merged with the change. This follows the same convention as the
+[posts](https://git.monoexp.com/bajdev/posts/src/main/pull-releases) repo.
+
+```
+pull-releases/
+  NNN-branch-name/      NNN = next number in sequence, branch name without feature/
+    README.md           title (first line), what changed, notes, testing
+    desktop.png         home page at 1280×900
+    mobile.png          home page at 390×844 (2× pixel density)
+```
+
+To open the PR on Gogs, use the compare link for the branch, paste the first
+line of `README.md` as the title and the rest as the description.
+
+Screenshots are taken from the branch's own `public/` folder served locally,
+before it is deployed, so they show exactly what the PR will put live:
+
+```bash
+python3 -m http.server 18005 --bind 127.0.0.1 --directory public &
+screenshot-site http://127.0.0.1:18005/ pull-releases/NNN-branch-name
+```
+
+Folder names use lowercase letters, digits and hyphens only. Embed the
+screenshots as `[![Desktop](desktop.png)](desktop.png)` so they link to the
+full-size image. Extra captures, such as a dark-mode view, can sit alongside.
+
+Unlike posts, this site has no version number and no release notes page:
+the records live in the repo only.
+
+| PR | Change |
+|---|---|
+| [001](001-home-page/) | Static home page, nginx vhost and setup README |