kaddidlehopper 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CONTEXT.md +139 -0
- package/README.md +47 -0
- package/add-ons/ai/README.md +34 -0
- package/add-ons/ai/assets/_dot_env.local.append +13 -0
- package/add-ons/ai/assets/src/components/AIAssistant.tsx +149 -0
- package/add-ons/ai/assets/src/lib/ai-hook.ts +21 -0
- package/add-ons/ai/assets/src/lib/weather-tools.ts +30 -0
- package/add-ons/ai/assets/src/routes/api.chat.ts +94 -0
- package/add-ons/ai/assets/src/routes/chat.css +175 -0
- package/add-ons/ai/assets/src/routes/chat.tsx +141 -0
- package/add-ons/ai/info.json +27 -0
- package/add-ons/ai/package.json +17 -0
- package/add-ons/ai/small-logo.svg +8 -0
- package/dist/cli.js +251 -0
- package/dist/index.js +33 -0
- package/dist/types/cli.d.ts +8 -0
- package/dist/types/index.d.ts +2 -0
- package/dist/types/types.d.ts +14 -0
- package/dist/types.js +1 -0
- package/examples/blog/README.md +60 -0
- package/examples/blog/assets/content/posts/beach.md +12 -0
- package/examples/blog/assets/content/posts/jungle.md.ejs +12 -0
- package/examples/blog/assets/content/posts/mountains.md.ejs +12 -0
- package/examples/blog/assets/content/posts/snorkeling.md.ejs +12 -0
- package/examples/blog/assets/content/posts/waterfall.md.ejs +12 -0
- package/examples/blog/assets/content-collections.ts +30 -0
- package/examples/blog/assets/public/beach.jpg +0 -0
- package/examples/blog/assets/public/jungle.jpg +0 -0
- package/examples/blog/assets/public/mountains.jpg +0 -0
- package/examples/blog/assets/public/snorkeling.jpg +0 -0
- package/examples/blog/assets/public/waterfall.jpg +0 -0
- package/examples/blog/assets/src/components/Header.tsx +52 -0
- package/examples/blog/assets/src/components/VacayAssistant.tsx +205 -0
- package/examples/blog/assets/src/components/blog-posts.tsx +78 -0
- package/examples/blog/assets/src/components/ui/card.tsx +92 -0
- package/examples/blog/assets/src/lib/blog-ai-hook.ts +25 -0
- package/examples/blog/assets/src/lib/blog-tools.ts +111 -0
- package/examples/blog/assets/src/lib/utils.ts +6 -0
- package/examples/blog/assets/src/routes/__root.tsx +57 -0
- package/examples/blog/assets/src/routes/api.blog-chat.ts +117 -0
- package/examples/blog/assets/src/routes/category.$category.tsx +19 -0
- package/examples/blog/assets/src/routes/index.tsx +19 -0
- package/examples/blog/assets/src/routes/posts.$slug.tsx +63 -0
- package/examples/blog/assets/src/styles.css +138 -0
- package/examples/blog/info.json +43 -0
- package/examples/blog/package.json +23 -0
- package/examples/events/README.md +110 -0
- package/examples/events/assets/content/speakers/andre-costa.md +22 -0
- package/examples/events/assets/content/speakers/hans-mueller.md.ejs +22 -0
- package/examples/events/assets/content/speakers/isabella-martinez.md.ejs +22 -0
- package/examples/events/assets/content/speakers/kenji-nakamura.md.ejs +22 -0
- package/examples/events/assets/content/speakers/marie-dubois.md.ejs +20 -0
- package/examples/events/assets/content/speakers/priya-sharma.md.ejs +22 -0
- package/examples/events/assets/content/talks/croissant-lamination-secrets.md +39 -0
- package/examples/events/assets/content/talks/french-macaron-mastery.md.ejs +39 -0
- package/examples/events/assets/content/talks/neapolitan-pizza-tradition-meets-innovation.md.ejs +39 -0
- package/examples/events/assets/content/talks/savory-breads-of-the-mediterranean.md.ejs +39 -0
- package/examples/events/assets/content/talks/sourdough-from-starter-to-masterpiece.md.ejs +36 -0
- package/examples/events/assets/content/talks/the-art-of-the-perfect-tart.md.ejs +32 -0
- package/examples/events/assets/content/talks/the-science-of-sugar.md.ejs +39 -0
- package/examples/events/assets/content/talks/umami-in-pastry-east-meets-west.md.ejs +39 -0
- package/examples/events/assets/content-collections.ts +56 -0
- package/examples/events/assets/public/background-1.jpg +0 -0
- package/examples/events/assets/public/background-2.jpg +0 -0
- package/examples/events/assets/public/background-3.jpg +0 -0
- package/examples/events/assets/public/background-4.jpg +0 -0
- package/examples/events/assets/public/conference-logo.png +0 -0
- package/examples/events/assets/public/favicon.ico +0 -0
- package/examples/events/assets/public/speakers/andre-costa.jpg +0 -0
- package/examples/events/assets/public/speakers/hans-mueller.jpg +0 -0
- package/examples/events/assets/public/speakers/isabella-martinez.jpg +0 -0
- package/examples/events/assets/public/speakers/kenji-nakamura.jpg +0 -0
- package/examples/events/assets/public/speakers/marie-dubois.jpg +0 -0
- package/examples/events/assets/public/speakers/priya-sharma.jpg +0 -0
- package/examples/events/assets/public/talks/croissant-lamination-secrets.jpg +0 -0
- package/examples/events/assets/public/talks/french-macaron-mastery.jpg +0 -0
- package/examples/events/assets/public/talks/neapolitan-pizza-tradition-meets-innovation.jpg +0 -0
- package/examples/events/assets/public/talks/savory-breads-of-the-mediterranean.jpg +0 -0
- package/examples/events/assets/public/talks/sourdough-from-starter-to-masterpiece.jpg +0 -0
- package/examples/events/assets/public/talks/the-art-of-the-perfect-tart.jpg +0 -0
- package/examples/events/assets/public/talks/the-science-of-sugar.jpg +0 -0
- package/examples/events/assets/public/talks/umami-in-pastry-east-meets-west.jpg +0 -0
- package/examples/events/assets/public/tanstack-circle-logo.png +0 -0
- package/examples/events/assets/public/tanstack-word-logo-white.svg +1 -0
- package/examples/events/assets/src/components/Header.tsx +59 -0
- package/examples/events/assets/src/components/HeaderNav.tsx +67 -0
- package/examples/events/assets/src/components/HeroCarousel.tsx +61 -0
- package/examples/events/assets/src/components/RemyAssistant.tsx +207 -0
- package/examples/events/assets/src/components/SpeakerCard.tsx +67 -0
- package/examples/events/assets/src/components/TalkCard.tsx +77 -0
- package/examples/events/assets/src/components/ui/card.tsx +92 -0
- package/examples/events/assets/src/lib/conference-ai-hook.ts +26 -0
- package/examples/events/assets/src/lib/conference-tools.ts +210 -0
- package/examples/events/assets/src/lib/model-selection.ts +1 -0
- package/examples/events/assets/src/lib/utils.ts +6 -0
- package/examples/events/assets/src/routes/__root.tsx +70 -0
- package/examples/events/assets/src/routes/api.remy-chat.ts +119 -0
- package/examples/events/assets/src/routes/index.tsx +192 -0
- package/examples/events/assets/src/routes/schedule.index.tsx +274 -0
- package/examples/events/assets/src/routes/speakers.$slug.tsx +122 -0
- package/examples/events/assets/src/routes/speakers.index.tsx +40 -0
- package/examples/events/assets/src/routes/talks.$slug.tsx +116 -0
- package/examples/events/assets/src/routes/talks.index.tsx +40 -0
- package/examples/events/assets/src/styles.css +182 -0
- package/examples/events/info.json +74 -0
- package/examples/events/package.json +23 -0
- package/examples/marketing/README.md +60 -0
- package/examples/marketing/assets/public/logo.png +0 -0
- package/examples/marketing/assets/public/motorcycle-adventure.jpg +0 -0
- package/examples/marketing/assets/public/motorcycle-cruiser.jpg +0 -0
- package/examples/marketing/assets/public/motorcycle-scooter.jpg +0 -0
- package/examples/marketing/assets/public/motorcycle-sport.jpg +0 -0
- package/examples/marketing/assets/public/motorcycle-supersport.jpg +0 -0
- package/examples/marketing/assets/src/components/Header.tsx +36 -0
- package/examples/marketing/assets/src/components/MotorcycleAIAssistant.tsx +162 -0
- package/examples/marketing/assets/src/components/MotorcycleRecommendation.tsx +53 -0
- package/examples/marketing/assets/src/data/motorcycles.ts.ejs +77 -0
- package/examples/marketing/assets/src/lib/motorcycle-ai-hook.ts +24 -0
- package/examples/marketing/assets/src/lib/motorcycle-tools.ts +42 -0
- package/examples/marketing/assets/src/routes/__root.tsx +57 -0
- package/examples/marketing/assets/src/routes/api.motorcycle-chat.ts +78 -0
- package/examples/marketing/assets/src/routes/index.tsx +72 -0
- package/examples/marketing/assets/src/routes/motorcycles/$motorcycleId.tsx +56 -0
- package/examples/marketing/assets/src/store/motorcycle-assistant.ts +3 -0
- package/examples/marketing/assets/src/styles.css +212 -0
- package/examples/marketing/info.json +38 -0
- package/examples/marketing/package.json +14 -0
- package/examples/resume/README.md +82 -0
- package/examples/resume/assets/content/education/code-school.md +17 -0
- package/examples/resume/assets/content/jobs/freelance.md.ejs +13 -0
- package/examples/resume/assets/content/jobs/initech-junior.md +20 -0
- package/examples/resume/assets/content/jobs/initech-lead.md.ejs +29 -0
- package/examples/resume/assets/content/jobs/initrode-senior.md.ejs +28 -0
- package/examples/resume/assets/content-collections.ts +36 -0
- package/examples/resume/assets/public/headshot-on-white.jpg +0 -0
- package/examples/resume/assets/src/components/Header.tsx +33 -0
- package/examples/resume/assets/src/components/ResumeAssistant.tsx +193 -0
- package/examples/resume/assets/src/components/ui/badge.tsx +46 -0
- package/examples/resume/assets/src/components/ui/card.tsx +92 -0
- package/examples/resume/assets/src/components/ui/checkbox.tsx +30 -0
- package/examples/resume/assets/src/components/ui/hover-card.tsx +44 -0
- package/examples/resume/assets/src/components/ui/separator.tsx +26 -0
- package/examples/resume/assets/src/lib/resume-ai-hook.ts +21 -0
- package/examples/resume/assets/src/lib/resume-tools.ts +165 -0
- package/examples/resume/assets/src/lib/utils.ts +6 -0
- package/examples/resume/assets/src/routes/api.resume-chat.ts +110 -0
- package/examples/resume/assets/src/routes/index.tsx +220 -0
- package/examples/resume/assets/src/styles.css +138 -0
- package/examples/resume/info.json +25 -0
- package/examples/resume/package.json +26 -0
- package/package.json +39 -0
- package/project/base/_dot_claude/skills/content-collections/SKILL.md +505 -0
- package/project/base/_dot_claude/skills/netlify-blobs/SKILL.md +410 -0
- package/project/base/_dot_claude/skills/netlify-db/SKILL.md +424 -0
- package/project/base/_dot_claude/skills/netlify-debugging/SKILL.md +419 -0
- package/project/base/_dot_claude/skills/netlify-forms/SKILL.md +243 -0
- package/project/base/_dot_claude/skills/netlify-functions/SKILL.md +372 -0
- package/project/base/_dot_claude/skills/tanstack-start-api-routes/SKILL.md +421 -0
- package/project/base/_dot_claude/skills/tanstack-start-loaders/SKILL.md +426 -0
- package/project/base/_dot_claude/skills/tanstack-start-project-setup/SKILL.md +493 -0
- package/project/base/_dot_claude/skills/tanstack-start-routes/SKILL.md +430 -0
- package/project/base/_dot_claude/skills/tanstack-start-server-functions/SKILL.md +445 -0
- package/project/base/_dot_claude/skills/tanstack-start-typesafe-routing/SKILL.md +494 -0
- package/project/base/_dot_gitignore +8 -0
- package/project/base/netlify.toml +7 -0
- package/project/base/package.json +33 -0
- package/project/base/public/favicon.ico +0 -0
- package/project/base/public/tanstack-circle-logo.png +0 -0
- package/project/base/public/tanstack-word-logo-white.svg +1 -0
- package/project/base/src/components/Header.tsx +17 -0
- package/project/base/src/components/HeaderNav.tsx.ejs +179 -0
- package/project/base/src/router.tsx +15 -0
- package/project/base/src/routes/__root.tsx +57 -0
- package/project/base/src/routes/index.tsx +48 -0
- package/project/base/src/styles.css +15 -0
- package/project/base/tsconfig.json +28 -0
- package/project/base/vite.config.ts.ejs +25 -0
- package/project/packages.json +22 -0
- package/scripts/check-outdated-packages.js +421 -0
- package/src/cli.ts +343 -0
- package/src/index.ts +49 -0
- package/src/types.ts +15 -0
- package/tsconfig.json +17 -0
|
@@ -0,0 +1,410 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: netlify-blobs
|
|
3
|
+
description: Store and retrieve unstructured data using Netlify Blobs key-value storage. Use for file uploads, caching, user-generated content, session storage, or any binary/JSON data persistence on Netlify.
|
|
4
|
+
license: Apache-2.0
|
|
5
|
+
metadata:
|
|
6
|
+
author: netlify
|
|
7
|
+
version: "1.0"
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# Netlify Blobs
|
|
11
|
+
|
|
12
|
+
Netlify Blobs is a built-in key-value store for unstructured data. It's ideal for storing files, JSON, or any binary data without setting up external storage.
|
|
13
|
+
|
|
14
|
+
## When to Use
|
|
15
|
+
|
|
16
|
+
- Storing user uploads (images, files)
|
|
17
|
+
- Caching API responses
|
|
18
|
+
- Session or state storage
|
|
19
|
+
- Persisting background function results
|
|
20
|
+
- Deploy-specific data storage
|
|
21
|
+
|
|
22
|
+
## Installation
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
npm install @netlify/blobs
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Basic Usage
|
|
29
|
+
|
|
30
|
+
### Writing Data
|
|
31
|
+
|
|
32
|
+
```typescript
|
|
33
|
+
import { getStore } from "@netlify/blobs";
|
|
34
|
+
|
|
35
|
+
// Get a store reference
|
|
36
|
+
const store = getStore("my-store");
|
|
37
|
+
|
|
38
|
+
// Store a string
|
|
39
|
+
await store.set("greeting", "Hello, World!");
|
|
40
|
+
|
|
41
|
+
// Store JSON (automatically serialized)
|
|
42
|
+
await store.setJSON("user", {
|
|
43
|
+
id: 1,
|
|
44
|
+
name: "Alice",
|
|
45
|
+
email: "alice@example.com"
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
// Store binary data (Buffer, ArrayBuffer, Blob, ReadableStream)
|
|
49
|
+
await store.set("image", imageBuffer);
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
### Reading Data
|
|
53
|
+
|
|
54
|
+
```typescript
|
|
55
|
+
import { getStore } from "@netlify/blobs";
|
|
56
|
+
|
|
57
|
+
const store = getStore("my-store");
|
|
58
|
+
|
|
59
|
+
// Get as string
|
|
60
|
+
const greeting = await store.get("greeting");
|
|
61
|
+
// → "Hello, World!"
|
|
62
|
+
|
|
63
|
+
// Get as JSON (automatically parsed)
|
|
64
|
+
const user = await store.get("user", { type: "json" });
|
|
65
|
+
// → { id: 1, name: "Alice", email: "alice@example.com" }
|
|
66
|
+
|
|
67
|
+
// Get as ArrayBuffer
|
|
68
|
+
const imageData = await store.get("image", { type: "arrayBuffer" });
|
|
69
|
+
|
|
70
|
+
// Get as Blob
|
|
71
|
+
const blob = await store.get("image", { type: "blob" });
|
|
72
|
+
|
|
73
|
+
// Get as Stream
|
|
74
|
+
const stream = await store.get("image", { type: "stream" });
|
|
75
|
+
|
|
76
|
+
// Returns null if key doesn't exist
|
|
77
|
+
const missing = await store.get("nonexistent");
|
|
78
|
+
// → null
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### Deleting Data
|
|
82
|
+
|
|
83
|
+
```typescript
|
|
84
|
+
const store = getStore("my-store");
|
|
85
|
+
|
|
86
|
+
// Delete a single key
|
|
87
|
+
await store.delete("old-data");
|
|
88
|
+
|
|
89
|
+
// Check if key exists before deleting
|
|
90
|
+
const exists = await store.get("maybe-exists");
|
|
91
|
+
if (exists) {
|
|
92
|
+
await store.delete("maybe-exists");
|
|
93
|
+
}
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
### Listing Keys
|
|
97
|
+
|
|
98
|
+
```typescript
|
|
99
|
+
const store = getStore("my-store");
|
|
100
|
+
|
|
101
|
+
// List all keys
|
|
102
|
+
const { blobs } = await store.list();
|
|
103
|
+
for (const blob of blobs) {
|
|
104
|
+
console.log(blob.key);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// List with prefix filter
|
|
108
|
+
const { blobs: userBlobs } = await store.list({ prefix: "users/" });
|
|
109
|
+
|
|
110
|
+
// Paginate through results
|
|
111
|
+
let cursor: string | undefined;
|
|
112
|
+
do {
|
|
113
|
+
const result = await store.list({ cursor });
|
|
114
|
+
for (const blob of result.blobs) {
|
|
115
|
+
console.log(blob.key);
|
|
116
|
+
}
|
|
117
|
+
cursor = result.cursor;
|
|
118
|
+
} while (cursor);
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
## Storing Files with Metadata
|
|
122
|
+
|
|
123
|
+
```typescript
|
|
124
|
+
const store = getStore("uploads");
|
|
125
|
+
|
|
126
|
+
// Store file with metadata
|
|
127
|
+
await store.set("profile-123.jpg", imageBuffer, {
|
|
128
|
+
metadata: {
|
|
129
|
+
contentType: "image/jpeg",
|
|
130
|
+
uploadedBy: "user-123",
|
|
131
|
+
originalName: "my-photo.jpg",
|
|
132
|
+
},
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
// Retrieve with metadata
|
|
136
|
+
const { data, metadata } = await store.getWithMetadata("profile-123.jpg", {
|
|
137
|
+
type: "arrayBuffer",
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
console.log(metadata.contentType); // "image/jpeg"
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
## Consistency Modes
|
|
144
|
+
|
|
145
|
+
### Eventual Consistency (Default)
|
|
146
|
+
|
|
147
|
+
Data is cached at the edge for fast reads. Updates propagate within 60 seconds.
|
|
148
|
+
|
|
149
|
+
```typescript
|
|
150
|
+
const store = getStore("my-store");
|
|
151
|
+
// Uses eventual consistency by default
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
### Strong Consistency
|
|
155
|
+
|
|
156
|
+
For when you need immediate read-after-write consistency:
|
|
157
|
+
|
|
158
|
+
```typescript
|
|
159
|
+
// Store-level strong consistency
|
|
160
|
+
const store = getStore({
|
|
161
|
+
name: "my-store",
|
|
162
|
+
consistency: "strong",
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
// Or per-operation
|
|
166
|
+
const store = getStore("my-store");
|
|
167
|
+
const data = await store.get("key", { consistency: "strong" });
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
Use strong consistency when:
|
|
171
|
+
- Reading immediately after writing
|
|
172
|
+
- Handling transactions or counters
|
|
173
|
+
- Data correctness is critical
|
|
174
|
+
|
|
175
|
+
## Deploy-Scoped Stores
|
|
176
|
+
|
|
177
|
+
Data tied to a specific deploy (cleaned up with deploy):
|
|
178
|
+
|
|
179
|
+
```typescript
|
|
180
|
+
import { getDeployStore } from "@netlify/blobs";
|
|
181
|
+
|
|
182
|
+
// This store is scoped to the current deploy
|
|
183
|
+
const store = getDeployStore("build-cache");
|
|
184
|
+
|
|
185
|
+
// Data is automatically cleaned up when deploy is deleted
|
|
186
|
+
await store.set("compiled-assets", compiledData);
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
## Using in Functions
|
|
190
|
+
|
|
191
|
+
### Serverless Function Example
|
|
192
|
+
|
|
193
|
+
```typescript
|
|
194
|
+
// netlify/functions/upload.ts
|
|
195
|
+
import { getStore } from "@netlify/blobs";
|
|
196
|
+
import type { Context } from "@netlify/functions";
|
|
197
|
+
|
|
198
|
+
export default async (request: Request, context: Context) => {
|
|
199
|
+
const store = getStore("uploads");
|
|
200
|
+
|
|
201
|
+
if (request.method === "POST") {
|
|
202
|
+
const formData = await request.formData();
|
|
203
|
+
const file = formData.get("file") as File;
|
|
204
|
+
|
|
205
|
+
if (!file) {
|
|
206
|
+
return new Response("No file provided", { status: 400 });
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const key = `${Date.now()}-${file.name}`;
|
|
210
|
+
const buffer = await file.arrayBuffer();
|
|
211
|
+
|
|
212
|
+
await store.set(key, buffer, {
|
|
213
|
+
metadata: {
|
|
214
|
+
contentType: file.type,
|
|
215
|
+
originalName: file.name,
|
|
216
|
+
size: file.size.toString(),
|
|
217
|
+
},
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
return Response.json({ key, message: "Upload successful" });
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
if (request.method === "GET") {
|
|
224
|
+
const url = new URL(request.url);
|
|
225
|
+
const key = url.searchParams.get("key");
|
|
226
|
+
|
|
227
|
+
if (!key) {
|
|
228
|
+
return new Response("Key required", { status: 400 });
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const { data, metadata } = await store.getWithMetadata(key, {
|
|
232
|
+
type: "arrayBuffer",
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
if (!data) {
|
|
236
|
+
return new Response("Not found", { status: 404 });
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
return new Response(data, {
|
|
240
|
+
headers: {
|
|
241
|
+
"Content-Type": metadata?.contentType || "application/octet-stream",
|
|
242
|
+
},
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
return new Response("Method not allowed", { status: 405 });
|
|
247
|
+
};
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
### Edge Function Example
|
|
251
|
+
|
|
252
|
+
```typescript
|
|
253
|
+
// netlify/edge-functions/cache.ts
|
|
254
|
+
import { getStore } from "@netlify/blobs";
|
|
255
|
+
|
|
256
|
+
export default async (request: Request) => {
|
|
257
|
+
const url = new URL(request.url);
|
|
258
|
+
const cacheKey = `page-cache:${url.pathname}`;
|
|
259
|
+
|
|
260
|
+
const store = getStore("page-cache");
|
|
261
|
+
|
|
262
|
+
// Try to get from cache
|
|
263
|
+
const cached = await store.get(cacheKey, { type: "json" });
|
|
264
|
+
|
|
265
|
+
if (cached && cached.expires > Date.now()) {
|
|
266
|
+
return new Response(cached.html, {
|
|
267
|
+
headers: {
|
|
268
|
+
"Content-Type": "text/html",
|
|
269
|
+
"X-Cache": "HIT",
|
|
270
|
+
},
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// Generate fresh content
|
|
275
|
+
const html = await generatePage(url.pathname);
|
|
276
|
+
|
|
277
|
+
// Cache for 5 minutes
|
|
278
|
+
await store.setJSON(cacheKey, {
|
|
279
|
+
html,
|
|
280
|
+
expires: Date.now() + 5 * 60 * 1000,
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
return new Response(html, {
|
|
284
|
+
headers: {
|
|
285
|
+
"Content-Type": "text/html",
|
|
286
|
+
"X-Cache": "MISS",
|
|
287
|
+
},
|
|
288
|
+
});
|
|
289
|
+
};
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
## File-Based Uploads (Build Time)
|
|
293
|
+
|
|
294
|
+
Place files in `.netlify/blobs/deploy` during build:
|
|
295
|
+
|
|
296
|
+
```
|
|
297
|
+
project/
|
|
298
|
+
├── .netlify/
|
|
299
|
+
│ └── blobs/
|
|
300
|
+
│ └── deploy/
|
|
301
|
+
│ ├── assets/
|
|
302
|
+
│ │ └── logo.png
|
|
303
|
+
│ ├── $assets/logo.png.json # Metadata file
|
|
304
|
+
│ └── config.json
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
Metadata file example (`$assets/logo.png.json`):
|
|
308
|
+
```json
|
|
309
|
+
{
|
|
310
|
+
"contentType": "image/png",
|
|
311
|
+
"uploadedAt": "2024-01-15T10:00:00Z"
|
|
312
|
+
}
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
## Common Patterns
|
|
316
|
+
|
|
317
|
+
### Rate Limiting
|
|
318
|
+
|
|
319
|
+
```typescript
|
|
320
|
+
const store = getStore({ name: "rate-limits", consistency: "strong" });
|
|
321
|
+
|
|
322
|
+
async function checkRateLimit(ip: string, limit: number, windowMs: number) {
|
|
323
|
+
const key = `rate:${ip}`;
|
|
324
|
+
const now = Date.now();
|
|
325
|
+
|
|
326
|
+
const data = await store.get(key, { type: "json" }) as {
|
|
327
|
+
count: number;
|
|
328
|
+
resetAt: number;
|
|
329
|
+
} | null;
|
|
330
|
+
|
|
331
|
+
if (!data || data.resetAt < now) {
|
|
332
|
+
await store.setJSON(key, { count: 1, resetAt: now + windowMs });
|
|
333
|
+
return { allowed: true, remaining: limit - 1 };
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
if (data.count >= limit) {
|
|
337
|
+
return { allowed: false, remaining: 0, resetAt: data.resetAt };
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
await store.setJSON(key, { count: data.count + 1, resetAt: data.resetAt });
|
|
341
|
+
return { allowed: true, remaining: limit - data.count - 1 };
|
|
342
|
+
}
|
|
343
|
+
```
|
|
344
|
+
|
|
345
|
+
### Session Storage
|
|
346
|
+
|
|
347
|
+
```typescript
|
|
348
|
+
const sessions = getStore({ name: "sessions", consistency: "strong" });
|
|
349
|
+
|
|
350
|
+
async function createSession(userId: string) {
|
|
351
|
+
const sessionId = crypto.randomUUID();
|
|
352
|
+
await sessions.setJSON(`session:${sessionId}`, {
|
|
353
|
+
userId,
|
|
354
|
+
createdAt: Date.now(),
|
|
355
|
+
expiresAt: Date.now() + 24 * 60 * 60 * 1000, // 24 hours
|
|
356
|
+
});
|
|
357
|
+
return sessionId;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
async function getSession(sessionId: string) {
|
|
361
|
+
const session = await sessions.get(`session:${sessionId}`, { type: "json" });
|
|
362
|
+
if (!session || session.expiresAt < Date.now()) {
|
|
363
|
+
return null;
|
|
364
|
+
}
|
|
365
|
+
return session;
|
|
366
|
+
}
|
|
367
|
+
```
|
|
368
|
+
|
|
369
|
+
### Background Job Results
|
|
370
|
+
|
|
371
|
+
```typescript
|
|
372
|
+
// Background function writes result
|
|
373
|
+
// netlify/functions/process-background.ts
|
|
374
|
+
import { getStore } from "@netlify/blobs";
|
|
375
|
+
|
|
376
|
+
export default async (request: Request) => {
|
|
377
|
+
const { jobId, data } = await request.json();
|
|
378
|
+
const store = getStore("job-results");
|
|
379
|
+
|
|
380
|
+
await store.setJSON(`job:${jobId}:status`, { status: "processing" });
|
|
381
|
+
|
|
382
|
+
// Do long-running work...
|
|
383
|
+
const result = await processData(data);
|
|
384
|
+
|
|
385
|
+
await store.setJSON(`job:${jobId}:status`, {
|
|
386
|
+
status: "complete",
|
|
387
|
+
result,
|
|
388
|
+
});
|
|
389
|
+
};
|
|
390
|
+
|
|
391
|
+
// Regular function checks status
|
|
392
|
+
// netlify/functions/job-status.ts
|
|
393
|
+
export default async (request: Request) => {
|
|
394
|
+
const url = new URL(request.url);
|
|
395
|
+
const jobId = url.searchParams.get("id");
|
|
396
|
+
|
|
397
|
+
const store = getStore("job-results");
|
|
398
|
+
const status = await store.get(`job:${jobId}:status`, { type: "json" });
|
|
399
|
+
|
|
400
|
+
return Response.json(status || { status: "not-found" });
|
|
401
|
+
};
|
|
402
|
+
```
|
|
403
|
+
|
|
404
|
+
## Limits
|
|
405
|
+
|
|
406
|
+
- **Key length**: 600 bytes max
|
|
407
|
+
- **Value size**: 5GB max per blob
|
|
408
|
+
- **Metadata**: 64KB max per blob
|
|
409
|
+
- **Store names**: Must be alphanumeric with hyphens
|
|
410
|
+
- **Consistency**: Eventual by default (60s propagation), strong available
|