runv.club docs
contents

Start here

Your corner of the internet

Writing

Photos

Make it yours

Together

Tools and services

Your account

Everything at once

Make it yours

Your blog and your photo gallery come with a plain look that works for everyone. You don't have to keep it. There are three ways to change it, and each one goes further than the last:

  1. A style.css changes colors, fonts, sizes and layout. Write only what you want different; everything else stays.
  2. A template.html replaces the page itself. Header, menus, the order of things, scripts: all of it is yours.
  3. Your own pages, by hand, in ~/public_html. No generator at all.

Everything here also works for the gallery, with small differences that each section points out. Gemini and Gopher stay as they are: they have no styles to change.

Where the files in your home end up

1. A style.css

Put a file named style.css in ~/blog, or in ~/pics for the gallery. It goes up next to your pages and loads after the default style, so any rule you write wins. The server picks it up within a few minutes, or run blog build (or pics) to see it now.

A plain file of up to 64 KB. Delete it and you're back to the default.

What the blog has to style

Selector What it is
html, body the page and its column of text
h1 the title of a post, or of the blog
.date the date under a post's title
ul.posts, ul.posts li the list of posts on the front page
pre, code code blocks and bits of code
img pictures in your posts
a links
footer the links at the bottom

What the gallery has to style

Selector What it is
html, body, h1, a the page itself
.grid the grid of photos on the front page
.grid a, .grid img, .grid span each photo, its thumbnail and its caption
figure, figcaption a photo on its own page, and its caption
.when the date under it
nav the newer and older links
footer the links at the bottom

Recipe: black and white, red underlines

Close to the one on my own blog. Plain, readable, a little bit of a terminal in the titles.

:root { --accent: #d00000; }
html { background: #fff; color: #000; border-top: 6px solid #000; }
body { max-width: 36rem; padding-top: 3rem; }
h1, h2, .date, footer, ul.posts li {
  font-family: ui-monospace, Menlo, Consolas, "DejaVu Sans Mono", monospace;
}
h1 { font-weight: 800; letter-spacing: -.03em; }
h1::before { content: "$ "; color: var(--accent); }
a {
  color: #000;
  text-decoration-color: var(--accent);
  text-decoration-thickness: 2px;
  text-underline-offset: .22em;
}
a:hover { background: var(--accent); color: #fff; text-decoration: none; }
pre { border-left: 4px solid var(--accent); padding: 1rem; background: #f2f2f2; }
ul.posts li { padding: .7rem 0; border-bottom: 1px solid #ddd; }

Recipe: dark when the reader's system is dark

@media (prefers-color-scheme: dark) {
  html { background: #111; color: #ddd; }
  a { color: #8cf; }
  .date { color: #999; }
}

Recipe: a font from Google Fonts

@import has to be the first line of the file.

@import url("https://fonts.googleapis.com/css2?family=IBM+Plex+Serif:ital@0;1&display=swap");
body { font-family: "IBM Plex Serif", Georgia, serif; }

Recipe: a gallery in black and white, color on hover

.grid { grid-template-columns: repeat(3, 1fr); gap: 4px; }
.grid img { filter: grayscale(1); transition: filter .3s; }
.grid a:hover img { filter: none; }
.grid a { position: relative; }
.grid span {
  position: absolute; left: 0; right: 0; bottom: 0; margin: 0;
  padding: .4rem .6rem; background: rgb(0 0 0 / .6); color: #fff;
  font-size: .85rem;
}

2. A template.html

When CSS isn't enough, take over the page. In the terminal:

blog template

That copies the template every blog uses to ~/blog/template.html (pics template does the same for ~/pics). Change it however you like; from then on every page of your blog is made from it. Delete it to go back.

It's a Jinja2 template: HTML with {{ something }} where a value goes and {% for %}, {% if %} for the rest. One file makes every page, and kind tells you which one is being made.

What a blog template gets

Name What it is
kind "index" for the front page, "post" for a post
title the title for the browser tab
post on a post: post.title, post.date, post.content (already HTML), post.url, post.slug
newer, older the posts next to this one, like post, or nothing
posts every post, newest first, each like post
user your username
blog_url, feed_url, home_url the addresses of your blog, its feed and your page
style the link to your style.css, if you have one
default_css our CSS, to keep inside a <style> or to leave out

What a gallery template gets

Name What it is
kind "index" for the gallery, "photo" for a photo's own page
title the title for the browser tab
photo on a photo page: photo.image, photo.thumb, photo.width, photo.height, photo.caption, photo.alt, photo.date, photo.url, photo.slug
newer, older the photos next to this one, like photo, or nothing
photos every photo, newest first, each like photo
user your username
gallery_url, home_url the addresses of your gallery and your page
style, default_css the same as for the blog

Text is escaped for you: a caption with < in it shows a <, it doesn't open a tag. post.content is already HTML, so it goes in as it is.

When it breaks

A typo in the template doesn't take your blog down. The pages stay as they were, and the reason, with the line number, goes in ~/blog/template.err (or ~/pics/template.err). blog build and pics print it too. Fix it, and the note goes away by itself on the next build.

Recipe: a blog with a menu, excerpts and links between posts

<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{{ title }}</title>
<link rel="alternate" type="application/atom+xml" href="{{ feed_url }}">
<style>
{{ default_css }}
header { display: flex; gap: 1rem; margin-bottom: 3rem; font-size: 1rem; }
.post-nav { display: flex; justify-content: space-between; margin-top: 3rem; }
</style>{{ style }}
</head>
<body>
<header>
  <a href="{{ home_url }}">home</a>
  <a href="{{ blog_url }}">blog</a>
  <a href="{{ feed_url }}">feed</a>
</header>
{% if kind == "post" %}
<article>
  <h1>{{ post.title }}</h1>
  <p class="date">{{ post.date }}</p>
  {{ post.content }}
</article>
<nav class="post-nav">
  {% if newer %}<a href="{{ newer.url }}">← {{ newer.title }}</a>{% else %}<span></span>{% endif %}
  {% if older %}<a href="{{ older.url }}">{{ older.title }} →</a>{% endif %}
</nav>
{% else %}
<h1>~{{ user }}</h1>
{% for p in posts %}
<section>
  <h2><a href="{{ p.url }}">{{ p.title }}</a></h2>
  <p class="date">{{ p.date }}</p>
  <p>{{ p.content|striptags|truncate(200) }}</p>
</section>
{% else %}
<p>Nothing here yet.</p>
{% endfor %}
{% endif %}
</body>
</html>

Recipe: the gallery as a slideshow

One photo at a time, on black, with arrows (and the keyboard arrows) to move. Each photo still has its own page for people who link to it.

<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{{ title }}</title>
<style>
html, body { margin: 0; height: 100%; background: #000; color: #eee; font: 1rem/1.5 system-ui, sans-serif; }
.show { min-height: 100%; display: flex; flex-direction: column; align-items: center; justify-content: center; }
.show img { max-width: 100%; max-height: 85vh; }
.show p { margin: .75rem 1rem; text-align: center; }
a { color: #eee; }
</style>{{ style }}
</head>
<body>
{% if kind == "index" and photos %}
<div class="show">
  <img id="pic" src="{{ photos[0].image }}" alt="{{ photos[0].alt }}">
  <p id="cap">{{ photos[0].caption }}</p>
  <p><a href="#" id="prev">← prev</a> · <a href="#" id="next">next →</a></p>
</div>
<script>
const photos = {{ photos|tojson }};
const pic = document.getElementById("pic"), cap = document.getElementById("cap");
let i = 0;
function show(n) {
  i = (n + photos.length) % photos.length;
  pic.src = photos[i].image;
  pic.alt = photos[i].alt;
  cap.textContent = photos[i].caption;
}
document.getElementById("prev").onclick = e => { e.preventDefault(); show(i - 1); };
document.getElementById("next").onclick = e => { e.preventDefault(); show(i + 1); };
document.addEventListener("keydown", e => {
  if (e.key === "ArrowLeft") show(i - 1);
  if (e.key === "ArrowRight") show(i + 1);
});
</script>
{% elif kind == "photo" %}
<div class="show">
  <img src="{{ photo.image }}" alt="{{ photo.alt }}">
  <p>{{ photo.caption }}</p>
  <p><a href="./">all of them</a></p>
</div>
{% else %}
<p>No photos yet.</p>
{% endif %}
</body>
</html>

Scripts run, but each page lives in a sandbox of its own: cookies and localStorage don't work, and a script can't read other pages on runv.club.

3. Your own pages, by hand

The blog and the gallery are the easy way, not the only one. Anything you put in ~/public_html is served as it is, at https://runv.club/~you/: HTML, CSS, JavaScript, pictures, fonts. Make a whole site by hand, use a static site generator (hugo is installed), or mix: a front page you wrote yourself, with links to the blog and the gallery the server makes for you. See web for the details.

What you can't change