Back to APIs list

Github API examples & templates

Use these vals as a playground to view and fork Github API examples and templates on Val Town. Run any example below or find templates that can be used as a pre-built solution.

Static Chess

chess

Check it out here: https://chess.maxmcd.com

Plain, brutalist, no bloat chess. Every page is only html and css. Every chess move is made by clicking a link. Send a link to your friend and they'll send you one back to make your move. No silly animations or slick interactivity to trip up your gameplay. When Google indexes this site will we successfully compute all possible chess moves?

Functionality is quite limited, and things might be broken. Please let me know if you find bugs!

Inspired by this HN discussion about sites that have all possible game states of tic-tac-toe.

I plan on extending this to support real gameplay. I think it could be a nice simple interface for long form games with friends. Might also be fun to add a static AI to play against. Feel free to PR any changes if you'd like to see something added.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
/** @jsxImportSource https://esm.sh/react */
import { modifyFetchHandler as codeOnValTown } from "https://esm.town/v/andreterron/codeOnValTown?v=45";
import { modifyResponse } from "https://esm.town/v/andreterron/codeOnVT_modifyResponse?v=9";
import { Chess, Move, Square } from "npm:chess.js";
import minify from "npm:css-simple-minifier";
import { renderToString } from "npm:react-dom/server";
class StaticChess {
size = 8;
rows = Array.from({ length: this.size }, (_, i) => i);
squares = Array.from({ length: this.size }, (_, i) => i);
constructor() {}
async fetch(req: Request): Promise<Response> {
const gameInfo = parseURL(req.url);
if (gameInfo === undefined) {
return new Response("Not Found", { status: 404, headers: { "cache-control": "max-age=86400, public" } });
}
const game = new Game(gameInfo.game, gameInfo.selected);
return new Response(
renderToString(
<html>
<head>
<title>Static Chess</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="icon" href="https://fav.farm/♟️" />
<style>{minify(CSS)}</style>
</head>
<body>
<div id="code-on-vt-host">
<link
rel="stylesheet"
href="https://cdnjs.cloudflare.com/ajax/libs/github-fork-ribbon-css/0.2.3/gh-fork-ribbon.min.css"
/>
<a
href="https://www.val.town/v/maxm/staticChess"
rel="source"
target="_blank"
className="github-fork-ribbon"
data-ribbon="Code on Val Town"
title="Code on Val Town"
>
Code on Val Town
</a>
</div>
<h1>Static Chess</h1>
<div>
<a href="https://www.val.town/v/maxm/staticChess">info</a> - <a href="/">reset</a>
</div>
<div className="board">
{this.rows.map(row => (
<div key={row} className="row">{this.squares.map(square => game.squareContent(row, square))}</div>
))}
</div>
<div className="info">
{game.selected
? "Click a highted square to move the selected piece, or select a different piece."
: `It is ${{ w: "white", b: "black" }[game.game.turn()]}'s turn. Click a piece to make a move.`}
</div>
</body>
</html>,
),
{ headers: { "content-type": "text/html", "cache-control": "max-age=86400, public" } },
);
}
}
class Game {
game: Chess;
selected?: string;
selectable: string[];
board;
nextMoves: { [key: string]: Move };
fen: string;
constructor(game: Chess, selected?: string) {
this.game = game;
this.selected = selected;
this.board = game.board();
this.fen = game.fen().replaceAll(" ", "_");
this.nextMoves = {};
this.selectable = game.moves({ verbose: true }).map((m) => m.from.toString());
if (this.selected) {
var moves = game.moves({
square: selected as Square,
verbose: true,
});
for (const move of moves) {
this.nextMoves[move.to] = move;
}
}
}
squareContent(row: number, square: number) {
console.log(row);
const pos = indexToPos(row, square);
const color = this.board[row][square]?.color;

Static Chess

chess

Check it out here: https://chess.maxmcd.com

Plain, brutalist, no bloat chess. Every page is only html and css. Every chess move is made by clicking a link. Send a link to your friend and they'll send you one back to make your move. No silly animations or slick interactivity to trip up your gameplay. When Google indexes this site will we successfully compute all possible chess moves?

Functionality is quite limited, and things might be broken. Please let me know if you find bugs!

Inspired by this HN discussion about sites that have all possible game states of tic-tac-toe.

I plan on extending this to support real gameplay. I think it could be a nice simple interface for long form games with friends. Might also be fun to add a static AI to play against. Feel free to PR any changes if you'd like to see something added.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
/** @jsxImportSource https://esm.sh/react */
import { modifyFetchHandler as codeOnValTown } from "https://esm.town/v/andreterron/codeOnValTown?v=45";
import { modifyResponse } from "https://esm.town/v/andreterron/codeOnVT_modifyResponse?v=9";
import { Chess, Move, Square } from "npm:chess.js";
import minify from "npm:css-simple-minifier";
import { renderToString } from "npm:react-dom/server";
class StaticChess {
size = 8;
rows = Array.from({ length: this.size }, (_, i) => i);
squares = Array.from({ length: this.size }, (_, i) => i);
constructor() {}
async fetch(req: Request): Promise<Response> {
const gameInfo = parseURL(req.url);
if (gameInfo === undefined) {
return new Response("Not Found", { status: 404, headers: { "cache-control": "max-age=86400, public" } });
}
const game = new Game(gameInfo.game, gameInfo.selected);
return new Response(
renderToString(
<html>
<head>
<title>Static Chess</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="icon" href="https://fav.farm/♟️" />
<style>{minify(CSS)}</style>
</head>
<body>
<div id="code-on-vt-host">
<link
rel="stylesheet"
href="https://cdnjs.cloudflare.com/ajax/libs/github-fork-ribbon-css/0.2.3/gh-fork-ribbon.min.css"
/>
<a
href="https://www.val.town/v/maxm/staticChess"
rel="source"
target="_blank"
className="github-fork-ribbon"
data-ribbon="Code on Val Town"
title="Code on Val Town"
>
Code on Val Town
</a>
</div>
<h1>Static Chess</h1>
<div>
<a href="https://www.val.town/v/maxm/staticChess">info</a> - <a href="/">reset</a>
</div>
<div className="board">
{this.rows.map(row => (
<div key={row} className="row">{this.squares.map(square => game.squareContent(row, square))}</div>
))}
</div>
<div className="info">
{game.selected
? "Click a highted square to move the selected piece, or select a different piece."
: `It is ${{ w: "white", b: "black" }[game.game.turn()]}'s turn. Click a piece to make a move.`}
</div>
</body>
</html>,
),
{ headers: { "content-type": "text/html", "cache-control": "max-age=86400, public" } },
);
}
}
class Game {
game: Chess;
selected?: string;
selectable: string[];
board;
nextMoves: { [key: string]: Move };
fen: string;
constructor(game: Chess, selected?: string) {
this.game = game;
this.selected = selected;
this.board = game.board();
this.fen = game.fen().replaceAll(" ", "_");
this.nextMoves = {};
this.selectable = game.moves({ verbose: true }).map((m) => m.from.toString());
if (this.selected) {
var moves = game.moves({
square: selected as Square,
verbose: true,
});
for (const move of moves) {
this.nextMoves[move.to] = move;
}
}
}
squareContent(row: number, square: number) {
const pos = indexToPos(row, square);
const color = this.board[row][square]?.color;
let className = "square";
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
/** @jsxImportSource https://esm.sh/react */
import { renderToString } from "npm:react-dom/server";
export default async function(req: Request) {
return new Response(
renderToString(
<html>
<head>
<meta charSet="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Tahir's TIL</title>
</head>
<body
style={{
maxWidth: "70ch",
padding: "3em 1em",
margin: "auto",
lineHeight: "1.75",
fontSize: "1.25em",
fontFamily: "sans-serif",
}}
>
<h1>Tahir's TIL</h1>
<h3>Design, Web and Data Things</h3>
<div style={{ display: "flex", flexDirection: "column", gap: "15px" }}>
<a href="https://github.com/Little-Languages/quiver" style={itemStyle}>
declarative arrows for the web. ⤵
</a>
<a href="https://github.com/steveruizok/perfect-arrows" style={itemStyle}>
Draw perfect arrows between points and shapes. By the the creator of tldraw.
</a>
<a href="https://news.ycombinator.com/item?id=32972004" style={itemStyle}>
58 bytes of CSS to look great nearly everywhere
</a>
<a href="https://maxbo.me/a-html-file-is-all-you-need" style={itemStyle}>
A HTML file is all you need. Lot's of useful client side JavaScript examples including charts, Python and
SQLite.
</a>
<a href="https://developer.mozilla.org/en-US/blog/color-palettes-css-color-mix/" style={itemStyle}>
color-mix() allows you to mix colours in CSS using 3 colour spaces: srgb, oklab or hsl
</a>
<a href="https://www.val.town/v/tfayyaz/honoJsDialogShowModal" style={itemStyle}>
val.town demo - Hono JS Dialog Show Modal
</a>
<a href="https://www.val.town/v/tfayyaz/honoJsInClientTemplate" style={itemStyle}>
Simple Hono JS val.town Template with client side JavaScript using html Helper
</a>
<a href="https://ourworldindata.org/electric-car-sales" style={itemStyle}>
Over 90% of new cars are electric in Norway + Our World in Data make beautiful charts
</a>
<a
href="https://webkit.org/blog/15131/speedometer-3-0-the-best-way-yet-to-measure-browser-performance/"
style={itemStyle}
>
Speedometer 3.0 - Browser Performance benchmarks by Apple, Google, Mozilla and Microsoft. Includes 4
charting libraries
</a>
<a href="https://www.typewolf.com/site-of-the-day/" style={itemStyle}>
Typewolf Site of the Day. Inspiration for font pairings
</a>
<a href="https://deadsimplesites.com/" style={itemStyle}>DSS Dead Simple Sites.</a>
<a href="https://www.csscade.com/" style={itemStyle}>CSS is awesome - CSSCade</a>
<a href="https://djr.com/job-clarendon" style={itemStyle}>
Job Clarendon is a stunning typeface that pays homage to job printing
</a>
<a href="https://courses.nan.fyi/login" style={itemStyle}>Interactive SVG Animations Course</a>
<a href="https://htmx.org/examples/active-search/" style={itemStyle}>HTMX Active Search</a>
<a href="https://www.bram.us/2024/02/18/custom-highlight-api-for-syntax-highlighting/" style={itemStyle}>
Search and highlight text
</a>
<a href="https://www.bram.us/2024/02/18/custom-highlight-api-for-syntax-highlighting/" style={itemStyle}>
Custom highlight API - Display style and script blocks
</a>
<a href="https://docs.val.town/quickstarts/first-website/" style={itemStyle}>
Building websites with Val.town is fast
</a>
<p>Notes: Asking ChatGPT to explain code is a great and fun way to learn</p>
</div>
</body>
</html>,
),
{
headers: {
"Content-Type": "text/html",
},
},
);
}
const itemStyle = {
padding: "10px",
color: "#222",
// backgroundColor: "#ff748d",
backgroundColor: "rgba(0, 0, 0, 0.02)",
background: "colorMix(in srgb, blue, white 80%)",
borderTop: "2px solid rgba(0, 0, 0, 0.20)",
borderRadius: "2px",
textDecoration: "none",
// boxShadow: "0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05)",
};

A Go http handler running in Val Town:

Using this go source file, this go library, this Deno library, and this script. Image rendering is happening here. Mandelbrot rendering code taken from here.

Blog post, libraries with readmes and more info coming!

package main

import (
	"fmt"
	"net/http"

	gotown "github.com/maxmcd/go-town"
)

func main() {
	img := renderImage()
	gotown.ListenAndServe(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if r.URL.Path == "/mandelbrot.png" {
			w.Header().Set("Content-Type", "image/png")
			w.Write(img)
			return
		}
		w.Header().Set("Content-Type", "text/html")
		fmt.Fprintf(w, `
			<head><link rel="icon" href="/mandelbrot.png"></head>
			<style>body {font-family: sans-serif}</style>
			Go-Rendered mandelbrot image served from a Go HTTP handler <a href="https://www.val.town/v/maxm/tinygoHttpExample">on Val Town</a>
			<br /><img src='/mandelbrot.png' />
		`)
	}))
}
1
2
3
4
5
6
7
8
9
import handler, { init } from "https://deno.land/x/gotown@v0.0.7/val-town-tinygo-http-example/mod.ts";
const resp = await fetch("https://deno.land/x/gotown@v0.0.7/val-town-tinygo-http-example/main.wasm");
init(new Uint8Array(await resp.arrayBuffer()));
export default async function(req: Request): Promise<Response> {
const resp = await handler(req);
resp.headers.set("Cache-Control", "max-age=3600");
return resp;
}

OpenAI - Docs ↗

Use OpenAI's chat completion API with std/openai. This integration enables access to OpenAI's language models without needing to acquire API keys.

Streaming is not yet supported. Upvote the HTTP response streaming feature request if you need it!

Usage

Create valimport { OpenAI } from "https://esm.town/v/std/openai"; const openai = new OpenAI(); const completion = await openai.chat.completions.create({ messages: [ { role: "user", content: "Say hello in a creative way" }, ], model: "gpt-4", max_tokens: 30, }); console.log(completion.choices[0].message.content);

Limits

While our wrapper simplifies the integration of OpenAI, there are a few limitations to keep in mind:

  • Usage Quota: We limit each user to 10 requests per minute.
  • Features: Chat completions is the only endpoint available.

If these limits are too low, let us know! You can also get around the limitation by using your own keys:

  1. Create your own API key on OpenAI's website
  2. Create an environment variable named OPENAI_API_KEY
  3. Use the OpenAI client from npm:openai:
Create valimport { OpenAI } from "npm:openai"; const openai = new OpenAI();

📝 Edit docs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
import { type ClientOptions, OpenAI as RawOpenAI } from "npm:openai";
/**
* API Client for interfacing with the OpenAI API. Uses Val Town credentials.
*/
export class OpenAI {
private rawOpenAIClient: RawOpenAI;
/**
* API Client for interfacing with the OpenAI API. Uses Val Town credentials.
*
* @param {number} [opts.timeout=10 minutes] - The maximum amount of time (in milliseconds) the client will wait for a response before timing out.
* @param {number} [opts.httpAgent] - An HTTP agent used to manage HTTP(s) connections.
* @param {Core.Fetch} [opts.fetch] - Specify a custom `fetch` function implementation.
* @param {number} [opts.maxRetries=2] - The maximum number of times the client will retry a request.
* @param {Core.Headers} opts.defaultHeaders - Default headers to include with every request to the API.
* @param {Core.DefaultQuery} opts.defaultQuery - Default query parameters to include with every request to the API.
* @param {boolean} [opts.dangerouslyAllowBrowser=false] - By default, client-side use of this library is not allowed, as it risks exposing your secret API credentials to attackers.
*/
constructor(options: Omit<ClientOptions, "baseURL" | "apiKey" | "organization"> = {}) {
this.rawOpenAIClient = new RawOpenAI({
...options,
baseURL: "https://std-openaiproxy.web.val.run/v1",
apiKey: Deno.env.get("valtown"),
organization: null,
});
}
get chat() {
return this.rawOpenAIClient.chat;
}
readonly beta = {
get chat(): RawOpenAI["beta"]["chat"] {
return this.rawOpenAIClient.beta.chat;
},
};
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
/** @jsxImportSource https://esm.sh/react */
import { renderToString } from "npm:react-dom/server";
export default async function(req: Request) {
return new Response(
renderToString(
<html>
<head>
<meta charSet="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Joe Schmo</title>
</head>
<body
style={{ padding: "30px", width: "300px", margin: "0 auto", fontFamily: "sans-serif", textAlign: "center" }}
>
<h1>Joe Schmo</h1>
<p>Just an average Joe</p>
<div style={{ display: "flex", flexDirection: "column", gap: "15px" }}>
<a href="https://www.instagram.com/joeschmo" style={itemStyle}>Instagram</a>
<a href="https://github.com/joeschmo" style={itemStyle}>Github</a>
<a href="https://www.linkedin.com/in/joeschmo" style={itemStyle}>LinkedIn</a>
<a href="https://twitter.com/joeschmo" style={itemStyle}>Twitter</a>
<a href="https://www.youtube.com/joeschmo" style={itemStyle}>YouTube</a>
</div>
</body>
</html>,
),
{
headers: {
"Content-Type": "text/html",
},
},
);
}
const itemStyle = {
padding: "10px",
color: "white",
backgroundColor: "#ff748d",
borderRadius: "20px",
textDecoration: "none",
boxShadow: "0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05)",
};

Get A Website for Your Gists

Example: https://yieldray-gists.web.val.run
Usage: fork this val and replace with your github usename

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
import ServeRouter from "https://esm.sh/serve-router@latest";
const app = ServeRouter();
const USERNAME = "yieldray"; // <--- Replace with your Github username
const gistList: Array<Gist> = await fetch(`https://api.github.com/users/${USERNAME}/gists`).then((res) => res.json());
app.get("/", (req) => {
return Response.json(
gistList.map((gist) => ({
id: gist.id,
description: gist.description,
url: new URL(`/${gist.id}`, req.url).toString(),
html_url: gist.html_url,
})),
);
});
app.get<{
id: string;
}>("/:id", (req, { params: { id } }) => {
const gist = gistList.find((gist) => gist.id === id) ?? gistList.find((gist) => gist.description === id);
if (!gist) return new Response("404 Gist Not Found", { status: 404 });
return Response.json(
Object.fromEntries(
Object.entries(gist.files).map(([fileName, file]) => [
fileName,
{
...file,
url: new URL(`/${gist.id}/${fileName}`, req.url).toString(),
},
]),
),
);
});
app.get<{
id: string;
file: string;
}>("/:id/:file", async (_req, { params: { id, file } }) => {
const gist = gistList.find((gist) => gist.id === id) ?? gistList.find((gist) => gist.description === id);
if (!gist) return new Response("404 Gist Not Found", { status: 404 });
const f = gist.files[file];
if (!f) return new Response("404 File Not Found", { status: 404 });
const res = await fetch(f.raw_url);
const headers = new Headers(res.headers);
// delete unwanted header
headers.delete("content-security-policy");
headers.delete("cross-origin-resource-policy");
for (const key of headers.keys()) {
if (key.startsWith("x-")) headers.delete(key);
}
if ([".ts", ".sh", ".cmd", ".bat", ".ps1"].some((end) => file.endsWith(end))) {
headers.set("Content-Type", "text/plain;charset=UTF-8");
} else {
headers.set("Content-Type", f.type);
}
return new Response(res.body, {
...res,
headers,
});
});
const handler = app.fetch
export default handler
interface Gist {
url: string;
forks_url: string;
commits_url: string;
id: string;
node_id: string;
git_pull_url: string;
git_push_url: string;
html_url: string;
files: Record<
string,
{
filename: string;
type: string;
language: string;
raw_url: string;
size: number;
}
>;
public: boolean;
created_at: Date;
updated_at: Date;
description: string;
comments: number;
user: null;
comments_url: string;
owner: null;
truncated: boolean;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
/**
* @title Extension Example
* @description A basic example tree for the chrome extension
*/
import { api } from "https://esm.town/v/pomdtr/api";
import type { Tree } from "https://esm.town/v/pomdtr/tree_types";
import { Hono } from "npm:hono";
// The tree is fetched using an http request, so any http framework can be used
const app = new Hono();
// The root endpoint will be called at first
app.get("/", (c) => {
const tree: Tree = [];
// Here, the items are statics
tree.push({
title: "🔗 Links",
expanded: true,
children: [
linkItem("View Tree Source", `https://www.val.town/pomdtr/tree_example`),
linkItem(
"Extension README",
"https://github.com/pomdtr/val-town-web-extension"
),
linkItem("LibSQL Studio", "https://libsqlstudio.com"),
],
});
// But the extension also supports dynamic trees
tree.push({
title: "🔖 Tags",
expanded: true,
children: [
{
title: "#example",
// Any endpoint can be specified here
children: "/tags/example",
},
{
title: "#blog",
children: "/tags/blog",
},
],
});
return c.json(tree);
});
// this endpoint is used to dynamically fetch a list of tags from the api
app.get("/tags/:tag", async (c) => {
let { tag } = c.req.param();
if (!tag.startsWith("#")) {
tag = "#" + tag;
}
const { data: vals } = await api(
`/v1/search/vals?query=${encodeURIComponent(tag)}`
);
const items = vals.map((val) => ({
title: val.name,
actions: [
{
type: "open",
icon: "🔗",
url: `https://www.val.town/v/${val.author.username}/${val.name}`,
},
],
}));
return c.json(items);
});
// You can build you own helper function
function linkItem(title: string, url: string): Item {
return {
title,
actions: [
{
type: "open",
url,
},
],
};
}
export default app.fetch;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
/** @jsxImportSource https://esm.sh/preact **/
import { blob } from "https://esm.town/v/std/blob?v=12";
import { compile } from "npm:@mdx-js/mdx";
import { Hono } from "npm:hono";
import { render } from "npm:preact-render-to-string";
import remarkFrontmatter from "npm:remark-frontmatter";
import remarkMdxFrontmatter from "npm:remark-mdx-frontmatter";
const css = `
.markdown-body {
box-sizing: border-box;
min-width: 200px;
max-width: 980px;
margin: 0 auto;
padding: 45px;
}
@media (max-width: 767px) {
.markdown-body {
padding: 15px;
}
}
`;
const app = new Hono();
app.get("/", (c) => {
return c.redirect("/p/index");
});
app.get("/p/:page", async (c) => {
const { page } = c.req.param();
const [basename, extension] = page.split(".");
console.log({ basename, extension });
const resp = await blob.get(`blog/${basename}.mdx`);
if (!extension) {
extension == "html";
}
if (extension == "mdx") {
return new Response(await resp.text(), {
headers: {
"Content-Type": "text/mdx",
},
});
}
if (extension == "jsx") {
const vfile = await compile(await resp.text(), {
jsx: true,
jsxImportSource: "https://esm.sh/preact",
remarkPlugins: [remarkFrontmatter, remarkMdxFrontmatter],
});
return new Response(vfile.toString(), {
headers: {
"Content-Type": "text/jsx",
},
});
}
const vfile = await compile(await resp.text(), {
jsxImportSource: "https://esm.sh/preact",
remarkPlugins: [remarkFrontmatter, remarkMdxFrontmatter],
});
if (extension == "js") {
return new Response(vfile.toString(), {
headers: {
"Content-Type": "text/js",
},
});
}
const importURL = URL.createObjectURL(
new Blob([vfile.toString()], {
type: "text/js",
})
);
const { default: MDXContent, frontmatter } = await import(importURL);
return c.html(
render(
<html>
<head>
<title>{frontmatter.title || page}</title>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link
href="https://cdnjs.cloudflare.com/ajax/libs/github-markdown-css/5.4.0/github-markdown.min.css"
rel="stylesheet"
/>
<link
href="https://cdn.jsdelivr.net/npm/prismjs@v1.29.0/themes/prism.css"
rel="stylesheet"
/>
<style dangerouslySetInnerHTML={{ __html: css }}></style>
</head>
<body class="markdown-body">
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
import { email } from "https://esm.town/v/std/email";
import {
CommandContext,
CommandData,
CommandDefinition,
} from "https://raw.githubusercontent.com/curtcox/CommandInterpreter/main/CommandDefinition.ts";
const meta = {
name: "email",
doc: "send an email",
source: import.meta.url,
input_formats: ["EmailOptions"],
output_formats: ["text"],
};
const func = async (context: CommandContext, options: CommandData) => {
const result = await email({
subject: options.content.subject,
text: options.content.text,
});
return {
commands: context.commands,
output: {
format: "JSON",
content: result,
},
};
};
export interface EmailOptions {
subject: string;
text: string;
}
export const command: CommandDefinition = {
meta,
func,
};

<3 Val Town

Val Town is my new favourite thing. Never heard of it ?

Well, according to it's homepage, Val Town is a social website to write and deploy TypeScript. It's often introduced as zappier for developers, or twitter for code.

The idea is simple: you write down a javascript snippet (named vals) in your browser, and it's instantly executed on a server. You can use it to:

  • execute a function on a cron schedule
  • host a small websites (this article hosted on Val Town)
  • send yourself emails
  • ...

But there is more to Val Town than this. If you take a look at the trending vals, you will quickly notice a pattern: most of the vals are about Val Town itself. People are using Val Town to extend Val Town, and it's fascinating to see what they come up with.

I've built a few of these extensions myself, and this article is about one of them.

Fixing the Val Town Search

Val.town is built around the http import feature of Deno. Each val is a standalone module, that you can import in other vals. It works both for your own vals, and for the vals of other users.

All of this is great, but there is one big issue: the search feature is terrible. It only works for exact text matches, and there is no way to set any filters based on username, creation_date, or anything else. This makes it really hard to find a val you are looking for, even if you are the one who wrote it.

In any other platform, I would have just given up and moved on. But Val Town is different. I was confident that I could address this issue in userspace, without having to wait for the platform to implement it.

Val Town allows you to run a val on a cron schedule, so I wrote a val that would fetch all the vals from the API, and store them as a sqlite table (did I mention that every user get it's own sqlite database ?).

Create valconst createQuery = `CREATE TABLE IF NOT EXISTS vals ( ... );`; // run every hour export default function(interval: Interval) { // create the val table await options.sqlite.execute(createQuery); let url = "https://api.val.town/v1/search/vals?query=%20&limit=100"; // fetch all vals, and store them in the sqlite table while (true) { const resp = await fetch(url); if (!resp.ok) { throw new Error(await resp.text()); } const res = await resp.json(); const rows = res.data.map(valToRow); await insertRows(rows, options); if (!res.links.next) { break; } url = res.links.next; } }

Once the val had finished running, I had a table with all the vals from the platform. I could now run queries on this table to find the vals I was looking for.

Create valimport { sqlite } from "https://esm.town/v/std/sqlite" const res = await sqlite.execute(`SELECT * FROM vals WHERE author = 'pomdtr' && code LIKE '%search%'`);

Of course I could have stopped there, but I wanted to go further. I wanted to share this table with other users, so they could run their own queries on it.

Isolating the Vals Table

There was still a challenge to overcome: the table was part of my account database, and I didn't want to give everyone access to it (there are some sensitive tables in there).

One way to solve this issue would be to publish a stripped-down api that only allows a few predefined queries. But that would be boring, and I wanted to give users the full power of SQL.

So I decided to isolate the val table in a separate account. There is a neat trick to achieve this on val.town: each val get's it own email address, and email sent to vals can be forwarded to your own email address.

Create valimport { email as sendEmail } from "https://esm.town/v/std/email?v=11"; // triggered each time an email is sent to pomdtr.sqlite_email@valtown.email export default async function(email: Email) { // forward the email to my own email address await sendEmail({ subject: email.subject, html: email.html, text: email.text, }); }

Since val.town account can be created with a val.email address, you can create an infinite number of accounts (and thus sqlite databases) using this trick.

So say hello to the sqlite account, which is a separate account that only contains the vals table.

After creating the account, I just needed to fork the cron val from my main account to get a copy of the vals table in the sqlite account.

Publishing the Table

The val.town stdlib provides a neat rpc function that provides a simple way to expose a function as an API. So I decided to write a simple val that would run a query on the table, and return the result.

Create valimport { rpc } from "https://esm.town/v/std/rpc?v=5"; import { InStatement, sqlite } from "https://esm.town/v/std/sqlite?v=4"; // rpc create an server, exposed on the val http endpoint export default rpc(async (statement: InStatement) => { try { // run the query, then return the result as json return await sqlite.execute(statement); } catch (e) { throw new Response(e.message, { status: 500, }); } });

Everyone can now run queries on the table thanks a publically accessible endpoint (you even have write access to it, but I trust you to not mess with it).

You can test it locally using curl and jq:

echo "SELECT * FROM vals WHERE lower(name) LIKE '%feed%' and lower(name) like '%email%' LIMIT 100" | jq -R '{args: [.]} ' | xargs -0 -I {} curl -X POST "https://sqlite-execute.web.val.run" -H "Content-Type: application/json" -d {} | jq

Of course I don't expect the average val.town user to use shell commands to run queries, so I also built an helper val to interact with the API, allowing users to run queries from their own vals.

Create val// only the import changed from the previous example import { db } from "https://esm.town/v/sqlite/db"; // this query will run on the `sqlite` account const res = await db.execute(`SELECT * FROM vals WHERE author = 'pomdtr' && code LIKE '%search%'`);

I've seen some really cool vals built on top of this API. Someone even wrote down a guide to help users interact with it from the command-line!

I hope that someone will build an search UI to interact with it at some point, but in the meantime, you can use a community-contributed sqlite web interface to run queries on top of the vals table.

Val.town as a code-taking app

As I've tried to show, having both a runtime, an editor and an API on the same platform is quite a magic formula. It's probably why val.town resonates so much with me.

Using CodeSandbox, Stackblitz, Repl.it, Gitpod, Github Codespaces or Gitpod feels pretty much the same, everything still revolves around the same concept of a project/repository. They feel uninspired somehow, trying to replicate the desktop IDE experience in the browser, instead of embracing the new possibilities that the web platform offers.

Val.town breaks this mold. I see it as a code-taking app, a place where I can just dump my ideas without worrying about the usual frictions of writing and deploying code.

1
2
3
4
5
6
7
8
9
import codeOnValTown from "https://esm.town/v/andreterron/codeOnValTown?v=50";
import { serveReadme } from "https://esm.town/v/pomdtr/serve_readme";
import { extractValInfo } from "https://esm.town/v/pomdtr/extractValInfo";
const val = extractValInfo(import.meta.url);
export default codeOnValTown(serveReadme({ val, title: "<3 Val.town" }));
// #blog
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
import { Hono } from "npm:hono";
export function githubExtension({
token: string,
}) {
const app = new Hono();
app.get("/", (c) => {
return c.json({
type: "list",
list: {
items: [
{
"title": "Search Repositories"
}
]
}
})
});
app.get("/repos/search", (c) => {
return {
type: "list",
list: {
items: []
}
}
})
return app.fetch
}
1
2
3
4
5
import {githubExtension} from "https://esm.town/v/pomdtr/github_cmdk_extension"
export default githubExtension({
token: Deno.env.get("GITHUB_TOKEN"),
})

Date Me Directory

This is entry-point val for the source code for the Date Me Directory. Contributions welcome!

This app uses Hono as the server framework and for JSX.

The vals are stored in Val Town SQLite.

Contributing

Forking this repo should mostly work, except for the sqlite database. You'll need to create the table & populate it with some data. This script should do it, but I think it has a couple bugs. If you're interested in contributing to this project contact me or comment on this val and I'll get it working for ya!

Todos

  • Make the SQLite database forkable and build a widget/workflow for that, ie fix @stevekrouse/dateme_sqlite
  • Require an email (that isn't shared publicly)
    • Verify the email address with a "magic link"
  • Refactor Location to an array of Lat, Lon
    • Geocode all the existing locations
    • Add a geocoder map input to the form
    • Allow selecting multiple location through the form
  • Profile performance & speed up site, possibly add more caching
  • Let people edit their forms
  • Featured profiles
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import { modifyFetchHandler } from "https://esm.town/v/andreterron/codeOnValTown?v=50";
import { form } from "https://esm.town/v/stevekrouse/date_me_form";
import browse from "https://esm.town/v/stevekrouse/dateme_browse";
import faq from "https://esm.town/v/stevekrouse/dateme_faq";
import home from "https://esm.town/v/stevekrouse/dateme_home";
import { dateMeRSS } from "https://esm.town/v/stevekrouse/dateMeRSS";
import { Hono } from "npm:hono@3";
const app = new Hono();
app.get("/", home);
app.get("/browse", browse);
app.route("/submit", form);
app.get("/faq", faq);
app.get("/rss.xml", c => dateMeRSS(c.req as unknown as Request));
export default modifyFetchHandler(app.fetch, {
style: `@media (max-width: 500px) {
.github-fork-ribbon {
display: none !important;
}
}`,
val: { handle: "stevekrouse", name: "dateme" },
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
export default async function handler(request: Request) {
if (request.method !== "POST") {
return Response.json({ message: "This val responds to POST requests." }, { status: 400 });
}
try {
const body = await request.text(); // await request.json();
console.log("request.text", body);
let result = generateSnippet(body);
return new Response(result);
} catch (e) {
console.log(e);
return Response.json({ message: "The body of this request was not JSON-encoded." }, {
status: 400,
});
}
}
function writeLog(text = "Hello World") {
console.log(text);
}
function generateSnippet(inputText) {
console.clear();
// Frontend protection
if (typeof inputText === "object") throw "Input is an object so no go!";
// Check if we have valid json
try {
inputText = JSON.parse(inputText);
} catch (error) {
let msg = "ERROR: generateSnippet - JSON Parse error from input!";
console.error(msg);
return msg;
throw msg;
}
// console.log({inputText})
let sections = Object.keys(inputText);
let urlCode = "";
let headerCode = "";
let authCode = "";
let methodCode = "";
let bodyCode = "";
let triggerCode = "";
sections.forEach((section) => {
if (section.toLowerCase() === "url") { urlCode = genUrlCode(inputText[section]); }
if (section.toLowerCase() === "header") { headerCode = genHeaderCode(inputText[section]); }
if (section.toLowerCase() === "auth") { authCode = genAuthCode(inputText[section]); }
if (section.toLowerCase() === "method") { methodCode = genMethodCode(inputText[section]); }
if (section.toLowerCase() === "body") { bodyCode = genBodyCode(inputText[section]); }
});
triggerCode = genTriggerCode();
// incase there are no headers
if (!headerCode) {
headerCode = genHeaderCode({});
}
// check for errors
let sectionVariables = [urlCode, headerCode, authCode, methodCode, bodyCode];
for (let codeResult in sectionVariables) {
// console.log(sectionVariables[codeResult])
if (sectionVariables[codeResult].startsWith("ERROR")) {
return sectionVariables[codeResult];
}
}
let finalCode = "//\n// Url code\n//\n";
finalCode += urlCode;
finalCode += "\n\n//\n// Header code\n//\n";
finalCode += headerCode;
if (authCode) {
finalCode += "\n\n//\n// Auth code\n//\n";
finalCode += authCode;
}
finalCode += "\n\n//\n// Method code\n//\n";
finalCode += methodCode;
finalCode += "\n\n//\n// Body code\n//\n";
finalCode += bodyCode;
finalCode += "\n\n//\n// Trigger code\n//\n";
finalCode += triggerCode;
// console.log(finalCode)
// let outputText = JSON.stringify(inputText,null,2)
return finalCode;
}
function genUrlCode(jsonObj) {
// console.log(jsonObj)
let url = getUrl(jsonObj);
let queryParamsArray = getQueryString(jsonObj);
// TODO: Handle variable code
let urlCode = "ASSIGN RestProxy_Url = \"" + url + "\"";
// console.log({queryParamsArray})
// Check if there is query params
if (queryParamsArray) {
urlCode += "\n\n// Url Query Params";
urlCode += "\nASSIGN RestProxy_UrlQueryParams = \"?\"";