racket-wiki

Current development version: 0.2.23.

A small self-hosted wiki with a Racket backend and an HTML5/CSS/JavaScript frontend.

Version 0.2.9 uses PostgreSQL as the wiki's complete content store. Users, sessions, pages, immutable page versions, attachment metadata and attachment bytes live in PostgreSQL. PostgreSQL full-text search is built into the page table and exposed by the wiki search UI.

The first-run flow is web based. An unconfigured wiki redirects immediately to /setup. Setup asks for the PostgreSQL connection, creates the schema, downloads the pinned frontend libraries and creates the first administrator. After setup the browser is sent to /login.

Components

The backend uses Racket web-server-lib for HTTP routing, db-lib for PostgreSQL and crypto-lib for password hashing. Racket's PostgreSQL support implements the wire protocol directly, so no PostgreSQL client library is required by racket-wiki.

Passwords use PBKDF2-HMAC-SHA256 through the libcrypto/OpenSSL provider. Session tokens are random client values whose SHA-256 hashes are stored in PostgreSQL; authenticated write requests also use a per-session CSRF token.

The editor uses EasyMDE 2.21.0. EasyMDE bundles CodeMirror for editing and Marked for Markdown rendering. The application uses easyMDE.markdown(...) for the editor preview, normal page display and historical page display. DOMPurify sanitizes rendered Markdown, highlight.js highlights fenced code blocks, and diff2html renders page-version differences.

Font Awesome is installed locally by the setup procedure and supplies the EasyMDE toolbar icons.

Roles

reader can read pages, page history and uploaded files.

editor inherits reader and can create, edit and archive pages and upload files.

admin inherits editor and can administer users.

There is no anonymous page access. Setup and login are server-rendered pages. The normal JavaScript wiki application is only served when the request has an authenticated session.

Data layout

PostgreSQL contains the relational/textual wiki state:

users
sessions
pages
page_versions
attachments
wiki_schema

pages.markdown contains the current Markdown source. Every save increments current_version and writes the matching immutable snapshot to page_versions in the same PostgreSQL transaction. Optimistic locking prevents an editor from silently overwriting a newer version.

The page search vector gives titles a higher weight than body Markdown and is indexed with a PostgreSQL GIN index. The simple text search configuration is used deliberately because a technical wiki commonly mixes Dutch, English, identifiers and package names.

The filesystem contains only installation/configuration assets during normal 0.2.9 operation:

wiki-data/
  database.rktd
  static/
    vendor/
      ... downloaded browser libraries ...

database.rktd contains the PostgreSQL connection settings, including the password when one is supplied. Protect the data directory accordingly. Attachment metadata and bytes are stored in PostgreSQL. An uploads/ directory may remain after migration from schema 1; it is retained only as a safety copy and is no longer read by 0.2.9.

Archiving a page marks it archived in PostgreSQL and keeps its history. Restore UI is a later addition.

First-run web setup

Start the server normally:

racket main.rkt --data ./wiki-data --port 8080

Open http://127.0.0.1:8080/. An incomplete installation is immediately redirected to /setup.

On a fresh installation the setup page asks for:

PostgreSQL server      localhost
PostgreSQL port        5432
Database               racket_wiki
User                   <database role>
Password               <optional according to PostgreSQL authentication>
TLS/SSL                no / optional / required

Administrator username
Display name
Administrator password

The PostgreSQL database itself must already exist and the supplied role must be allowed to connect and create the racket-wiki tables and indexes in the selected database. Setup tests the connection before storing the settings.

Setup creates the schema, including users, sessions, pages, page versions, attachments and their binary content, the schema migration history and the page full-text-search GIN index. It then downloads the pinned frontend libraries and creates the first administrator. After all setup conditions are satisfied, the browser is redirected to /login; setup deliberately does not log the administrator in automatically.

Setup is considered complete only when the PostgreSQL schema is reachable, at least one enabled administrator exists and all required browser vendor files are installed. /setup therefore also acts as a repair path when frontend assets are missing.

Frontend libraries

The web setup downloads these pinned browser builds:

font-awesome/css/font-awesome.min.css
font-awesome/fonts/fontawesome-webfont.woff2
easymde.min.js
easymde.min.css
purify.min.js
highlight.min.js
highlight-scheme.min.js
highlight-github.min.css
diff2html-ui-base.min.js
diff2html.min.css

EasyMDE already bundles CodeMirror and Marked, so racket-wiki does not download a separate Markdown renderer or CodeMirror build.

The setup downloader validates HTTPS certificates and hostnames. The running wiki serves these files locally from the configured data directory, so no CDN is required after setup.

setup-vendor.rkt remains available as a manual/recovery command. It now writes to the same data directory as the web setup:

racket setup-vendor.rkt --data ./wiki-data

Normal first-run installation does not require this command.

Development bootstrap

The bootstrap URL used for this development snapshot is:

https://rktsndbx.dijkewijk.nl/bootstrap-racket?next=opper-vrolijk-fresh-pocket

When development is performed in an isolated sandbox without a Racket installation, use that Racket bootstrap URL. The bootstrap page lists a numbered set of text/plain files. Each file contains the base64 representation of one binary part of racket.zip.

Use network access only through the environment web fetch/download facility. Do not use curl, wget, Python networking or shell networking for the bootstrap.

Use these fixed paths:

work directory : /tmp/racket_bootstrap
zip file       : /tmp/racket.zip
Racket runtime : /mnt/data/racket
Racket binary  : /mnt/data/racket/bin/racket

The bootstrap procedure is:

1. Open the supplied bootstrap URL with the web fetcher.
2. Read the number of parts and the exact part URLs from that page.
3. Download every part to /tmp/racket_bootstrap/part-NNNNNN.txt.
4. Decode every text file separately with base64 -d to part-NNNNNN.
5. Concatenate the binary parts in numeric order to /tmp/racket.zip.
6. Unpack the zip at filesystem root: unzip /tmp/racket.zip -d /
7. Use /mnt/data/racket/bin/racket and /mnt/data/racket/bin/raco.

The archive intentionally contains the path mnt/data/racket, so unpacking at / places the runtime at /mnt/data/racket. Additional Racket packages should be obtained through the package/fetch facilities exposed by the bootstrap environment rather than by adding unrelated shell-network tooling.

This bootstrap is development tooling only. A normal installation can use an existing Racket installation and the dependencies declared in info.rkt.

Starting from DrRacket

main.rkt can be run directly from DrRacket. Racket treats its main submodule in the same way as when the file is run with the racket executable, so pressing Run starts the wiki with the normal defaults when no command-line arguments are configured.

For interactive development, main.rkt also provides start. Require the module from an interactions file or another development module and supply keyword arguments directly:

(require "main.rkt")

(start #:data-dir "wiki-data"
       #:port 8080
       #:listen-ip "127.0.0.1"
       #:site-title "Racket Wiki"
       #:secure-cookie? #f)

Use #:listen-ip "*" or #:listen-ip #f to listen on all interfaces. #:session-seconds can be supplied when the default twelve-hour session lifetime is not desired.

The same web setup procedure applies when the server is started from DrRacket.

Starting

Normally only the server start command is required:

racket main.rkt --data ./wiki-data --port 8080

Open http://127.0.0.1:8080/. A fresh data directory redirects to /setup; after setup the browser goes to /login.

To listen on all interfaces:

racket main.rkt --data ./wiki-data --listen '*' --port 8080

When the site is served through HTTPS, add --secure-cookie so the session cookie gets the Secure attribute.

The --create-admin command-line option remains available for recovery or scripted administration, but it is no longer part of the normal first-run procedure.

Editing

EasyMDE supplies the Markdown editor, Markdown-aware styling while editing, line numbers, keyboard shortcuts, preview, side-by-side mode and fullscreen mode. On a normal desktop-sized window racket-wiki opens the editor in side-by-side mode by default. EasyMDE is configured with sideBySideFullscreen: false, so split editing remains inside the normal wiki page rather than taking over the complete browser window.

The toolbar uses the normal EasyMDE-style Font Awesome icons, installed locally during setup. It includes formatting, headings, lists, checklists, code blocks, tables, links, image upload, general file upload, undo/redo, preview, split view and fullscreen.

The sidebar gives the current page table of contents priority. The global page list is secondary and collapsible. The search box performs real PostgreSQL full-text search rather than filtering page titles in the browser.

A fenced block such as:

```racket
(define (hello name)
  (displayln name))
```

is highlighted in the preview through highlight.js. highlight.js has a Scheme grammar rather than a dedicated Racket grammar; racket-wiki registers racket as an alias for that grammar.

For a normal new page the editor asks only for the page title. The Racket backend derives the slug when the page is first saved. Whitespace and punctuation become hyphens, the slug is lower-cased, and Unicode compatibility decomposition is used so common accented Latin titles produce readable addresses. A later title change does not rename the slug, which keeps existing links stable.

A link can explicitly name a page slug. In rendered Markdown, simple relative links such as [Install](install) and root links such as [Install](/install) are treated as wiki-page links; #/install also remains supported. External URLs and upload URLs are left alone.

When an editor or administrator follows a wiki link whose target does not exist, racket-wiki opens a new empty editor at that requested slug instead of exposing an API/Racket error. The title is still entered by the editor. A reader gets a normal Page not found view. An unauthenticated request never reaches the wiki page application: it is redirected to /login first.

The sidebar search field calls the authenticated /api/search endpoint. PostgreSQL builds a tsquery with websearch_to_tsquery('simple', ...), ranks matches with ts_rank, and returns a short ts_headline excerpt. Search results are relevance sorted, with title terms weighted above body terms. The search vector is updated atomically whenever a page is created or saved.

Uploads

EasyMDE handles image upload through its normal image-upload hook. PNG, JPEG, GIF and WebP can therefore be inserted by its image button, drag/drop or clipboard paste. The upload itself still goes to the Racket API and the returned page-local URL is inserted into the Markdown.

The additional File toolbar button is for arbitrary attachments such as PDF, ZIP or source files. Dropping one or more non-image files on the editor is handled by racket-wiki and inserts Markdown links. A mixed drop containing images and other files uses this general file path so every dropped file is retained.

For a new page, save it once before uploading attachments. This is intentional: uploads always belong to an existing page directory.

Uploads are limited to 50 MiB per request in this first setup.

Rendering

There is no separate application Markdown renderer. EasyMDE's markdown(...) method is the single rendering path for the live editor preview, normal reader view and rendering a selected historical version.

That method uses EasyMDE's bundled Marked renderer and the same rendering configuration, including syntax highlighting and DOMPurify sanitization. This avoids subtle differences between what an editor previews and what a reader sees after saving.

Versioning

Every create or save writes a new immutable row to page_versions in the same PostgreSQL transaction as the current page update. Updates use optimistic locking: the browser sends the version it originally opened. If somebody else saved the page in the meantime, the API returns HTTP 409 instead of silently overwriting their version.

The History view can render an old version or compare two consecutive versions. The browser creates a unified textual diff and diff2html renders it side by side.

Security notes

Markdown rendering is sanitized with DOMPurify before it is inserted into the page. This also means raw HTML embedded in Markdown is only retained to the extent that DOMPurify permits it.

Session cookies are HttpOnly and SameSite=Strict. Authenticated write operations also require a per-session CSRF token.

Untrusted uploads are not blindly rendered inline. Only common raster image formats are served inline. Other files use Content-Disposition: attachment; SVG is therefore not treated as an inline image.

For internet-facing deployment, put the server behind a TLS terminating reverse proxy, use --secure-cookie, and add normal operational controls such as backups, access logging and upload limits appropriate to the installation.

Next useful steps

The backend and storage API remain independent of EasyMDE. Useful next additions include internal wiki-link syntax, page namespaces/navigation, restore-from-archive, site settings, per-page ACLs and a richer admin console.

Version 0.2.7

Version 0.2.7 stores all wiki content in PostgreSQL: users, sessions, pages, immutable page versions and binary attachments. Page writes and version snapshots are transactional. PostgreSQL full-text search with a weighted tsvector and GIN index is exposed through a real wiki search field. Database schema changes are handled by ordered migrations recorded in wiki_schema.

The same release also makes the page-local contents list the primary sidebar navigation, adds a DokuWiki-style breadcrumb, adopts restrained Racket/Scribble-inspired document typography, installs Font Awesome for the EasyMDE toolbar, and makes source-editor heading sizes match the rendered heading scale.

Page contents and editor appearance

The sidebar shows a page-local table of contents before the global page list. The table of contents is derived from Markdown headings. In the editor it updates while the Markdown source changes; selecting an entry moves the editor cursor to that heading. In page view, selecting an entry scrolls to the rendered heading.

The wiki stylesheet is intentionally document-oriented and takes visual cues from Racket's Scribble/manual help pages without depending on Scribble's internal CSS classes. EasyMDE preview and the normal rendered page share the same heading and body sizes. CodeMirror heading tokens use the same heading scale, so a Markdown heading is not larger in the source editor than in the rendered preview.

Font Awesome 4.7 is installed locally by the web setup together with EasyMDE and the other browser dependencies. No CDN access is needed after setup.

0.2.6 setup and missing-page behaviour

When PostgreSQL explicitly requests cleartext password authentication, the setup page now explains that scram-sha-256 is preferred and reminds the administrator that PostgreSQL uses the first matching pg_hba.conf rule. A failed setup POST preserves server, port, database, user, SSL mode, administrator username and display name. Password fields are deliberately cleared.

For editors and administrators, a missing wiki page is now shown as an empty page target with one Edit action. Editing that target creates the page on first save. The separate global New page action is hidden while such a missing target is open, avoiding simultaneous create/edit choices for the same page.

Database schema migrations

The PostgreSQL schema has its own version history in table wiki_schema. Each applied schema version is recorded with applied_at. On startup racket-wiki detects the existing schema and applies every missing migration in order before serving the normal application. A database whose schema is newer than the running racket-wiki version is refused rather than silently downgraded.

Schema 1 is the PostgreSQL layout introduced by 0.2.5/0.2.6: pages and page versions are in PostgreSQL while attachment bytes are on disk. Schema 2, introduced by 0.2.7, adds attachments.mime_type and attachments.content BYTEA. During migration 1 -> 2, existing files below wiki-data/uploads/<slug>/ are copied into the matching attachment rows. The migration runs transactionally and is only recorded as version 2 when every attachment can be migrated. Legacy upload files are deliberately left on disk after a successful migration as a safety copy; 0.2.7 no longer reads them at runtime. They may be removed after the migrated attachments have been verified.

The application libraries downloaded by setup and the local database connection settings remain in the data directory; they are installation/configuration data rather than wiki content.

Version 0.2.9

Version 0.2.9 fixes the Racket regexp used by the server-side todo(...) extractor. The extractor processes one Markdown source line at a time, so the regexp now excludes only parentheses; it no longer contains the invalid alphabetic regexp escapes for carriage return and newline.

Version 0.2.8

Version 0.2.8 adds wiki-wide todo markers, offline detection and translatable frontend labels.

A todo marker is written directly in Markdown as todo(text). Markers inside fenced code blocks are ignored. On every page save the current markers are indexed in PostgreSQL. The Todo view gathers the unresolved markers from all active pages and links each item back to its source page. Standard Markdown task lists such as - [ ] item remain available for ordinary page-local checklists.

The browser sends a lightweight request to /api/ping every 15 seconds. A failed or timed-out request shows an Offline indicator. When the site becomes reachable again the indicator briefly reports Online and then disappears.

The old New page button has been removed. New pages are created by following or entering a wiki address that does not exist yet and choosing Edit.

UI text is routed through private/translate.rkt. The server can be started with a language, for example:

racket main.rkt --data ./wiki-data --language nl

Built-in English and Dutch translations are supplied. Administrators also get a Translations action. It opens the single special page wiki-translations. When that page does not exist yet, the editor is prefilled with every built-in translation key. Each line can contain several languages, for example save = nl:Opslaan, en:Save, de:Speichern. Values containing commas or quotes can be quoted. The active language selects the matching value and falls back to en when the requested language is not present. Saving the page is enough; the overrides are loaded from PostgreSQL on the next frontend load. Setup and login retain their built-in translations and therefore do not depend on that special page.

Database schema 3 adds todo_items. Migration 2 -> 3 creates the table and indexes todo markers from all existing, non-archived pages. The migration is recorded in wiki_schema just like earlier schema changes.

The web setup also contains a UI-language selector. The selected language is stored in wiki-data/language.rktd; the command-line --language value is the default when that file does not yet exist.

Version 0.2.12

Version 0.2.12 makes the first/start page title the visible wiki name and breadcrumb root. The account line is moved to the upper-right navigation area as compact links (name · role · sign out · admin), and page actions such as Edit, History and Delete use the same small link style as the breadcrumb. The breadcrumb/account/action chrome stays sticky while reading long pages. The sidebar keeps * Todo list directly below the wiki name; Users and Translations are reached through the Admin page.

Wiki image options now support placement in addition to width. Examples are ![Schema](schema.png){width=50% center}, ![Schema](schema.png){width=320px right}, ![Schema](schema.png){width=320px right float} and ![Schema](schema.png){width=320px float=left}. Without float, left/center/right controls block alignment. With float enabled, surrounding text can flow around left- or right-aligned images. Width remains limited to pixels and percentages and rendered images keep max-width: 100% and automatic height.

The special translations page is now one page named wiki-translations. It is prefilled from the built-in language tables and accepts several languages on one line, for example save = nl:Opslaan, en:Save, de:Speichern.

Version 0.2.13

Version 0.2.13 centers the wiki name in the sidebar and makes it a link to the first/start page. The sticky top chrome now starts without top padding (padding-top: 0).

0.2.15

The page table of contents now includes a (context) link. It opens a graph containing the current page and all pages that link directly to it or are linked directly from it. The current page is highlighted. Heading anchors also reserve space for the sticky top navigation so a selected heading remains visible.

Recent pages and bookmarks

The sidebar provides dynamic Recent and Bookmarks views. Recent shows current pages ordered by their last edit time. Bookmarks are stored per user in PostgreSQL and can be grouped into user-defined sections. The database migration to schema version 4 creates the bookmarks table automatically.

0.2.19

Todo markers are case-insensitive. todo(...), Todo(...) and TODO(...) are all indexed and rendered as todo items. Database migration 4 -> 5 rebuilds the todo index for existing pages so already-saved mixed-case markers become visible without re-saving the pages.

Editors can start section editing from the small edit link beside a heading in the page contents. The whole page is still saved as one version; section edit only opens the editor at the selected heading.

Pages whose slug starts with template- are available in the editor Template dropdown. Selecting a template copies that page's Markdown into the current editor. Existing non-empty content is only replaced after confirmation.

0.2.20

Inline todo(...) markers render as a yellow Todo: text link. Clicking the marker opens the Todo view at the matching item and highlights it. Todo markers inside fenced code blocks remain untouched.

0.2.23

Breadcrumbs now keep a per-browser-tab history of visited wiki pages. The start page remains the fixed root. Earlier pages in the trail are clickable; selecting one truncates the trail at that page, after which navigation continues from there. Returning to the wiki name/Home clears the trail and opens the start page. Special views such as Todo, Recent, Bookmarks, Graph and Admin do not become entries in the page-history breadcrumb. The trail is stored in browser session storage and is limited to the most recent eight non-home wiki pages.

0.2.23

CamelCase and normalized unlinked page mentions are rendered as automatic links to existing wiki pages. Section edit temporarily highlights the target source line. The EasyMDE toolbar contains a Raw Markdown toggle; the preference is kept in the browser.

0.2.24

Attachment references are tracked in database schema 6. Admin contains an Orphaned uploads page showing uploads no current page references, together with their last historical page/version when available.

S
Description
A simple wiki using markdown syntax
Readme MIT 1.1 MiB
Languages
JavaScript 52.7%
Racket 35.3%
CSS 7.2%
HTML 4.7%