Skip to main content

Kaksik

Middleware library for creating applications for Gemini protocol on top of Deno runtime, written in TypeScript.

Heavily inspired by oak and denoscuri.

Feature roadmap

  • Serve gemtext (out of the box, see Gemtext usage)
  • Serve static files at configured URLs (via middleware, see serveStatic)
  • Serve programmable resources at configured URLs (via middleware, see handleRoutes)
  • Serve redirect responses at configured URLs (via middleware, see handleRedirects)
  • Document Gemtext usage
  • Serve gone responses at configured URLs (via middleware)
  • Improve Response class
  • – ‘Good enough’ point –
  • Propose yours by filing an issue

Usage

Prerequisites

  1. Install Deno executable
  2. Obtain SSL certificates. You can generate self-signed ones using openssl command:
    openssl req -x509 -newkey rsa:4096 -keyout key.pem -out cert.pem -days 365 -nodes

Your first app

Create minimal application in app.ts:

import { Application } from 'https://deno.land/x/kaksik/mod.ts'

const app = new Application({
  keyFile: '/path/to/key.pem',
  certFile: '/path/to/cert.pem',
})

app.use(ctx => {
  ctx.response.body = '# Hello World!'
})

await app.start()

Then run it:

deno run --allow-net --allow-read app.ts

Gemtext usage

Gemtext class represents a text/gemini media type that is native to Gemini protocol (see chapter 5 of spec). It’s a line-based text format, so essentially Gemtext is just an Array<Line> with helpers. All six line types are implemented:

  • LineText
  • LineLink
  • LinePreformattedToggle
  • LineHeading
  • LineQuote
  • LineListItem

Response.body setter accepts Gemtext for convenience.

const app = new Application({
  keyFile: '/path/to/key.pem',
  certFile: '/path/to/cert.pem',
})

app.use(ctx => {
  ctx.response.body = new Gemtext(
    new LineHeading('Gemtext demo', 1),
    new LineText(),
    new LineLink('gemini://s.tymo.name', 'stymo'),
    new LineText(),
    new LineText('There will be wrapped text. Elit eius magnam quae dolor ipsa eveniet aut? Facilis natus eum reiciendis reprehenderit odio. Sed et consectetur fuga quod illum ex minus. Iste quia dolor minus saepe in! Recusandae eligendi iusto blanditiis nostrum ipsum! Consequuntur tempora eaque dolore reiciendis sit. At exercitationem repudiandae doloremque quasi non. Nesciunt veritatis aliquid magnam unde pariatur'),
    new LineText(),
    new LineQuote('To be or not to be?'),
    new LinePreformattingToggle(),
    new LineText('There will be unwrapped text. Put some ASCII-art!'),
    new LinePreformattingToggle(),
  )
})

await app.start()

Appending new lines and other Gemtext instances:

const content = new Gemtext(
  new LineHeading('Second page', 1),
  new LineText(),
)

// do some calculation
const prevPageId = 1
const nextPageId = 3

// append more lines
content.append(
  new LineHeading('Navigation'),
  new LineText(),
)

// create anoter Gemtext instance
const nav = new Gemtext(
  new LineLink(`/pages/${prevPageId}`, 'Previous page'),
  new LineLink(`/pages/${nextPageId}`, 'Next page'),
  // Gemtext constructor accepts other Gemtext instances
  new Gemtext(
    new LineText('~~~~~~~~~'),
    new LineText('2020 A.D.'),
  ),
)

// appending mixed lines and Gemtext instances works too
content.append(
  new LineText('----'),
  nav,
  new LineText('----'),
)

Other examples

See examples folder.

Available middleware

serveStatic

Serves static files from a directory to specified URL

import { Application, serveStatic } from 'https://deno.land/x/kaksik/mod.ts'

const app = new Application({
  keyFile: '/path/to/key.pem',
  certFile: '/path/to/cert.pem',
})

app.use(serveStatic('./log/', '/gemlog/'))
app.use(serveStatic('./public/'))

await app.start()

Beware of ordering of serveStatic middleware usages: more generic URLs should occur later that more specific, e.g., /path/subpath/ must be before /path/.

handleRoutes

Runs specified async function when request path matches configured route.

import {
  Application,
  handleRoutes,
  Route,
} from 'https://deno.land/x/kaksik/mod.ts'

const app = new Application({
  keyFile: '/path/to/key.pem',
  certFile: '/path/to/cert.pem',
})

app.use(handleRoutes(
  new Route('/test', async (ctx) => {
    ctx.response.body = '# Test page'
  }),
  new Route<{id?: string}>('/param/:id', async (ctx) => {
    ctx.response.body = '# Parametrized page\r\n' +
      'id = ' + ctx.pathParams.id
  }),
  new Route('/', async (ctx) => {
    ctx.response.body = '# HOME page\r\n' +
      '=> /test Test page served by other route\r\n' +
      '=> /param/7 Parametrized page, where id=7\r\n' +
      '=> /404 No routes matched'
  }),
))

app.use(async (ctx) => {
  ctx.response.body = '# No routes matched\r\n' +
    'Running fallback middleware'
})

await app.start()

handleRedirects

Sends either temporary or permanent redirect response when path matches configuration.

import {
  Application,
  handleRedirects,
  handleRoutes,
  Redirect,
  Route,
} from 'https://deno.land/x/kaksik/mod.ts'

const app = new Application({
  keyFile: '/path/to/key.pem',
  certFile: '/path/to/cert.pem',
})

app.use(handleRedirects(
  new Redirect('/short', '/long-very-long-url', true),
  new Redirect('/home', 'https://tymo.name'),
))

app.use(handleRoutes(
  new Route('/long-very-long-url', async (ctx) => {
    ctx.response.body = '# Redirect target page'
  }),
))

await app.start()

Trivia

“Kaksik” means “twin” in Estonian.