Skip to content

Commit 1956d8c

Browse files
Merge pull request #1 from depatchedmode/netlify
Netlify
2 parents ae8b91f + 7633a96 commit 1956d8c

File tree

14 files changed

+637
-2124
lines changed

14 files changed

+637
-2124
lines changed

.gitignore

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,34 @@
11
*.nix
2-
node_modules/
3-
.replitnode_modules/
2+
.env
3+
.replit
4+
5+
# Local Netlify folder
6+
.netlify
7+
8+
# Logs
9+
logs
10+
*.log
11+
npm-debug.log*
12+
yarn-debug.log*
13+
yarn-error.log*
14+
pnpm-debug.log*
15+
lerna-debug.log*
16+
17+
# sharp issue: https://sharp.pixelplumbing.com/install#cross-platform
18+
# package-lock.json
19+
20+
node_modules
21+
dist
22+
dist-ssr
23+
*.local
24+
25+
# Editor directories and files
26+
.vscode/*
27+
!.vscode/extensions.json
28+
.idea
29+
.DS_Store
30+
*.suo
31+
*.ntvs*
32+
*.njsproj
33+
*.sln
34+
*.sw?

.replit

Lines changed: 0 additions & 17 deletions
This file was deleted.

README.md

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
# Simplest Frame
2+
## A zero-cost, zero-framework, dynamic Farcaster Frame template
3+
4+
### My needs for starting this were:
5+
1. **Zero Framework:** Didn't want a framework baked in, and most options default to Next.js/React
6+
2. **Zero Cost:** Frames are for experiments! Experimenting is more fruitful when it's free.
7+
3. **Stable:** The domain and its attached state should be reasonably stable over the horizon of an experiment. Replit can only give you this at cost (see above)
8+
4. **Dynamic Generation:** You can get all the above pretty easy with static files, but let's be real: we want dynamism!
9+
5. **Cool Tech 😎:** We want to be at the 🤬 here, people!
10+
11+
### Getting started
12+
13+
1. Clone the repo
14+
2. Install the Netlify CLI
15+
3. `npm install`
16+
4. `netlify dev`
17+
18+
### Caveats
19+
I am a designer larping as a dev. I invite your collaboration and feedback. Please be kind.
20+
21+
And please! Can we make it simpler?
22+
23+
### Roadmap
24+
1. Less bad
25+
2. More better
26+
3. Migration to the [everywhere.computer](https://everywhere.computer)

api/frame.js

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
export default async (req, context) => {
2+
const url = new URL(req.url);
3+
const count = url.searchParams.get('count') || 0;
4+
5+
const font = {
6+
file: 'Redaction-Regular.woff2',
7+
name: 'Redaction'
8+
};
9+
10+
const html = `
11+
<html>
12+
<head>
13+
<style>
14+
@font-face {
15+
font-family: "${font.name}";
16+
src:
17+
local("Trickster"),
18+
url("/fonts/${font.file}") format("woff2");
19+
}
20+
body {
21+
margin: 0;
22+
padding: 0;
23+
}
24+
fc-frame {
25+
font-family: "${font.name}";
26+
display: flex;
27+
width: 100vw;
28+
height: 100vh;
29+
color: white;
30+
background: black;
31+
align-items: center;
32+
justify-content: center;
33+
font-size: 5em;
34+
line-height: 1;
35+
}
36+
</style>
37+
</head>
38+
<body>
39+
<fc-frame>
40+
i've been framed ${count} times
41+
</fc-frame>
42+
</body>
43+
</html>
44+
`
45+
46+
return new Response(html,
47+
{
48+
status: 200,
49+
headers: { 'Content-Type': 'text/html' },
50+
}
51+
);
52+
}
53+
54+
export const config = {
55+
path: "/frame"
56+
};

api/index.js

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import { getStore } from "@netlify/blobs";
2+
import { URLSearchParams } from 'url';
3+
4+
5+
export default async (req, context) => {
6+
const store = getStore('frameState');
7+
let rawCount = await store.get('count');
8+
let count = parseInt(rawCount);
9+
10+
if (Number.isNaN(count)) count = 0;
11+
12+
console.debug('rawCount',rawCount);
13+
console.debug('parsedCount',count);
14+
15+
const host = process.env.URL;
16+
17+
if (req.method === 'POST') {
18+
let data;
19+
if (req.headers['content-type'] === 'application/json') {
20+
// Parse JSON body
21+
data = JSON.parse(req.body);
22+
} else if (req.headers['content-type'] === 'application/x-www-form-urlencoded') {
23+
// Parse URL-encoded body
24+
data = Object.fromEntries(new URLSearchParams(req.body));
25+
}
26+
const newCount = count+1
27+
console.debug('newCount',newCount);
28+
await store.set('count', newCount);
29+
rawCount = await store.get('count');
30+
console.debug('rawCount:updated',rawCount);
31+
}
32+
33+
const imagePath = `${host}/og-image?count=${count}`;
34+
35+
const html = `
36+
<!doctype html>
37+
<html>
38+
<head>
39+
<style>
40+
figure {
41+
display: inline-block;
42+
margin: 0;
43+
max-width: 100%;
44+
}
45+
img {
46+
max-width: 100%;
47+
border: 4px inset black;
48+
}
49+
</style>
50+
<meta property="og:image" content="${imagePath}" />
51+
<meta property="fc:frame" content="vNext" />
52+
<meta property="fc:frame:image" content="${imagePath}" />
53+
<meta property="fc:frame:button:1" content="Frame me!" />
54+
<title>Simplest Frame</title>
55+
</head>
56+
<body>
57+
<h1>The Simplest Frame</h1>
58+
<figure>
59+
<img width="600" src="${imagePath}" />
60+
</figure>
61+
<!-- Form for POST request -->
62+
<form action="/" method="post">
63+
<input type="submit" value="Frame me!" /> ${count}
64+
</form>
65+
</body>
66+
</html>
67+
`
68+
69+
return new Response(html,
70+
{
71+
status: 200,
72+
headers: { 'Content-Type': 'text/html' },
73+
}
74+
);
75+
}
76+
77+
export const config = {
78+
path: "/"
79+
};

api/og-image.js

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import satori from "satori";
2+
import sharp from "sharp";
3+
import { html } from "satori-html";
4+
5+
export default async (req, context) => {
6+
const url = new URL(req.url);
7+
const count = url.searchParams.get('count') || 0;
8+
9+
const host = process.env.URL;
10+
const htmlResponse = await fetch(`${host}/frame?count=${count}`);
11+
const markup = await htmlResponse.text();
12+
13+
const font = {
14+
fileName: 'Redaction-Regular.otf',
15+
cssName: 'Redaction'
16+
};
17+
const fontResponse = await fetch(`${host}/fonts/${font.fileName}`);
18+
const fontData = await fontResponse.arrayBuffer();
19+
20+
const svg = await satori(
21+
html(markup),
22+
{
23+
width: 1200,
24+
height: 800,
25+
fonts: [
26+
{
27+
name: font.cssName,
28+
data: fontData,
29+
weight: 400,
30+
style: "normal",
31+
},
32+
],
33+
});
34+
const svgBuffer = Buffer.from(svg);
35+
const png = sharp(svgBuffer).png();
36+
const response = await png.toBuffer();
37+
38+
return new Response(response,
39+
{
40+
status: 200,
41+
headers: { 'Content-Type': 'image/png' }
42+
}
43+
);
44+
}
45+
46+
export const config = {
47+
path: "/og-image"
48+
};

index.js

Lines changed: 0 additions & 70 deletions
This file was deleted.

netlify.toml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
[build]
2+
functions = "/api"
3+
publish = "/public"
4+
5+
[functions]
6+
external_node_modules = ["sharp"]
7+
included_files = ["node_modules/sharp/**/*"]

0 commit comments

Comments
 (0)