fuzzyworld-tools / commit
One site-wide footer (gen.site_footer) for code, apps, landing page and blog
README.md | 5 +++-- server/blog.py | 18 ++++++++++++------ server/gen.py | 40 +++++++++++++++++++++++++--------------- server/install.sh | 2 +- server/site-blog/blog.css | 5 +++++ server/site/index.html | 2 +- tests/test_blog.py | 30 +++++++++++++++++++++++++++--- 7 files changed, 74 insertions(+), 28 deletions(-) diff --git a/README.md b/README.md index 1b99d3a..5b0ce7d 100644 --- a/README.md +++ b/README.md @@ -300,8 +300,9 @@ fw blog deploy ~/Blog/fw-blog.json --dry-run # then, for real: --yes (or answer top of `server/blog.py`. `source` and `comments` are relative to the config file's folder; `pages_dir` and `media` are relative to `source`. `site` puts the site-wide bar above the blog's own bar (as on code and apps): `{"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`). + `"nav": "gen"` takes the fuzzyworld nav from `NAV` in `server/gen.py`, the one source, and also shows the shared + four-column footer (`gen.site_footer()`, with the blog's copyright and Feed line under it). The landing page's nav and + footer are built from the same functions by `gen.py landing` in `install.sh` (`<!--SITE-NAV-->`, `<!--SITE-FOOTER-->`). 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 diff --git a/server/blog.py b/server/blog.py index b38cbb2..13afe6c 100644 --- a/server/blog.py +++ b/server/blog.py @@ -116,9 +116,11 @@ def load_config(path): raise BlogError("nav must be a list of [label, path] pairs") site = cfg["site"] if site is not None: + if isinstance(site, dict) and "footer" in site: + raise BlogError('site: "footer" is set by "nav": "gen", not in the config') if isinstance(site, dict) and site.get("nav") == "gen": - site = cfg["site"] = {**site, "nav": gen.site_nav()} # "gen": the fuzzyworld nav, built from NAV in gen.py - if not (isinstance(site, dict) and set(site) <= {"name", "url", "nav", "current"} and {"name", "url", "nav"} <= set(site) + site = cfg["site"] = {**site, "nav": gen.site_nav(), "footer": True} # "gen": the fuzzyworld nav and footer, built in gen.py + if not (isinstance(site, dict) and set(site) <= {"name", "url", "nav", "current", "footer"} and {"name", "url", "nav"} <= set(site) and isinstance(site["name"], str) and isinstance(site["url"], str) and isinstance(site["nav"], list) and isinstance(site.get("current", ""), str) and all(isinstance(n, list) and len(n) == 2 and all(isinstance(x, str) for x in n) for n in site["nav"])): @@ -682,6 +684,13 @@ def blog_page(cfg, title, body, path): header = (f'<header class="site"><div class="bar"><a class="brand" href="/">{esc(cfg["title"])}</a>{tagline}' f'<nav class="site-nav" aria-label="Site">{links}</nav></div></header>') feed = cfg["feed"]["path"] + legal = (f'<p>{esc(copyright_text(cfg))}</p>\n' + f'<p><a href="{esc(site["url"] if site else "https://fuzzyworld.net/", quote=True)}">{esc(site["name"] if site else "fuzzyworld.net")}</a>' + f' · <a href="{esc(feed, quote=True)}">Feed</a></p>') + if site and site.get("footer"): # the site-wide four-column footer, then the blog's own line under it + footer = f'<footer class="site"><div class="cols">{gen.site_footer("writing")}</div>\n<div class="cols legal">\n{legal}\n</div></footer>' + else: + footer = f'<footer class="site"><div class="cols">\n{legal}\n</div></footer>' head_title = cfg["title"] if path == "/" else f'{title} · {cfg["title"]}' return f"""<!doctype html> <html lang="en"> @@ -701,10 +710,7 @@ def blog_page(cfg, title, body, path): <main> {body} </main> -<footer class="site"><div class="cols"> -<p>{esc(copyright_text(cfg))}</p> -<p><a href="{esc(site["url"] if site else "https://fuzzyworld.net/", quote=True)}">{esc(site["name"] if site else "fuzzyworld.net")}</a> · <a href="{esc(feed, quote=True)}">Feed</a></p> -</div></footer> +{footer} </body> </html> """ diff --git a/server/gen.py b/server/gen.py index 1696f70..eacb455 100755 --- a/server/gen.py +++ b/server/gen.py @@ -4,7 +4,7 @@ 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 and <!--LANDING-TEXT--> from landing.md + gen.py landing <src> <dest> write the landing page: src with <!--SITE-NAV--> filled in from NAV, <!--SITE-FOOTER--> from site_footer() 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) @@ -158,14 +158,33 @@ def site_nav(): return [[label, SITE_URLS[st] + path] for _, label, st, path in NAV] +def site_footer(here): + """The columns of the site-wide footer (inside <div class="cols">), with links relative to the site `here` + ("code", "apps", "writing", or "landing" for fuzzyworld.net). One source for the code and apps pages, the + landing page and the blog.""" + def link(st, path): + return SITE_URLS[st] + path if st != here else path + return (f'<div><h2>fuzzyworld</h2><a href="{HOME_URL}/">fuzzyworld.net</a>' + f'<a href="{link("code", "/")}">Code: all repositories</a>' + f'<a href="{link("apps", "/")}">Apps</a>' + f'<a href="{WRITING_URL}/">Writing</a>' + f'<a href="{link("code", "/ci/")}">CI: what is building</a></div>' + f'<div><h2>About this site</h2><a href="{link("code", "/why-i-left-gitlab/")}">Why I left GitLab</a>' + f'<a href="{link("code", "/contribute/")}">Contribute and report bugs</a></div>' + f'<div><h2>Built with</h2><a href="{link("code", "/fuzzyworld-tools/")}">fuzzyworld-tools</a>' + '<span>Static files only: no accounts, no sign-in, no rate limits. MIT-licensed.</span></div>') + + def build_landing(src, dest): - """Writes the landing page: `src` with <!--SITE-NAV--> replaced by the nav links built from NAV and, if the + """Writes the landing page: `src` with <!--SITE-NAV--> replaced by the nav links built from NAV, + <!--SITE-FOOTER--> by site_footer("landing") 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") + for marker in ("<!--SITE-NAV-->", "<!--SITE-FOOTER-->"): + if marker not in text: + raise SystemExit(f"{src}: no {marker} placeholder") if "<!--LANDING-TEXT-->" in text: if not markdown: raise SystemExit("gen.py landing needs Python-Markdown in " + PYLIB) @@ -175,6 +194,7 @@ def build_landing(src, dest): 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) + text = text.replace("<!--SITE-FOOTER-->", site_footer("landing")) write(dest, text.replace("<!--SITE-NAV-->", links)) @@ -192,17 +212,7 @@ def page(title, body, crumbs=None, view=None, site="code", refresh=None, section for key, label, st, path in NAV) header = (f'<header class="site"><div class="bar"><a class="brand" href="{HOME_URL}/">fuzzyworld</a>' f'<nav class="site-nav" aria-label="Site">{links}</nav></div></header>') - footer = ('<footer class="site"><div class="cols">' - f'<div><h2>fuzzyworld</h2><a href="{HOME_URL}/">fuzzyworld.net</a>' - f'<a href="{site_link("code", "/", site)}">Code: all repositories</a>' - f'<a href="{site_link("apps", "/", site)}">Apps</a>' - f'<a href="{WRITING_URL}/">Writing</a>' - f'<a href="{site_link("code", "/ci/", site)}">CI: what is building</a></div>' - f'<div><h2>About this site</h2><a href="{site_link("code", "/why-i-left-gitlab/", site)}">Why I left GitLab</a>' - f'<a href="{site_link("code", "/contribute/", site)}">Contribute and report bugs</a></div>' - f'<div><h2>Built with</h2><a href="{site_link("code", "/fuzzyworld-tools/", site)}">fuzzyworld-tools</a>' - '<span>Static files only: no accounts, no sign-in, no rate limits. MIT-licensed.</span></div>' - '</div></footer>') + footer = f'<footer class="site"><div class="cols">{site_footer(site)}</div></footer>' nav = "" if view: nav = (f'<nav class="repo-nav"><a href="{url(view.name)}/">Summary</a>' diff --git a/server/install.sh b/server/install.sh index 1a33f4a..6edd2a9 100755 --- a/server/install.sh +++ b/server/install.sh @@ -35,7 +35,7 @@ cp ~/fw/tools/server/site/_shared.css ~/fw/tools/server/site/_icons.css ~/fw/too chmod 644 "$apps"/_shared.css "$apps"/_icons.css "$apps"/style.css "$apps"/favicon.ico # Main fuzzyworld.net site main_site=~/fuzzyworld.net -python3 ~/fw/tools/server/gen.py landing ~/fw/tools/server/site/index.html "$main_site/index.html" # nav built from NAV in gen.py +python3 ~/fw/tools/server/gen.py landing ~/fw/tools/server/site/index.html "$main_site/index.html" # nav and footer built in gen.py cp ~/fw/tools/server/site/main.css "$main_site/" mkdir -p "$main_site/projects" cp ~/fw/tools/server/site/projects-redirect.html "$main_site/projects/index.html" # the old projects page, now /projects/ on code. diff --git a/server/site-blog/blog.css b/server/site-blog/blog.css index 1c53e2a..5c1e975 100644 --- a/server/site-blog/blog.css +++ b/server/site-blog/blog.css @@ -32,6 +32,11 @@ footer.site { margin-top: var(--spacing-xl); border-top: 1px solid var(--color-b color: var(--color-fg-muted); font-size: var(--font-size-sm); } footer.site .cols { padding-top: var(--spacing-md); padding-bottom: var(--spacing-lg); } footer.site p { margin: var(--spacing-xs) 0; } +/* The site-wide footer columns (gen.site_footer), same look as on fuzzyworld.net; the blog's own line sits under them. */ +footer.site .cols:has(> div) { display: grid; grid-template-columns: repeat(auto-fit, minmax(14rem, 1fr)); gap: var(--spacing-lg); padding-top: var(--spacing-lg); } +footer.site h2 { font-size: var(--font-size-xs); text-transform: uppercase; letter-spacing: 0.06em; color: var(--color-fg-muted); margin: 0 0 var(--spacing-xs); } +footer.site .cols > div a, footer.site .cols > div span { display: block; padding: var(--spacing-xs) 0; } +footer.site .legal { padding-top: 0; } /* Plain lists: room for the numbers and bullets, which sit outside the list box (the reset in _shared.css removes all list padding). Same fix as on code.fuzzyworld.net. */ diff --git a/server/site/index.html b/server/site/index.html index d5d8d86..5ee556d 100644 --- a/server/site/index.html +++ b/server/site/index.html @@ -14,6 +14,6 @@ <!--LANDING-TEXT--> </main> -<footer class="site"><div class="cols"><div><h2>fuzzyworld</h2><a href="/">fuzzyworld.net</a><a href="https://code.fuzzyworld.net/">Code: all repositories</a><a href="https://apps.fuzzyworld.net/">Apps</a><a href="https://writing.fuzzyworld.net/">Writing</a><a href="https://code.fuzzyworld.net/ci/">CI: what is building</a></div><div><h2>About this site</h2><a href="https://code.fuzzyworld.net/why-i-left-gitlab/">Why I left GitLab</a><a href="https://code.fuzzyworld.net/contribute/">Contribute and report bugs</a></div><div><h2>Built with</h2><a href="https://code.fuzzyworld.net/fuzzyworld-tools/">fuzzyworld-tools</a><span>Static files only: no accounts, no sign-in, no rate limits. MIT-licensed.</span></div></div></footer> +<footer class="site"><div class="cols"><!--SITE-FOOTER--></div></footer> </body> </html> diff --git a/tests/test_blog.py b/tests/test_blog.py index 65bdc7e..20bfa72 100644 --- a/tests/test_blog.py +++ b/tests/test_blog.py @@ -368,12 +368,36 @@ class Build(unittest.TestCase): self.assertIn(f'<a href="{url}"', page) self.assertIn(f">{label}</a>", page) + def test_site_footer_is_shared_by_blog_and_landing_page(self): + t = self.blog_tree(site={"name": "fuzzyworld", "url": "https://fuzzyworld.net/", "nav": "gen", "current": "Writing"}) + _, out = t.build() + page = t.read(out, "2018/03/02/first-post/index.html") + self.assertIn(gen.site_footer("writing"), page) + self.assertIn("All rights reserved", page) # the blog's own line stays under the shared columns + 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><footer><!--SITE-FOOTER--></footer>") + gen.build_landing(src, dest) + with open(dest) as f: + self.assertIn(gen.site_footer("landing"), f.read()) + self.assertIn('href="/ci/"', gen.site_footer("code")) # own site stays relative + self.assertIn('href="https://apps.fuzzyworld.net/"', gen.site_footer("code")) + self.assertIn('href="/"', gen.site_footer("apps")) + self.assertIn('href="https://code.fuzzyworld.net/"', gen.site_footer("apps")) + t = self.blog_tree(site={"name": "x", "url": "https://x.org/", "nav": [["Code", "https://c.x.org/"]]}) + _, out = t.build() + self.assertNotIn("Built with", t.read(out, "2018/03/02/first-post/index.html")) # explicit nav: no shared footer + with self.assertRaises(blog.BlogError): + Tree(self, {"site": {"name": "x", "url": "u", "nav": [], "footer": True}}).cfg() + self.assertIn('href="https://code.fuzzyworld.net/ci/"', gen.site_footer("apps")) + def test_landing_page_nav_comes_from_nav(self): import tempfile 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>') + f.write('<nav><!--SITE-NAV--></nav><!--SITE-FOOTER-->') gen.build_landing(src, dest) with open(dest) as f: text = f.read() @@ -385,7 +409,7 @@ class Build(unittest.TestCase): 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-->") + f.write("<nav><!--SITE-NAV--></nav><!--SITE-FOOTER--><!--LANDING-TEXT-->") with self.assertRaises(SystemExit): gen.build_landing(src, os.path.join(d, "out.html")) @@ -394,7 +418,7 @@ class Build(unittest.TestCase): 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>") + f.write("<nav><!--SITE-NAV--></nav><!--SITE-FOOTER--><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)