Back to APIs list

Vercel API examples & templates

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

An http and class wrapper for Vercel's AI SDK

Usage:

  • Groq: https://yawnxyz-ai.web.val.run/generate?prompt="tell me a beer joke"&provider=groq&model=llama3-8b-8192
  • Perplexity: https://yawnxyz-ai.web.val.run/generate?prompt="what's the latest phage directory capsid & tail article about?"&provider=perplexity
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
import { Hono } from "npm:hono@3";
import { cors } from "npm:hono/cors";
import { openai, createOpenAI } from "npm:@ai-sdk/openai";
import { anthropic } from "npm:@ai-sdk/anthropic";
import { generateText, streamText, generateObject } from "npm:ai";
import { z } from "npm:zod";
const app = new Hono();
app.use('*', cors({
origin: '*',
allowMethods: ['GET', 'POST'],
allowHeaders: ['Content-Type'],
}));
openai.apiKey = Deno.env.get("OPENAI_API_KEY");
anthropic.apiKey = Deno.env.get("ANTHROPIC_API_KEY");
const groq = createOpenAI({
baseURL: 'https://api.groq.com/openai/v1',
apiKey: Deno.env.get("GROQ_API_KEY"),
});
const perplexity = createOpenAI({
apiKey: Deno.env.get("PERPLEXITY_API_KEY") ?? '',
baseURL: 'https://api.perplexity.ai/',
});
class ModelProvider {
async generateResponse(c, provider, model, prompt, maxTokens, streaming, schema, messages, tools) {
if (provider === 'openai') {
return this.generateOpenAIResponse(c, model, prompt, maxTokens, streaming, schema, messages, tools);
} else if (provider === 'anthropic') {
return this.generateAnthropicResponse(c, model, prompt, maxTokens, streaming, schema, messages, tools);
} else if (provider === 'groq') {
return this.generateGroqResponse(c, model, prompt, maxTokens, streaming, schema, messages, tools);
} else if (provider === 'perplexity') {
return this.generatePerplexityResponse(c, model, prompt, maxTokens, streaming, schema, messages, tools);
} else {
throw new Error('Invalid provider');
}
}
async generateOpenAIResponse(c, model, prompt, maxTokens, streaming, schema, messages) {
const options = {
model: openai(model || 'gpt-3.5-turbo'),
max_tokens: maxTokens || 100,
};
if (prompt && messages && messages.length > 0) {
throw new Error('prompt and messages cannot be defined at the same time');
} else if (prompt) {
options.prompt = prompt;
} else if (messages && messages.length > 0) {
options.messages = messages;
}
if (tools) {
options.tools = tools;
}
if (schema) {
const { object } = await generateObject({
...options,
schema: z.object(JSON.parse(schema)),
});
return c.json(object);
} else if (streaming) {
const encoder = new TextEncoder();
const stream = new ReadableStream({
async start(controller) {
const { textStream } = await streamText(options);
for await (const delta of textStream) {
controller.enqueue(encoder.encode(`data: ${JSON.stringify({ token: delta })}\n\n`));
}
controller.enqueue(encoder.encode(`data: [DONE]\n\n`));
controller.close();
},
});
return c.body(stream, 200, {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive',
});
} else {
const { text, finishReason, usage } = await generateText(options);
return c.json({ text, finishReason, usage });
}
}
async generateAnthropicResponse(c, model, prompt, maxTokens, streaming, schema, messages) {
if (schema) {
const { object } = await generateObject({
model: anthropic(model || 'claude-3-haiku-20240307'),
prompt: prompt,

gameplay_agent

This is a val.town mirror of gameplay/games/agent.

Click the link to see 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
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
/**
* # Agent
*
* This module provides a way to create agents for games.
*
* Agents are http services that get POSTed a game state and return an action.
* If you define your agent with {@link Connect4Agent} or {@link PokerAgent},
* then you can use {@link agentHandler} to create an http service that
* serves it. The service it creates is a standard fetch handler that can be
* used with a variety of different http server libraries.
*
* For Example
*
* ## Deno
*
* ```ts
* import { agentHandler } from "@gameplay/games/agent";
* import { myConnect4Agent } from "./my_connect4_agent.ts";
*
* const handler = agentHandler([
* { game: GameKind.Connect4, agentname: "my-agent", agent: myConnect4Agent },
* ]});
*
* Deno.serve(handler);
* ```
*
* ## Val.Town
*
* ```ts
* import { agentHandler } from "@gameplay/games/agent";
* import { myConnect4Agent } from "./my_connect4_agent.ts";
*
* const handler = agentHandler([
* { game: GameKind.Connect4, agentname: "my-agent", agent: myConnect4Agent },
* ]});
*
* export default handler;
* ```
*
* ## Bun
*
* ```ts
* import { agentHandler } from "@gameplay/games/agent";
* import { myConnect4Agent } from "./my_connect4_agent.ts";
*
* const handler = agentHandler([
* { game: GameKind.Connect4, agentname: "my-agent", agent: myConnect4Agent },
* ]});
*
* Bun.serve({fetch: handler});
* ```
*
* More than one agent can be registered so you can have multiple agents served
* by the same http service.
*
* You must host your own agent service and make sure it's publically
* accessible. You could use a service like Vercel, Cloudflare Workers, or
* Deno Deploy. The best and easiest way to host your agent service is to use
* val.town.
*
* You can also write your own agent service that implements the same http
* interface as {@link agentHandler}. This means you can use python or go or any
* other language to write your agent service, but you don't get the benefit of
* having all the game logic that you do by writing your agent in javascript or
* typescript and using this library.
*
* @module
*/
import type { Connect4Agent, Connect4AsyncAgent } from "https://esm.town/v/saolsen/gameplay_connect4";
import type { GameKind, Json } from "https://esm.town/v/saolsen/gameplay_games";
import type { PokerAgent, PokerAsyncAgent } from "https://esm.town/v/saolsen/gameplay_poker";
/**
* A Connect4 Agent
*
* @template T The type of the agent data.
* * Must extend {@link Json} which restricts it to a JSON serializable type.
*/
export interface Connect4AgentSpec<
T extends Json = Json,
> {
game: GameKind.Connect4;
/** The name of the agent. */
agentname: string;
/** The agent function. */
agent: Connect4Agent<T> | Connect4AsyncAgent<T>;
}
/**
* A Poker Agent
*
* @template T The type of the agent data.
* * Must extend {@link Json} which restricts it to a JSON serializable type.*
*/
export interface PokerAgentSpec<
T extends Json = Json,
> {
game: GameKind.Poker;
/** The name of the agent. */
agentname: string;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import { fetch } from "https://esm.town/v/std/fetch";
export async function fetchTweet(url) {
const tweetId = url.match(/(\d{19})/)[1];
const token = getToken(tweetId);
const tweetURL = new URL("https://cdn.syndication.twimg.com/tweet-result");
tweetURL.searchParams.set("id", tweetId);
tweetURL.searchParams.set("lang", "en");
tweetURL.searchParams.set("token", token);
const res = await fetch(tweetURL, {
redirect: "follow",
});
const tweetData = await res.json();
return tweetData;
}
// @see: https://github.com/vercel/react-tweet/blob/a292ca7/packages/react-tweet/src/api/fetch-tweet.ts#L27-L31
function getToken(id: string) {
return ((Number(id) / 1e15) * Math.PI)
.toString(6 ** 2)
.replace(/(0+|\.)/g, "");
}

petemillspaugh.com clippings: #1 – January 2024

Process for sending out a newsletter:

  1. Publish newsletter on the Web
  2. Fork this val and update subject, webUrl, targetSendDate
  3. Uncomment call to insertIntoNewslettersTable
  4. Add to @petermillspaugh/newsletters list Val
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 { sqlite } from "https://esm.town/v/std/sqlite?v=4";
export function getJanuary2024Newsletter() {
const subject = "#1 — January 2024";
const webUrl = "https://petemillspaugh.com/january-2024";
const targetSendDate = "2024-02-01 23:40:00";
const jsx = (
<main>
<h1>{subject}</h1>
<p>Hello!</p>
<p>
This is the first clipping from my digital garden. I’m still thinking through what I want these to be, but my
rough idea is an email newsletter that I send every month or two (ish) with a selection of stuff I’ve written
since the last clipping. I planted a note riffing on{" "}
<a href="https://petemillspaugh.com/newsletters">what I want clippings to be</a>, and I’m also writing about
{" "}
<a href="https://petemillspaugh.com/cultivating-emails">
my custom email setup using Val Town
</a>, if you’re curious.
</p>
<p>
There are relatively few of you subscribed, so thanks for being an early reader! Please do{" "}
<a href="mailto:pete@petemillspaugh.com">reply</a> if you feel like it to lmk what you think.
</p>
<h2>Planting my digital garden</h2>
<p>
This clipping will be a bit longer than most because I’m rounding up January 2024 and also looking back on what
I planted in 2023.
</p>
<p>
In the fall I redesigned my personal website as a digital garden. I cover this on my{" "}
<a href="https://petemillspaugh.com/about">about</a>{" "}
page, so I’ll save words here. To learn about digital gardening <em>generally</em>, you can skip right to{" "}
<a href="https://maggieappleton.com/garden-history">Maggie Appleton’s wonderful essay</a>{" "}
on the ethos and history of digital gardens.
</p>
<p>
I wrote about all sorts of stuff last year. Most of it relates to the Web in some way, but there are some bits
about effective learning and career ambitions mixed in. It’s the first time I’ve consistently written in public,
which feels good. I still write plenty for myself—the ratio of private to public writing I do is probably like 4
to 1. Here are some of my personal favorites from 2023:
</p>
<ul>
<li>
<a href="https://petemillspaugh.com/edison-bulb">
<strong>Edison bulb night mode</strong>
</a>. My coworker <a href="https://www.dannyguo.com/">Danny</a> linked this in a{" "}
<a href="https://news.ycombinator.com/item?id=38135892">post</a>{" "}
on Hacker News, which generated some helpful feedback and sent me over the Vercel analytics free tier (feeding
my vanity, ofc)
</li>
<li>
<a href="https://petemillspaugh.com/silly-tlds">
<strong>Silly TLDs</strong>
</a>. This is a short, fun one. It’s the thing I’ve written that friends outside of tech seem most interested
in / leads to the most fun conversations
</li>
<li>
<a href="https://petemillspaugh.com/my-next-next-next-job">
<strong>My next, next, next job</strong>
</a>. I initially wrote this as a private thought exercise then published it after a nudge from some friends
who I’d shared it with
</li>
<li>
<a href="https://petemillspaugh.com/weeks-of-your-life">
<strong>Weeks of your life</strong>
</a>. I built{" "}
<a href="https://weeksofyour.life">weeksofyour.life</a>—an interactive visualization of your life in
weeks—during a couple free days over the holidays. I posted it on{" "}
<a href="https://news.ycombinator.com/item?id=38753911">Show HN</a>, which spurred some heady philosophical
discussions about the meaning of life and also handy tips around performance (and again—vanity food)
</li>
<li>
<a href="https://petemillspaugh.com/map-in-the-woods">
<strong>Downloading a 30MB map in the woods</strong>
</a>. This was my first stab at a format I came up with called "Brainstorms" where I scribble down a thought
stream of questions on a topic I’m curious about (sans Internet), then return later to research
</li>
<li>
<a href="https://petemillspaugh.com/nextjs-search-with-pagefind">
<strong>Add search to your Next.js static site with Pagefind</strong>
</a>. This project was type 2 fun, and I’m really glad I stuck with it. The Pagefind creator{" "}
<a href="https://pagefind.app/docs/resources/#using-pagefind-with-a-specific-ssg">added</a>{" "}
my show ’n tell to the Pagefind docs, which felt good
</li>
<li>
<a href="https://petemillspaugh.com/think-small">
<strong>Think small</strong>
</a>. Of all the things I’ve written, this is what pops into my head most day to day, probably because it’s so
widely applicable

Example usage of the add_to_notion_w_ai val

Try with the money database.

Read and watch the demo run here

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";
import process from "node:process";
import Instructor from "npm:@instructor-ai/instructor";
import { Client } from "npm:@notionhq/client";
import OpenAI from "npm:openai";
import { render } from "npm:preact-render-to-string";
import { z } from "npm:zod";
const dbid = "DB_ID_GOES_HERE";
const NOTION_API_KEY = process.env.NOTION_API_KEY;
const notion = new Client({
auth: NOTION_API_KEY,
});
const MAGIC_AI_FILL_SYMB = "✨";
const supported_notion_props = {
"checkbox": "boolean",
"date": "date",
"multi_select": "array_enum",
"number": "number",
"rich_text": "string",
"select": "enum",
"status": "enum",
"title": "string",
"url": "string_url",
"email": "string_email",
};
const oai = new OpenAI({
apiKey: process.env.OPENAI_API_KEY ?? undefined,
});
const client = Instructor({
client: oai,
mode: "TOOLS",
});
function createPrompt(title, description, properties) {
let prompt =
"You are processing content into a database. Based on the title of the database, its properties, their types and options, and any existing descriptions, infer appropriate values for the fields:\n";
prompt += `Database Title: ${title}\n`;
if (description) {
prompt += `Database Description: ${description}\n\n`;
} else {
prompt += "\n";
}
prompt += "Properties (with types and options where applicable.):\n";
Object.keys(properties).forEach(key => {
const prop = properties[key];
prompt += `Name: ${prop.name}, Type: ${prop.type}`;
if (prop.description) {
prompt += `, Description: ${prop.description}`;
}
prompt += "\n";
if (prop.options) {
if (prop.type === "select" || prop.type === "status") {
prompt += "Options (choose one or none of these options):\n";
} else if (prop.type === "multi_select") {
prompt += "Options (choose one, multiple or none of these options):\n";
}
prop.options.forEach(option => {
prompt += ` - ${option.name}`;
if (option.description) {
prompt += `: ${option.description}`;
}
prompt += "\n";
});
}
});
prompt +=
"\nInfer and assign values to these properties based on the provided content and the aforementioned cotext.";
return prompt;
}
function processProperties(jsonObject) {
const properties = jsonObject.properties;
const filteredProps = {};
Object.keys(properties).forEach(key => {
const prop = properties[key];
const supportedType = supported_notion_props[prop.type];
if (supportedType && (prop.description?.startsWith(MAGIC_AI_FILL_SYMB) || prop.type === "title")) {
filteredProps[key] = {
name: prop.name,
type: prop.type,
description: prop.description ? prop.description.replace(MAGIC_AI_FILL_SYMB, "") : "",
};
1
2
3
4
5
import { fetchHTML } from "https://esm.town/v/stevekrouse/fetchHTML?v=9";
export function ping2() {
fetchHTML("https://simonharrisco-site.vercel.app/");
}

Val Town Docs Feedback Form & Handler

This feedback form is linked on our docs site.

Screenshot 2023-09-07 at 14.24.25@2x.png

This val renders an HTML form, including pre-fills the user's email address if they've submitted the form in the past (via a cookie), and pre-fills the URL by grabbing it out of the query params.

It handles form submissions, including parsing the form, saving the data into @stevekrouse.docsFeedback, a private JSON val, and then returns a thank you message, and set's the user's email address as a cookie, to save them some keystrokes the next time they fill out the form.

Another val, @stevekrouse.formFeedbackAlert, polls on an interval for new form submissions, and if it finds any, forwards them on a private Val Town discord channel.

There are a number of subtleties to the way each of some features are implemented.

A user submitted three pieces of feedback in quick succession, so I thought it'd be nice if we remembered user's email addresses after their first form submissions. There are classically two ways to do this, cookies or localstorage. I choose cookies. It requires setting them in the response header and getting them out of the request header. I used a Deno library to parse the cookie but I set it manually because that seemed simpler.

You may be wondering about how I'm getting the referrer out of the query params instead of from the HTTP Referrer header. I tried that at first, but it's increasingly difficult to get path data from it due to more restrictive security policies. So instead I decided to include the URL data in a query param. I get it there via this script in my blog's site:

Create valfunction updateFeedback(ref) { let feedback = [...document.getElementsByTagName('a')].find(e => e.innerText == 'Feedback') feedback.setAttribute('href', "https://stevekrouse-docfeedbackform.web.val.run/?ref=" + ref) } setTimeout(() => updateFeedback(document.location.href), 100); navigation.addEventListener('navigate', e => updateFeedback(e.destination.url));

Finally, you may be wondering why I queue up feedback in @stevekrouse.docsFeedback, a private JSON val, and then process it via @stevekrouse.formFeedbackAlert instead of sending it along to Discord directly in this val. I tried that originally but it felt too slow to wait for the API call to Discord before returning the "Thanks for your feedback" message. This is where the context.waitUntil method (that Cloudflare workers and Vercel Edge Functions support) would really come in handy – those allow you to return a Response, and then continue to compute. Currently Val Town requires you to stop all compute with the returning of your Response, so the only way to compute afterwards is to queue it up for another val to take over, and that's what I'm doing here.

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
import { waterCSS } from "https://esm.town/v/stevekrouse/waterCSS";
import { html } from "https://esm.town/v/stevekrouse/html";
import { set } from "https://esm.town/v/std/set?v=11";
import { docsFeedback } from "https://esm.town/v/stevekrouse/docsFeedback";
export let docFeedbackForm = async (req: Request) => {
if (req.method === "POST") {
let formData = await req.formData();
docsFeedback.push({
time: Date.now(),
feedback: formData.get("feedback"),
url: formData.get("url"),
email: formData.get("email"),
});
await set(
"docsFeedback",
docsFeedback,
);
return html("Thanks for your feedback!", {
headers: {
"Set-Cookie": `email=${formData.get("email")}`,
},
});
}
const { getCookies } = await import("https://deno.land/std/http/cookie.ts");
let ref = new URL(req.url).searchParams.get("ref") ?? "";
let email = getCookies(req.headers).email ?? "";
return html(`${waterCSS}
<div style="margin-left: auto;margin-right: auto;width: 500px;padding: 30px;">
<h1>Val Town Docs Feedback</h1>
<form action="https://stevekrouse-docFeedbackForm.web.val.run" method="post">
<textarea name="feedback" placeholder="Feedback..." rows="4" cols="50"></textarea>
<br/><input name="url" value="${ref}" placeholder="URL" size="50" />
<br/><input name="email" value="${email}" placeholder="Email" size="30" />
<br/> <button>Submit</button>
</form>
<a href="https://www.val.town/v/stevekrouse.docFeedbackForm"><b><i>vt</i></b> source code</a>
</div>`);
};
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
export let hackerNewsAuthors = [
"janpaul123", // JP Posma
"spiralganglion", // Ivan Reese (future of coding)
"danshipper", // Dan Shipper (every)
"Glench", // Glen Chiachiarri (dynamicland, scratch)
"zwass", // Zach Wasserman (osquery, fleet)
"breckyunits", // Breck Yuntis
"Rauchg", // Guillermo Rauch (vercel)
"ibdknox", // Chis Granger (eve, light table)
"conal", // Conal Elliott (frp)
"alankay", // Alan Kay
"jonathanedwards", // Jonathan Edwards (subtext)
"akkartik", // Kartik Agaram
"maccaw", // Alex MacCaw (reflect, clearbit)
"paul", // Paul Bucheit (gmail)
"pg", // Paul Graham (yc)
"worrydream", // Bret Victor (dynamicland)
"stevewoz", // Steve Wozniak (apple)
"sama", // Sam Altman (openai, yc)
"geoff", // Geoff Ralston (yc)
"dang", // Dan G (hn)
"obi1kenobi", // Predrag Gruevski
// people I want if they have accounts:
// rich hickey
// david nolan
// pete hunt
// jordan walke
// evan you
// ...everyone I follow on twitter
];

Val Town Docs Feedback Form & Handler

This feedback form is linked on our docs site.

Screenshot 2023-09-07 at 14.24.25@2x.png

This val renders an HTML form, including pre-fills the user's email address if they've submitted the form in the past (via a cookie), and pre-fills the URL by grabbing it out of the query params.

It handles form submissions, including parsing the form, saving the data into @stevekrouse.docsFeedback, a private JSON val, and then returns a thank you message, and set's the user's email address as a cookie, to save them some keystrokes the next time they fill out the form.

Another val, @stevekrouse.formFeedbackAlert, polls on an interval for new form submissions, and if it finds any, forwards them on a private Val Town discord channel.

There are a number of subtleties to the way each of some features are implemented.

A user submitted three pieces of feedback in quick succession, so I thought it'd be nice if we remembered user's email addresses after their first form submissions. There are classically two ways to do this, cookies or localstorage. I choose cookies. It requires setting them in the response header and getting them out of the request header. I used a Deno library to parse the cookie but I set it manually because that seemed simpler.

You may be wondering about how I'm getting the referrer out of the query params instead of from the HTTP Referrer header. I tried that at first, but it's increasingly difficult to get path data from it due to more restrictive security policies. So instead I decided to include the URL data in a query param. I get it there via this script in my blog's site:

Create valfunction updateFeedback(ref) { let feedback = [...document.getElementsByTagName('a')].find(e => e.innerText == 'Feedback') feedback.setAttribute('href', "https://stevekrouse-docfeedbackform.web.val.run/?ref=" + ref) } setTimeout(() => updateFeedback(document.location.href), 100); navigation.addEventListener('navigate', e => updateFeedback(e.destination.url));

Finally, you may be wondering why I queue up feedback in @stevekrouse.docsFeedback, a private JSON val, and then process it via @stevekrouse.formFeedbackAlert instead of sending it along to Discord directly in this val. I tried that originally but it felt too slow to wait for the API call to Discord before returning the "Thanks for your feedback" message. This is where the context.waitUntil method (that Cloudflare workers and Vercel Edge Functions support) would really come in handy – those allow you to return a Response, and then continue to compute. Currently Val Town requires you to stop all compute with the returning of your Response, so the only way to compute afterwards is to queue it up for another val to take over, and that's what I'm doing here.

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
import { html } from "https://esm.town/v/timlin/html";
import { email as email2 } from "https://esm.town/v/std/email?v=9";
export let docFeedbackForm = async (req: Request) => {
if (req.method === "POST") {
let formData = await req.formData();
const feedbackData = {
time: Date.now(),
feedback: formData.get("feedback"),
email: formData.get("email"),
};
await email2({
text: JSON.stringify(feedbackData, null, 2),
subject: "New Docs Feedback!",
});
return html("Thanks for your feedback!", {
headers: {
"Set-Cookie": `email=${formData.get("email")}`,
},
});
}
const { getCookies } = await import("https://deno.land/std/http/cookie.ts");
let ref = new URL(req.url).searchParams.get("ref") ?? "";
let email = getCookies(req.headers).email ?? "";
return html(
`<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/water.css@2/out/water.css">
<div style="margin-left: auto;margin-right: auto;width: 500px;padding: 30px;">
<h1>Val Town Docs Feedback</h1>
<form action="https://timlin-docFeedbackForm.web.val.run" method="post">
<textarea name="feedback" placeholder="Feedback..." rows="4" cols="50"></textarea>
<br/><input name="email" value="${email}" placeholder="Email" size="30" />
<br/> <button>Submit</button>
</form>
<a href="https://www.val.town/v/stevekrouse.docFeedbackForm"><b><i>vt</i></b> source code</a>
</div>`,
);
};

Val Town Docs Feedback Form & Handler

This feedback form is linked on our docs site.

Screenshot 2023-09-07 at 14.24.25@2x.png

This val renders an HTML form, including pre-fills the user's email address if they've submitted the form in the past (via a cookie), and pre-fills the URL by grabbing it out of the query params.

It handles form submissions, including parsing the form, saving the data into @stevekrouse.docsFeedback, a private JSON val, and then returns a thank you message, and set's the user's email address as a cookie, to save them some keystrokes the next time they fill out the form.

Another val, @stevekrouse.formFeedbackAlert, polls on an interval for new form submissions, and if it finds any, forwards them on a private Val Town discord channel.

There are a number of subtleties to the way each of some features are implemented.

A user submitted three pieces of feedback in quick succession, so I thought it'd be nice if we remembered user's email addresses after their first form submissions. There are classically two ways to do this, cookies or localstorage. I choose cookies. It requires setting them in the response header and getting them out of the request header. I used a Deno library to parse the cookie but I set it manually because that seemed simpler.

You may be wondering about how I'm getting the referrer out of the query params instead of from the HTTP Referrer header. I tried that at first, but it's increasingly difficult to get path data from it due to more restrictive security policies. So instead I decided to include the URL data in a query param. I get it there via this script in my blog's site:

Create valfunction updateFeedback(ref) { let feedback = [...document.getElementsByTagName('a')].find(e => e.innerText == 'Feedback') feedback.setAttribute('href', "https://stevekrouse-docfeedbackform.web.val.run/?ref=" + ref) } setTimeout(() => updateFeedback(document.location.href), 100); navigation.addEventListener('navigate', e => updateFeedback(e.destination.url));

Finally, you may be wondering why I queue up feedback in @stevekrouse.docsFeedback, a private JSON val, and then process it via @stevekrouse.formFeedbackAlert instead of sending it along to Discord directly in this val. I tried that originally but it felt too slow to wait for the API call to Discord before returning the "Thanks for your feedback" message. This is where the context.waitUntil method (that Cloudflare workers and Vercel Edge Functions support) would really come in handy – those allow you to return a Response, and then continue to compute. Currently Val Town requires you to stop all compute with the returning of your Response, so the only way to compute afterwards is to queue it up for another val to take over, and that's what I'm doing here.

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
import { html } from "https://esm.town/v/parkerdavis/html";
import { set } from "https://esm.town/v/std/set?v=11";
import { feedback as feedback2 } from "https://esm.town/v/parkerdavis/feedback";
import { email as email2 } from "https://esm.town/v/std/email?v=9";
export let docFeedbackForm = async (req: Request) => {
if (req.method === "POST") {
let formData = await req.formData();
let data = {
feedback: formData.get("feedback"),
url: formData.get("url"),
email: formData.get("email"),
time: Date.now(),
};
await email2({
text: data,
subject: "Form submission received!",
});
feedback2.push(data);
await set("feedback", feedback2);
return html(
`
<html>
<head>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/water.css@2/out/water.css">
<meta http-equiv="refresh" content="7; url='https://parkerdavis.dev'" />
</head>
<body>
<p>Hey, thanks for the feedback!</p>
<p>You will be redirected to <a href="https://parkerdavis.dev">https://parkerdavis.dev</a> in a few seconds.
</body>
</html>
`,
{
headers: {
"Set-Cookie": `email=${formData.get("email")}`,
},
},
);
}
return html("Hello!");
};
1
Next