fuzzyworld-tools / commit
Landing page text from landing.md; blog and til retired; Markdown installed by install.sh
PLAN.md | 5 ++--- README.md | 4 +++- server/gen.py | 18 ++++++++++++++---- server/install.sh | 2 ++ server/site/index.html | 6 +----- server/site/landing.md | 26 ++++++++++++++++++++++++++ tests/test_blog.py | 23 +++++++++++++++++++++++ 7 files changed, 71 insertions(+), 13 deletions(-) diff --git a/PLAN.md b/PLAN.md index 456975b..6e76a03 100644 --- a/PLAN.md +++ b/PLAN.md @@ -46,7 +46,7 @@ The order goes from cheap to expensive, and each step reuses what the one before 0. **Runners without TrueNAS** (decided 2026-10-28; see [Runners](#runners)): runner tags, the laptop as a runner when idle and plugged in, secrets in a folder on each runner, and "no runners" as a normal state. Goes first. - **Built 2026-10-28; deployed 2026-10-31** (server, then the TrueNAS runner, then the laptop runner with `--authorize`, + **Built and committed 2026-10-31 (a499715); deployed 2026-10-31** (server, then the TrueNAS runner, then the laptop runner with `--authorize`, in that order: a new runner against the old server has every heartbeat refused). Smoke-tested: both runners poll DreamHost and find nothing to run; no real job has gone through them yet. Review follow-ups (claim check on `done`, tolerant `release`) deployed the same day. @@ -105,8 +105,7 @@ the source); footer "All rights reserved" notice with an AI-training line and a WordPress text wins when a source file differs (similarity under 0.95: 100 of the 142 posts are written from WordPress into `~/Blog/wordpress/`); the old site's custom CSS is carried over (hidden `<br>`, dotted scene breaks, underlined chapter headings); a light/dark switch and reading-progress bar added (they needed `script-src 'self'`). The Atom -writer exists now, so Next 1 (Feeds) can reuse it. **Still to do:** redirect `blog.fuzzyworld.net` to `writing.` -(Q6, needs a WordPress backup first), building from the push hook after the move to a private repo, +writer exists now, so Next 1 (Feeds) can reuse it. **Done 2026-11-01 (Q5/Q6):** `blog.fuzzyworld.net` 301-redirects every path (query kept) to `writing.`; `til.fuzzyworld.net` answers 410 Gone. Both WordPress folders stay on the server, unserved; the original `.htaccess` of each is saved as `.htaccess.wp-20261101`. Backup: a WordPress export of the blog (Pavel, `~/Downloads`). The ancient `old.blog.fuzzyworld.net` copy was moved to `~/old/` on the server (it was not served). **Still to do:** building from the push hook after the move to a private repo, the `til` blog (Q14, same importer with `--wp https://til.fuzzyworld.net`). Below, the plan as written before the build: it still says `blog.fuzzyworld.net` in places; read that as `writing.fuzzyworld.net`. diff --git a/README.md b/README.md index 21fcac9..1b99d3a 100644 --- a/README.md +++ b/README.md @@ -109,7 +109,7 @@ Other settings in the repo config: `fuzzyworld.description`, `fuzzyworld.ci` (`o | `~/bin/fw` | symlink to `~/fw/tools/server/fw` | | `~/fw/state/` | per-repo page manifests and locks | | `~/fw/logs/<name>.log` | output of each push's publish run | -| `~/fw/pylib/` | Python-Markdown 3.7 (README rendering) and Pygments 2.21.0 (syntax highlighting), `pip install --target`; `install.sh` adds Pygments | +| `~/fw/pylib/` | Python-Markdown 3.7 (README rendering) and Pygments 2.21.0 (syntax highlighting), `pip install --target`; `install.sh` adds both (the landing page needs Markdown) | | `~/fw/queue/pending/`, `running/` | CI jobs, one JSON file each (see [CI](#ci)); `running/<job>.beat` is the runner's heartbeat, `declined/` the runners that gave a job back | | `~/fw/runners/<runner>` | one file per runner: its tags, and (by its time) when it last checked in | | `~/fw/ci/<name>/<sha>/` | CI results uploaded by runners: `pipeline.json` and one log per job | @@ -302,6 +302,8 @@ fw blog deploy ~/Blog/fw-blog.json --dry-run # then, for real: --yes (or answer `{"name": "fuzzyworld", "url": "https://fuzzyworld.net/", "nav": [[label, url], ...], "current": "Writing"}`; `"nav": "gen"` takes the fuzzyworld nav from `NAV` in `server/gen.py`, the one source (the landing page's nav is built from it too, by `gen.py landing` in `install.sh`). + The landing page's text is Markdown in `server/site/landing.md`, rendered into the `<!--LANDING-TEXT-->` spot of + `server/site/index.html` by the same command. - **Output:** posts at `/YYYY/MM/DD/<slug>/`, `/blog/` (every post by year and month, with excerpts and a "Continue reading" link), `/tag/<slug>/`, pages, an Atom feed at `/feed/` (`server/atom.py`), `.htaccess` (redirects from `redirects` and `wp_ids`, no script execution, a security policy that allows only the site's own `blog.js`), diff --git a/server/gen.py b/server/gen.py index c980f54..1696f70 100755 --- a/server/gen.py +++ b/server/gen.py @@ -4,15 +4,15 @@ gen.py repo <name> build or update the pages of one public repo gen.py index rebuild the site index (public repos only) gen.py apps rebuild the apps.fuzzyworld.net index from deployed apps - gen.py landing <src> <dest> write the landing page: src with <!--SITE-NAV--> filled in from NAV + gen.py landing <src> <dest> write the landing page: src with <!--SITE-NAV--> filled in from NAV and <!--LANDING-TEXT--> from landing.md gen.py content render server/content/<name>.md to code.fuzzyworld.net/<name>/ (site pages written in Markdown) gen.py ci <name> rebuild one public repo's CI pages, then the CI dashboard gen.py ci-dashboard rebuild code.fuzzyworld.net/ci/ (queue, running, recent; public repos only) gen.py ci-status the same, as text for the terminal, private repos included (fw ci-watch) Only the git CLI and the standard library are required. From ~/fw/pylib, when -importable: Python-Markdown renders README/.md files (otherwise plain text), and -Pygments colors source code (otherwise uncolored). +importable: Python-Markdown renders README/.md files (otherwise plain text; `landing` +and `content` need it) and Pygments colors source code (otherwise uncolored). Pages are incremental: a manifest per repo (~/fw/state/<name>.json) records the blob SHA behind every file page, so a push rewrites only what changed. @@ -159,12 +159,22 @@ def site_nav(): def build_landing(src, dest): - """Writes the landing page: `src` with <!--SITE-NAV--> replaced by the nav links built from NAV.""" + """Writes the landing page: `src` with <!--SITE-NAV--> replaced by the nav links built from NAV and, if the + page has a <!--LANDING-TEXT-->, that replaced by landing.md (next to `src`) rendered from Markdown.""" links = "".join(f'<a href="{html.escape(url, quote=True)}">{html.escape(label)}</a>' for label, url in site_nav()) with open(src, encoding="utf-8") as f: text = f.read() if "<!--SITE-NAV-->" not in text: raise SystemExit(f"{src}: no <!--SITE-NAV--> placeholder") + if "<!--LANDING-TEXT-->" in text: + if not markdown: + raise SystemExit("gen.py landing needs Python-Markdown in " + PYLIB) + md_path = os.path.join(os.path.dirname(src), "landing.md") + if not os.path.exists(md_path): + raise SystemExit(f"{src}: has <!--LANDING-TEXT--> but {md_path} does not exist") + with open(md_path, encoding="utf-8") as f: + body = markdown.markdown(f.read(), extensions=["sane_lists"], output_format="html") + text = text.replace("<!--LANDING-TEXT-->", body) write(dest, text.replace("<!--SITE-NAV-->", links)) diff --git a/server/install.sh b/server/install.sh index a17e9bf..1a33f4a 100755 --- a/server/install.sh +++ b/server/install.sh @@ -21,6 +21,8 @@ git config --global core.bigFileThreshold 16m mkdir -p ~/bin ~/fw/state ~/fw/logs ~/fw/queue/pending ~/fw/queue/running ~/fw/ci chmod +x ~/fw/tools/server/runner-api ~/fw/tools/server/mirror-only # Pygments colors source code on the file pages (gen.py works without it, uncolored). +# Python-Markdown renders the README pages and the landing page text (`gen.py landing` needs it). +[ -d ~/fw/pylib/markdown ] || python3 -m pip install --quiet --target ~/fw/pylib "Markdown==3.7" [ -d ~/fw/pylib/pygments ] || python3 -m pip install --quiet --target ~/fw/pylib "Pygments==2.21.0" ln -sfn ~/fw/tools/server/fw ~/bin/fw site=~/code.fuzzyworld.net diff --git a/server/site/index.html b/server/site/index.html index b266c40..0585247 100644 --- a/server/site/index.html +++ b/server/site/index.html @@ -11,7 +11,7 @@ <header class="site"><div class="bar"><a class="brand" href="/">fuzzyworld</a><nav class="site-nav" aria-label="Site"><!--SITE-NAV--></nav></div></header> <main> <h1>Welcome to Pavel Safronov's website</h1> -<p>I'm a software engineer who dabbles in fiction writing.</p> +<!--LANDING-TEXT--> <h2>Links</h2> <ul class="list-vertical"> @@ -39,10 +39,6 @@ <img src="/github.png" class="small-logo" alt="" /> <a href="https://github.com/PavelSafronov">GitHub (work)</a> </li> - <li> - <img src="/favicon.ico" class="small-logo" alt="" /> - <a href="http://til.fuzzyworld.net/">TIL Blog</a> - </li> <li> <script type='text/javascript' src='https://storage.ko-fi.com/cdn/widget/Widget_2.js'></script> <script type='text/javascript'>kofiwidget2.init('Support me on Ko-fi', '#72a4f2', 'Q5Q01YFWPW'); kofiwidget2.draw();</script> diff --git a/server/site/landing.md b/server/site/landing.md new file mode 100644 index 0000000..ff7df1c --- /dev/null +++ b/server/site/landing.md @@ -0,0 +1,26 @@ +My name is Pavel Safronov and I'm a software engineer and I write fiction. +This website started out as a personal hub for finding me and for some small projects, though recently I've started transitioning various dependencies here as well. About a week ago I moved my code development from GitLab to this website, complete with a simple UI, git repos, and even CI. +[Why I left GitLab](https://code.fuzzyworld.net/why-i-left-gitlab/) is a short write-up about the move. + +Recently it became so easy for me to add more content and move other parts of my digital life here. +So a week after the GitLab move (today, as I write this, November 1, 2026), I moved my writing site from WordPress to a custom approach that publishes my Markdown files directly to this site whenever I want. + +As I move away from relying on third-party platforms (GitLab and WordPress, so far), I am writing the tools to do this, and these tools are entirely open source: [fuzzyworld-tools](https://code.fuzzyworld.net/fuzzyworld-tools/). +That is the package that powers this site, so aside from the content, everything you see here is built with it. + +I also write fiction. +Mostly it's just short sci-fi stories, but I am currently (November 1, 2026) working on four different manuscripts that I'm going to try to get published. +You can see some of my [past highlights](https://writing.fuzzyworld.net/) and read about my [upcoming work](https://writing.fuzzyworld.net/upcoming/). + +You can find my code, web-apps, and writing by following the menu at the top of the page, or by going directly to: + +- [Code](https://code.fuzzyworld.net/) +- [Apps and games](https://apps.fuzzyworld.net/) +- [Writing](https://writing.fuzzyworld.net/) + +And if you want to get in touch, there are many ways: + +- Email: <pavel@fuzzyworld.net> +- [LinkedIn](https://www.linkedin.com/in/psafronov/) +- [GitHub](https://github.com/PavelSafronov) (mostly work-related) +- [Schedule a meeting with me](https://calendly.com/pavel-safronov/) using Calendly diff --git a/tests/test_blog.py b/tests/test_blog.py index 42620dd..65bdc7e 100644 --- a/tests/test_blog.py +++ b/tests/test_blog.py @@ -381,6 +381,29 @@ class Build(unittest.TestCase): for label, url in gen.site_nav(): self.assertIn(f'<a href="{url}">{label}</a>', text) + def test_landing_text_without_landing_md_is_an_error(self): + with tempfile.TemporaryDirectory() as d: + src = os.path.join(d, "in.html") + with open(src, "w") as f: + f.write("<nav><!--SITE-NAV--></nav><!--LANDING-TEXT-->") + with self.assertRaises(SystemExit): + gen.build_landing(src, os.path.join(d, "out.html")) + + @needs_markdown + def test_landing_text_is_rendered_from_landing_md(self): + with tempfile.TemporaryDirectory() as d: + src, dest = os.path.join(d, "in.html"), os.path.join(d, "out.html") + with open(src, "w") as f: + f.write("<nav><!--SITE-NAV--></nav><main><!--LANDING-TEXT--></main>") + with open(os.path.join(d, "landing.md"), "w") as f: + f.write("Hello <https://example.com/>\n\n- one\n- two\n") + gen.build_landing(src, dest) + with open(dest) as f: + text = f.read() + self.assertNotIn("LANDING-TEXT", text) + self.assertIn('<a href="https://example.com/">', text) + self.assertIn("<li>two</li>", text) + def test_landing_page_without_the_placeholder_is_an_error(self): with tempfile.TemporaryDirectory() as d: src = os.path.join(d, "in.html")