-
Notifications
You must be signed in to change notification settings - Fork 300
Blog: Best practices for handling screenshots in your app #2714
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 4 commits
36fba30
5120c67
ea4bb1f
687893a
772d578
a4ea586
619e64d
e5f3659
9e7522e
4961949
2a73d53
0f25e69
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||
|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,291 @@ | ||||||||
| --- | ||||||||
| layout: post | ||||||||
| title: Best practices for handling screenshots in your app | ||||||||
| description: Learn how to efficiently capture, store, and serve webpage screenshots without wasting resources or slowing down your app. | ||||||||
| date: 2026-01-26 | ||||||||
| cover: /images/blog/screenshots-best-practices/cover.png | ||||||||
| timeToRead: 5 | ||||||||
| author: atharva | ||||||||
| category: tutorial | ||||||||
| featured: false | ||||||||
| callToAction: true | ||||||||
| --- | ||||||||
|
|
||||||||
| Screenshots are everywhere in modern applications. Link previews in chat apps, visual documentation in developer tools, thumbnail galleries in dashboards, and compliance archives in enterprise software. What seems like a simple feature, "show me what this page looks like", can quickly become a performance bottleneck and maintenance headache if not handled properly. | ||||||||
|
|
||||||||
| The naive approach of generating a screenshot every time it is needed works fine during development. But in production, with real traffic, that approach falls apart. API rate limits get hit, response times spike, users stare at loading spinners, and costs climb. | ||||||||
|
|
||||||||
| This guide covers practical patterns for handling screenshots efficiently. Whether you are building link previews, automated reports, or visual testing pipelines, these best practices will help you avoid common pitfalls and build a robust screenshot workflow. | ||||||||
|
|
||||||||
| # Stop regenerating the same screenshot | ||||||||
|
|
||||||||
| The most common mistake is treating screenshot generation as a real-time operation. Every time a user requests a preview, the app fires off a new screenshot request, waits for it to render, and serves the result. This works for a handful of requests but scales poorly. | ||||||||
|
|
||||||||
| The fix is simple: **store screenshots in persistent storage and serve them from there**. | ||||||||
|
|
||||||||
| Here is how this looks with Appwrite. First, generate the screenshot: | ||||||||
|
|
||||||||
| ```js | ||||||||
| import { Client, Avatars } from "appwrite"; | ||||||||
|
|
||||||||
| const client = new Client() | ||||||||
| .setEndpoint('https://<REGION>.cloud.appwrite.io/v1') | ||||||||
| .setProject('<PROJECT_ID>'); | ||||||||
|
|
||||||||
| const avatars = new Avatars(client); | ||||||||
|
|
||||||||
| const screenshotUrl = avatars.getScreenshot({ | ||||||||
| url: 'https://example.com', | ||||||||
| viewportWidth: 1200, | ||||||||
| viewportHeight: 630, | ||||||||
| output: 'webp', | ||||||||
| quality: 85 | ||||||||
| }); | ||||||||
| ``` | ||||||||
|
|
||||||||
| Then, fetch it and store it in a bucket: | ||||||||
|
|
||||||||
| ```js | ||||||||
| import { Client, Storage, ID } from "appwrite"; | ||||||||
|
|
||||||||
| const storage = new Storage(client); | ||||||||
|
|
||||||||
| // Fetch the screenshot as a blob | ||||||||
| const response = await fetch(screenshotUrl); | ||||||||
| const blob = await response.blob(); | ||||||||
|
|
||||||||
| // Store in your screenshots bucket | ||||||||
| const file = await storage.createFile({ | ||||||||
| bucketId: 'screenshots', | ||||||||
| fileId: ID.unique(), | ||||||||
| file: new File([blob], 'example-com.webp', { type: 'image/webp' }) | ||||||||
| }); | ||||||||
|
|
||||||||
| console.log(file.$id); // Save this ID for later retrieval | ||||||||
| ``` | ||||||||
|
|
||||||||
| Now you have a permanent reference to the screenshot. Subsequent requests fetch from storage instead of regenerating. | ||||||||
|
|
||||||||
| # Track screenshots in a table | ||||||||
|
|
||||||||
| When storing screenshots, you need a way to look them up later. Use a table to map URLs to their stored file IDs: | ||||||||
|
|
||||||||
| ```js | ||||||||
| import { Client, Storage, TablesDB, Avatars, ID, Query } from "appwrite"; | ||||||||
|
|
||||||||
| const client = new Client() | ||||||||
| .setEndpoint('https://<REGION>.cloud.appwrite.io/v1') | ||||||||
| .setProject('<PROJECT_ID>'); | ||||||||
|
|
||||||||
| const storage = new Storage(client); | ||||||||
| const tablesDB = new TablesDB(client); | ||||||||
| const avatars = new Avatars(client); | ||||||||
| async function getOrCreateScreenshot(url) { | ||||||||
| // Check if we already have this screenshot | ||||||||
| const existing = await tablesDB.listRows({ | ||||||||
| databaseId: 'main', | ||||||||
| tableId: 'screenshots', | ||||||||
| queries: [Query.equal('url', [url])] | ||||||||
| }); | ||||||||
|
|
||||||||
| if (existing.total > 0) { | ||||||||
| return storage.getFileView({ bucketId: 'screenshots', fileId: existing.rows[0].fileId }); | ||||||||
| } | ||||||||
|
|
||||||||
| // Generate and store new screenshot | ||||||||
| const screenshotUrl = avatars.getScreenshot({ url }); | ||||||||
| const response = await fetch(screenshotUrl); | ||||||||
| const blob = await response.blob(); | ||||||||
|
|
||||||||
| const file = await storage.createFile({ | ||||||||
| bucketId: 'screenshots', | ||||||||
| fileId: ID.unique(), | ||||||||
| file: new File([blob], 'screenshot.webp', { type: 'image/webp' }) | ||||||||
| }); | ||||||||
|
|
||||||||
| // Save the mapping | ||||||||
| await tablesDB.createRow({ | ||||||||
| databaseId: 'main', | ||||||||
| tableId: 'screenshots', | ||||||||
| rowId: ID.unique(), | ||||||||
| data: { url: url, fileId: file.$id } | ||||||||
| }); | ||||||||
|
|
||||||||
| return storage.getFileView({ bucketId: 'screenshots', fileId: file.$id }); | ||||||||
| } | ||||||||
| ``` | ||||||||
|
|
||||||||
| This lets you query by URL directly. Each row automatically gets `$createdAt` and `$updatedAt` timestamps you can use for invalidation. | ||||||||
|
|
||||||||
| # Implement smart refresh strategies | ||||||||
|
|
||||||||
| Stored screenshots go stale. A page you captured last week might look completely different today. You need a strategy for keeping screenshots fresh without regenerating them unnecessarily. | ||||||||
|
|
||||||||
| **Time-based expiration** is the simplest approach. Check the `$updatedAt` timestamp and regenerate when the screenshot is too old: | ||||||||
|
|
||||||||
| ```js | ||||||||
| async function getScreenshot(url, maxAgeHours = 24) { | ||||||||
| const existing = await tablesDB.listRows({ | ||||||||
| databaseId: 'main', | ||||||||
| tableId: 'screenshots', | ||||||||
| queries: [Query.equal('url', [url])] | ||||||||
| }); | ||||||||
|
|
||||||||
| if (existing.total > 0) { | ||||||||
| const row = existing.rows[0]; | ||||||||
| const age = Date.now() - new Date(row.$updatedAt).getTime(); | ||||||||
| const maxAge = maxAgeHours * 60 * 60 * 1000; | ||||||||
|
|
||||||||
| if (age < maxAge) { | ||||||||
| return storage.getFileView({ bucketId: 'screenshots', fileId: row.fileId }); | ||||||||
| } | ||||||||
|
|
||||||||
| // Screenshot expired, regenerate and update the row | ||||||||
| const newFileId = await captureAndStore(url); | ||||||||
|
||||||||
| const newFileId = await captureAndStore(url); | |
| const newFileId = await createScreenshot(url); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@atharvadeosthale where is this function called from?
Outdated
Copilot
AI
Jan 26, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This function returns createScreenshot(url), but createScreenshot isn’t defined anywhere in the post. Either define it or reuse the earlier screenshot creation logic so the snippet is complete.
| return await createScreenshot(url); | |
| const fileId = await captureAndStore(url); | |
| return storage.getFileView('screenshots', fileId); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
atharvadeosthale marked this conversation as resolved.
Show resolved
Hide resolved
atharvadeosthale marked this conversation as resolved.
Show resolved
Hide resolved
Outdated
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should improve that first part and include a mention of loading
Also, pages with heavy JavaScript is a little too informal
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Add a server-side security note for auth headers.
The sentence can be read as client-side guidance. Please add a short caution to keep tokens server-side to avoid exposing secrets in browser code.
🛡️ Suggested wording
-For authenticated pages, use the `headers` parameter to pass authorization tokens or session cookies:
+For authenticated pages, use the `headers` parameter to pass authorization tokens or session cookies. Do this from a trusted backend/service to avoid exposing secrets in client-side code:📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| For authenticated pages, use the `headers` parameter to pass authorization tokens or session cookies: | |
| For authenticated pages, use the `headers` parameter to pass authorization tokens or session cookies. Do this from a trusted backend/service to avoid exposing secrets in client-side code: |
🤖 Prompt for AI Agents
In `@src/routes/blog/post/screenshots-best-practices/`+page.markdoc at line 246,
Add a short server-side security caution after the line "For authenticated
pages, use the `headers` parameter to pass authorization tokens or session
cookies:" clarifying that tokens should never be embedded in client-side or
browser-exposed code; recommend sending auth tokens from server-side code (e.g.,
in a load function or API route) and passing only necessary session data to the
client to avoid exposing secrets. Reference the `headers` parameter and the
phrase "authenticated pages" so the note is placed immediately afterward.
Uh oh!
There was an error while loading. Please reload this page.