Skip to content

Commit

Permalink
feat: image handler (#294)
Browse files Browse the repository at this point in the history
* feat: add `initial` property in frame handler options

* chore: format

* feat: add `number` type to `refreshing`

* nit: add jsdoc

* nit: lowercase header key

* nit: try to pass both values

* feat: add `.image` handler, drop defining images in frame options

* chore: changesets

* nit: lint

* nit: ban `imageOptions` in frame response if `image` is `string`

* nit: trying to release canary

* ci: add setup bun step

* feat: detect image handler presence automatically

* nit: lint

* chore: changesets

* docs: up

* docs: up

---------

Co-authored-by: dalechyn <dalechyn@users.noreply.github.com>
  • Loading branch information
dalechyn and dalechyn committed May 23, 2024
1 parent 32bfa4e commit c9257f5
Show file tree
Hide file tree
Showing 19 changed files with 778 additions and 120 deletions.
5 changes: 5 additions & 0 deletions .changeset/seven-birds-float.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"frog": patch
---

Introduced `.image` handler to handle images separately from the frame handler.
4 changes: 4 additions & 0 deletions .github/actions/install-dependencies/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@ runs:
cache: pnpm
node-version: 20

- uses: oven-sh/setup-bun@v1
with:
bun-version: latest

- name: Install dependencies
shell: bash
run: pnpm install
17 changes: 0 additions & 17 deletions playground/src/clock.tsx

This file was deleted.

4 changes: 2 additions & 2 deletions playground/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ import { neynar } from 'frog/hubs'
import { Box, Heading, vars } from './ui.js'

import { app as castActionApp } from './castAction.js'
import { app as clock } from './clock.js'
import { app as fontsApp } from './fonts.js'
import { app as initial } from './initial.js'
import { app as middlewareApp } from './middleware.js'
import { app as neynarApp } from './neynar.js'
import { app as routingApp } from './routing.js'
Expand Down Expand Up @@ -191,7 +191,7 @@ export const app = new Frog({
return c.error({ message: 'Bad inputs!' })
})
.route('/castAction', castActionApp)
.route('/clock', clock)
.route('/initial', initial)
.route('/ui', uiSystemApp)
.route('/fonts', fontsApp)
.route('/middleware', middlewareApp)
Expand Down
28 changes: 28 additions & 0 deletions playground/src/initial.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { Button, Frog } from 'frog'
import { Heading, VStack, vars } from './ui.js'

export const app = new Frog({
ui: { vars },
})
.frame('/', (c) => {
return c.res({
image: '/refreshing-image/cool-parameter',
intents: [<Button>Check again</Button>],
})
})
.image('/refreshing-image/:param', (c) => {
return c.res({
imageOptions: {
headers: {
'Cache-Control': 'max-age=0',
},
},
image: (
<VStack grow gap="4">
<Heading color="text400">
Current time: {new Date().toISOString()}
</Heading>
</VStack>
),
})
})
61 changes: 61 additions & 0 deletions site/pages/concepts/image-handler.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# Image Handler [Moving image rendering into another handler.]

Internally, **Frog** serves an image handler for every Frame at the frame path + `/image` endpoint.
Although it comes with ease, this approach has several limitations:
- Making *refreshing frames* would not be possible as initial frame response is cached indefinitely and image URL would change
if the image changes.
- If your Frame is heavily composed of different UI elements, browsers might *cut* a part of image URL that contains the compressed
image in the query parameter, making it fail to render.

In order to mitigate that, **Frog** has `.image` handler that can be used to serve an image at a *static URL*.

```tsx twoslash
// @noErrors
import { Frog } from 'frog'

export const app = new Frog()

app.frame('/', (c) => { // [!code focus]
return c.res({
image: '/img'
/* ... */
})
})

app.image('/img', (c) => { // [!code focus]
return c.res({/* ... */})
})
```

Since the image URL is static now, you're open to add `Cache-Control: max-age=0` header to image response to achieve refreshing initial frame.

:::warning
By default, image response will have `Cache-Control: public, immutable, no-transform, max-age=31536000` header set.
However, since HTTP headers are case-insensitive, different frameworks (i.e. Next.JS, Vercel) might
treat them differently and use lowercased format of such.

Thus, if you're overriding the `Cache-Control` header and can't see the changes to kick in – try lowercasing the header.
:::

```tsx twoslash
// @noErrors
import { Frog } from 'frog'

export const app = new Frog()

app.frame('/', (c) => {
return c.res({
image: '/img'
/* ... */
})
})

app.image('/img', (c) => {
return c.res({
headers: { // [!code focus]
'Cache-Control': 'max-age=0' // [!code focus]
}, // [!code focus]
/* ... */
})
})
```
96 changes: 31 additions & 65 deletions site/pages/reference/frog-cast-action-response.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -9,35 +9,15 @@ import { Frog } from 'frog'
export const app = new Frog()

app.castAction('/', (c) => {
return c.res({ // [!code focus]
// ... // [!code focus]
}) // [!code focus]
})
```

## headers

- **Type:** `Record<string, string>`

HTTP response headers to set for the action.

```tsx twoslash
// @noErrors
/** @jsxImportSource frog/jsx */
// ---cut---
import { Button, Frog } from 'frog'

export const app = new Frog()

app.castAction('/action/foo', (c) => {
return c.res({
headers: { // [!code focus]
'Cache-Control': 'max-age=0', // [!code focus]
}, // [!code focus]
message: 'Success!',
statusCode: 200,
})
})
return c.res({ // [!code focus]
// ... // [!code focus]
}) // [!code focus]
},
{
name: 'My Action',
icon: 'log'
}
)
```

## message
Expand All @@ -54,15 +34,20 @@ import { Button, Frog } from 'frog'
export const app = new Frog()

app.castAction('/', (c) => {
return c.res({
message: 'Action Succeeded!', // [!code focus]
statusCode: 200,
})
})
return c.res({
message: 'Action Succeeded!', // [!code focus]
type: 'message'
})
},
{
name: 'My Action',
icon: 'log'
}
)
```

## link
#

:::warning
The `link` property is valid to be added only in response with HTTP status 200.
Thus you cannot add one in `.error()` response.
Expand All @@ -81,34 +66,15 @@ import { Button, Frog } from 'frog'
export const app = new Frog()

app.castAction('/', (c) => {
return c.res({
message: 'Action Succeeded!',
link: 'https://frog.fm', // [!code focus]
statusCode: 200,
})
})
return c.res({
message: 'Action Succeeded!',
link: 'https://frog.fm', // [!code focus]
type: 'message',
})
},
{
name: 'My Action',
icon: 'log'
}
)
```

## statusCode

- **Type:** `200 | ClientErrorStatusCode | undefined`
- **Default:** `200`.

HTTP Status code to respond with.

```tsx twoslash
/** @jsxImportSource frog/jsx */
// ---cut---
import { Button, Frog } from 'frog'

export const app = new Frog()

app.castAction('/', (c) => {
return c.res({
message: 'Action Succeeded!',
statusCode: 200, // [!code focus]
})
})
```


57 changes: 47 additions & 10 deletions site/pages/reference/frog-cast-action.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,13 @@ import { Button, Frog } from 'frog'
const app = new Frog()

app.castAction('/', (c) => { // [!code focus]
console.log('Apple and Banana')
return c.res({ message: 'Action Succeeded' }) // [!code focus]
}) // [!code focus]
return c.message({ message: 'Action Succeeded' }) // [!code focus]
}, // [!code focus]
{ // [!code focus]
name: 'My Action', // [!code focus]
icon: 'log' // [!code focus]
} // [!code focus]
) // [!code focus]
```

## Parameters
Expand All @@ -41,10 +45,13 @@ const app = new Frog()
app.castAction(
'/foo/bar', // [!code focus]
(c) => {
console.log('Apple and Banana')
return c.res({ message: 'Action Succeeded' }) // [!code focus]
}
)
return c.message({ message: 'Action Succeeded' }) // [!code focus]
}, // [!code focus]
{ // [!code focus]
name: 'My Action', // [!code focus]
icon: 'log' // [!code focus]
} // [!code focus]
) // [!code focus]
```

### handler
Expand All @@ -63,9 +70,39 @@ const app = new Frog()
app.castAction(
'/foo/bar',
(c) => { // [!code focus]
console.log('Apple and Banana')
return c.res({ message: 'Action Succeeded' }) // [!code focus]
} // [!code focus]
return c.message({ message: 'Action Succeeded' }) // [!code focus]
}, // [!code focus]
{
name: 'My Action',
icon: 'log'
}
)
```

### options

- **Type:** `RouteOptions<'castAction'>`

Options for a Cast Action

```tsx twoslash
/** @jsxImportSource frog/jsx */
// ---cut---
import { Button, Frog } from 'frog'

const app = new Frog()

app.castAction(
'/foo/bar',
(c) => {
return c.message({ message: 'Action Succeeded' })
},
{
aboutUrl: 'https://frog.fm/reference/frog-cast-action', // [!code focus]
name: 'My Action', // [!code focus]
description: 'My awesome action.', // [!code focus]
icon: 'log' // [!code focus]
}
)
```

Expand Down
Loading

0 comments on commit c9257f5

Please sign in to comment.