The Teletype Games blog as a BBS: the latest posts and the categories, a post read a screenful at a time. The editors log in on the same line and write, edit and delete posts.
Live at
teletypegames.org:2328, and in the browser on bbsweb, and through TTG Gate BBS
A Ruby app on the rubbs gem, like the Teletype BBS, Alexandria BBS and Feedia BBS. It has two faces on one port, and both are line by line, without windows:
The menus, the lists, the fields and the full-screen editor are the same on both. A call starts with rubbs' BBS::Terminal question: RETURN takes the guess, 1 picks the modern face, 2 the C64.
| Key | Action |
|---|---|
L |
Latest posts, newest first |
C |
Categories with their post counts, then that category's posts |
G |
Go to a post by its number (the web shows it beside the post) |
E |
Editor login |
Q |
Log off |
Lists are numbered: the number and RETURN opens a post, RETURN alone turns the page, Q or ← goes back. A post shows its title, category, date and author, the date of its last change, the summary, then the body (Markdown), paged a screenful at a time.
On the C64 every screen fills the whole 40×25: the menus and lists are cursor menus (CRSR moves the bar, RETURN takes it, the keys work too) with a reversed footer bar that tells the screen's keys. The front page lists as many of the newest posts as fit. A post keeps its footer on every page: RETURN, + or CRSR down turns the page, - or CRSR up goes back one, Q or ← leaves; for an editor E edit, P publish and D delete stand on the row over the footer, the questions come in the footer over the post, and after an edit or a publish the post is shown again.
After logging in, the menu has U unpublished posts, W write a post, P change your password and O log out, and every post has E edit, P publish or unpublish and D delete under it.
A new post is unpublished: only the editors see it (marked so in the lists), the readers and the web do not. Saving a new post keeps it unpublished; P under the post publishes it. The first publishing stamps published_at and rings Discord; taking a post back and publishing it again does not ring it twice. A new post asks for the title (up to 80 characters), the category, a one-line summary (up to 280) and the body, then shows the draft: T title, C category, S summary, B body, V view, W save, Q quit.
What an editor writes is kept as a draft until it is saved, so a dropped line loses nothing: after every field, every 15 seconds in the editor while the text changes (TTG_BLOG_BBS_AUTOSAVE_SECONDS), and when the caller hangs up. The draft is made at the first change, so opening a post and leaving makes none. Nobody but the composer sees a draft: not the lists, the counts, G or the web.
W saves: an edit is written into its post and the draft goes; a new post's draft becomes the post itself, unpublished.Q after a change asks: K keeps the draft for later, D discards it, RETURN (on a C64 back) goes back to the draft.E on a post with drafts, and W while there are drafts of new posts, start from a list: the post as it is now (or a new post), then the drafts with their time and editor, the last first. D discards one (on a modern terminal D and then its number). Every editor sees every draft; the menu shows the drafts of new posts beside W.W asks before it overwrites their save.The name, the passwords and the title are typed in a field of rubbs (BBS::Field), the summary and the body in its full-screen editor (BBS::Editor). Both start from the current text, so a title or a post is edited, not typed again: the cursor keys move anywhere, typing inserts at the cursor, DEL deletes before it, and the editor wraps the lines as they are typed. A field ends with RETURN; RUN/STOP or ← (Esc) leaves it.
| Key in the editor | Action |
|---|---|
| cursor keys | Move; up and down keep the column |
| HOME / END | Start / end of the screen row |
| PgUp / PgDn | A screenful up / down (modern terminal) |
RUN/STOP or ← (Esc), then S |
Save |
RUN/STOP or ← (Esc), then Q |
Quit, keep the text as it was |
| F1 (Ctrl-S) | Save at once (the UltimateTerm of the C64 Ultimate keeps F1 for its help) |
The summary is one paragraph: RETURN saves it. In the body RETURN starts a new line and an empty line a new paragraph (Markdown).
The posts are the blog of teletypegames.org/blog, in both languages of the site. Every post shows its number, the terminal it was typed on (ANSI or C64), and a C64 screen view (?view=c64): the lines a Commodore 64 caller sees, in the C64 palette. A card beside it dials the board in the browser (bbsweb, modern or as a C64), with the telnet command and G plus the post's number. The RSS of the site (/api/v1/rss/blog) carries the posts too.
The board serves them read-only over HTTP, in the same process as the telnet line (Web::Api, port 8080). The container joins the interstack network, and the API of the site reads it there as http://ttg-blog-bbs:8080 (TTG_BLOG_BBS_URL); the port is never published.
| Request | Answer |
|---|---|
GET /posts |
the published posts newest first, without their bodies |
GET /posts/SLUG |
one post: Markdown, HTML (kramdown, GFM) and the C64 screen |
GET /categories |
the categories with their post counts |
GET /health |
{"ok":true} |
A post gets its slug when it is saved: its date and its title in plain ASCII (2026-10-28-hello-world). It stays when the title changes, so links keep working. With TTG_BLOG_BBS_DISCORD_WEBHOOK in the stack's .env, a new post goes to Discord too, with its link.
The categories come from config/categories.yml (id, name, about): News, Devlog, Releases, Retro, Events. A change is a commit to that file; a push to master deploys it. A post names its category by id.
Editors and posts are in one SQLite file, data/ttgblog.sqlite3 in the stack (users, posts with slug, face, status (draft, unpublished, published), published_at, created_at, updated_at, and for a draft original_id and base_updated_at). Passwords are PBKDF2-SHA256 digests; three wrong logins end the tries of a call. The editors are managed on the server:
docker exec -it ttg-blog-bbs bin/user add NAME # asks the password twice
docker exec -it ttg-blog-bbs bin/user passwd NAME
docker exec -it ttg-blog-bbs bin/user remove NAME # their posts stay, without an author
docker exec ttg-blog-bbs bin/user list
A push to master deploys it from Woodpecker (RuboCop, compose check, ci-deploy ttg-blog-bbs); the container is ttg-blog-bbs, in /srv/stacks/ttg-blog-bbs, data/ stays between deploys. The entrypoint updates rubbs on start.