feat: correctly cache images with redis
This commit is contained in:
parent
1917fc7d8f
commit
af8adf9ce7
29
components/Card.tsx
Normal file
29
components/Card.tsx
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
import { Recipe } from "../lib/recipes.ts";
|
||||||
|
|
||||||
|
export function Card(
|
||||||
|
{ link, title, image }: { link?: string; title?: string; image?: string },
|
||||||
|
) {
|
||||||
|
return (
|
||||||
|
<a
|
||||||
|
href={link}
|
||||||
|
style={{
|
||||||
|
backgroundImage: `url(${image})`,
|
||||||
|
backgroundSize: "cover",
|
||||||
|
}}
|
||||||
|
class="bg-gray-900 text-white rounded-3xl shadow-md p-4 relative overflow-hidden
|
||||||
|
lg:w-56 lg:h-56
|
||||||
|
sm:w-40 sm:h-40
|
||||||
|
w-32 h-32"
|
||||||
|
>
|
||||||
|
<div class="h-full flex flex-col justify-between relative z-10">
|
||||||
|
<div>
|
||||||
|
{/* Recipe Card content */}
|
||||||
|
</div>
|
||||||
|
<div class="mt-2">
|
||||||
|
{title}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="absolute inset-x-0 bottom-0 h-3/4 bg-gradient-to-t from-black to-transparent" />
|
||||||
|
</a>
|
||||||
|
);
|
||||||
|
}
|
@ -1,29 +1,18 @@
|
|||||||
import { Recipe } from "../lib/recipes.ts";
|
import { Card } from "@components/Card.tsx";
|
||||||
|
import { Recipe } from "@lib/recipes.ts";
|
||||||
|
|
||||||
export function RecipeCard({ recipe }: { recipe: Recipe }) {
|
export function RecipeCard({ recipe }: { recipe: Recipe }) {
|
||||||
|
const { meta: { image = "Recipes/images/placeholder.jpg" } = {} } = recipe;
|
||||||
|
|
||||||
|
const imageUrl = image.startsWith("Recipes/images/")
|
||||||
|
? `/api/images?image=${image}&width=200&height=200`
|
||||||
|
: image;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<a
|
<Card
|
||||||
href={`/recipes/${recipe.id}`}
|
title={recipe.name}
|
||||||
style={{
|
image={imageUrl}
|
||||||
backgroundImage: `url(${
|
link={`/recipes/${recipe.id}`}
|
||||||
recipe?.meta?.image || "/api/recipes/images/placeholder.jpg"
|
/>
|
||||||
}?width=200&height=200)`,
|
|
||||||
backgroundSize: "cover",
|
|
||||||
}}
|
|
||||||
class="bg-gray-900 text-white rounded-3xl shadow-md p-4 relative overflow-hidden
|
|
||||||
lg:w-56 lg:h-56
|
|
||||||
sm:w-40 sm:h-40
|
|
||||||
w-32 h-32"
|
|
||||||
>
|
|
||||||
<div class="h-full flex flex-col justify-between relative z-10">
|
|
||||||
<div>
|
|
||||||
{/* Recipe Card content */}
|
|
||||||
</div>
|
|
||||||
<div class="mt-2">
|
|
||||||
{recipe.name}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="absolute inset-x-0 bottom-0 h-3/4 bg-gradient-to-t from-black to-transparent" />
|
|
||||||
</a>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,10 +1,16 @@
|
|||||||
import { Recipe } from "../lib/recipes.ts";
|
import { Recipe } from "@lib/recipes.ts";
|
||||||
|
|
||||||
export function RecipeHero({ recipe }: { recipe: Recipe }) {
|
export function RecipeHero({ recipe }: { recipe: Recipe }) {
|
||||||
|
const { meta: { image = "Recipes/images/placeholder.jpg" } = {} } = recipe;
|
||||||
|
|
||||||
|
const imageUrl = image.startsWith("Recipes/images/")
|
||||||
|
? `/api/images?image=${image}&width=800`
|
||||||
|
: image;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class="relative w-full h-[400px] rounded-3xl overflow-hidden bg-black">
|
<div class="relative w-full h-[400px] rounded-3xl overflow-hidden bg-black">
|
||||||
<img
|
<img
|
||||||
src={recipe?.meta?.image + "?width=800"}
|
src={imageUrl}
|
||||||
alt="Recipe Banner"
|
alt="Recipe Banner"
|
||||||
class="object-cover w-full h-full"
|
class="object-cover w-full h-full"
|
||||||
/>
|
/>
|
||||||
@ -18,11 +24,11 @@ export function RecipeHero({ recipe }: { recipe: Recipe }) {
|
|||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div class="absolute inset-x-0 bottom-0 py-4 px-8 py-8 noisy-gradient">
|
||||||
class="absolute inset-x-0 bottom-0 py-4 px-8 py-8"
|
<h2
|
||||||
style={{ background: "linear-gradient(#0000, #fff9)" }}
|
class="relative text-4xl font-bold mt-4 z-10"
|
||||||
>
|
style={{ color: "#1F1F1F" }}
|
||||||
<h2 class="text-4xl font-bold mt-4" style={{ color: "#1F1F1F" }}>
|
>
|
||||||
{recipe.name}
|
{recipe.name}
|
||||||
</h2>
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
"lock": false,
|
"lock": false,
|
||||||
"tasks": {
|
"tasks": {
|
||||||
"start": "deno run -A --watch=static/,routes/ dev.ts",
|
"start": "deno run -A --watch=static/,routes/ dev.ts",
|
||||||
|
"debug": "deno run --inspect-wait -A main.ts",
|
||||||
"update": "deno run -A -r https://fresh.deno.dev/update ."
|
"update": "deno run -A -r https://fresh.deno.dev/update ."
|
||||||
},
|
},
|
||||||
"lint": {
|
"lint": {
|
||||||
|
12
fresh.gen.ts
12
fresh.gen.ts
@ -4,9 +4,9 @@
|
|||||||
|
|
||||||
import * as $0 from "./routes/_404.tsx";
|
import * as $0 from "./routes/_404.tsx";
|
||||||
import * as $1 from "./routes/_app.tsx";
|
import * as $1 from "./routes/_app.tsx";
|
||||||
import * as $2 from "./routes/api/index.ts";
|
import * as $2 from "./routes/api/images/index.ts";
|
||||||
import * as $3 from "./routes/api/recipes/[name].ts";
|
import * as $3 from "./routes/api/index.ts";
|
||||||
import * as $4 from "./routes/api/recipes/images/[image].ts";
|
import * as $4 from "./routes/api/recipes/[name].ts";
|
||||||
import * as $5 from "./routes/api/recipes/index.ts";
|
import * as $5 from "./routes/api/recipes/index.ts";
|
||||||
import * as $6 from "./routes/index.tsx";
|
import * as $6 from "./routes/index.tsx";
|
||||||
import * as $7 from "./routes/recipes/[name].tsx";
|
import * as $7 from "./routes/recipes/[name].tsx";
|
||||||
@ -17,9 +17,9 @@ const manifest = {
|
|||||||
routes: {
|
routes: {
|
||||||
"./routes/_404.tsx": $0,
|
"./routes/_404.tsx": $0,
|
||||||
"./routes/_app.tsx": $1,
|
"./routes/_app.tsx": $1,
|
||||||
"./routes/api/index.ts": $2,
|
"./routes/api/images/index.ts": $2,
|
||||||
"./routes/api/recipes/[name].ts": $3,
|
"./routes/api/index.ts": $3,
|
||||||
"./routes/api/recipes/images/[image].ts": $4,
|
"./routes/api/recipes/[name].ts": $4,
|
||||||
"./routes/api/recipes/index.ts": $5,
|
"./routes/api/recipes/index.ts": $5,
|
||||||
"./routes/index.tsx": $6,
|
"./routes/index.tsx": $6,
|
||||||
"./routes/recipes/[name].tsx": $7,
|
"./routes/recipes/[name].tsx": $7,
|
||||||
|
29
lib/cache.ts
29
lib/cache.ts
@ -1,29 +0,0 @@
|
|||||||
import { connect } from "https://deno.land/x/redis/mod.ts";
|
|
||||||
|
|
||||||
const REDIS_HOST = Deno.env.get("REDIS_HOST");
|
|
||||||
const REDIS_PASS = Deno.env.get("REDIS_PASS");
|
|
||||||
const REDIS_PORT = Deno.env.get("REDIS_PORT");
|
|
||||||
|
|
||||||
async function createCache() {
|
|
||||||
if (REDIS_HOST && REDIS_PASS) {
|
|
||||||
const client = await connect({
|
|
||||||
password: REDIS_PASS,
|
|
||||||
hostname: REDIS_HOST,
|
|
||||||
port: REDIS_PORT || 6379,
|
|
||||||
});
|
|
||||||
console.log("COnnected to redis");
|
|
||||||
return client;
|
|
||||||
}
|
|
||||||
|
|
||||||
return new Map<string, any>();
|
|
||||||
}
|
|
||||||
|
|
||||||
const cache = await createCache();
|
|
||||||
|
|
||||||
export async function get(id: string) {
|
|
||||||
return await cache.get(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function set(id: string, content: any) {
|
|
||||||
return await cache.set(id, content);
|
|
||||||
}
|
|
42
lib/cache/cache.ts
vendored
Normal file
42
lib/cache/cache.ts
vendored
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
import {
|
||||||
|
connect,
|
||||||
|
Redis,
|
||||||
|
RedisConnectOptions,
|
||||||
|
RedisValue,
|
||||||
|
} from "https://deno.land/x/redis@v0.31.0/mod.ts";
|
||||||
|
|
||||||
|
const REDIS_HOST = Deno.env.get("REDIS_HOST");
|
||||||
|
const REDIS_PASS = Deno.env.get("REDIS_PASS") || "";
|
||||||
|
const REDIS_PORT = Deno.env.get("REDIS_PORT");
|
||||||
|
|
||||||
|
async function createCache<T>(): Promise<Map<string, T> | Redis> {
|
||||||
|
if (REDIS_HOST) {
|
||||||
|
const conf: RedisConnectOptions = {
|
||||||
|
hostname: REDIS_HOST,
|
||||||
|
port: REDIS_PORT || 6379,
|
||||||
|
};
|
||||||
|
if (REDIS_PASS) {
|
||||||
|
conf.password = REDIS_PASS;
|
||||||
|
}
|
||||||
|
const client = await connect(conf);
|
||||||
|
console.log("Connected to redis");
|
||||||
|
return client;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Map<string, T>();
|
||||||
|
}
|
||||||
|
|
||||||
|
const cache = await createCache();
|
||||||
|
|
||||||
|
export async function get<T>(id: string, binary = false) {
|
||||||
|
if (binary && !(cache instanceof Map)) {
|
||||||
|
return await cache.sendCommand("GET", [id], {
|
||||||
|
returnUint8Arrays: true,
|
||||||
|
}) as T;
|
||||||
|
}
|
||||||
|
return await cache.get(id) as T;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function set<T extends RedisValue>(id: string, content: T) {
|
||||||
|
return await cache.set(id, content);
|
||||||
|
}
|
57
lib/cache/documents.ts
vendored
Normal file
57
lib/cache/documents.ts
vendored
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
import { Document } from "@lib/documents.ts";
|
||||||
|
import * as cache from "@lib/cache/cache.ts";
|
||||||
|
|
||||||
|
type DocumentsCache = {
|
||||||
|
lastUpdated: number;
|
||||||
|
documents: Document[];
|
||||||
|
};
|
||||||
|
|
||||||
|
const CACHE_INTERVAL = 5000; // 5 seconds;
|
||||||
|
const CACHE_KEY = "documents";
|
||||||
|
|
||||||
|
export async function getDocuments() {
|
||||||
|
const docs = await cache.get<DocumentsCache>(CACHE_KEY);
|
||||||
|
if (!docs) return;
|
||||||
|
|
||||||
|
if (Date.now() > docs.lastUpdated + CACHE_INTERVAL) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return docs.documents;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setDocuments(documents: Document[]) {
|
||||||
|
return cache.set(
|
||||||
|
CACHE_KEY,
|
||||||
|
JSON.stringify({
|
||||||
|
lastUpdated: Date.now(),
|
||||||
|
documents,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
type DocumentCache = {
|
||||||
|
lastUpdated: number;
|
||||||
|
content: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function getDocument(id: string) {
|
||||||
|
const doc = await cache.get<DocumentCache>(CACHE_KEY + "/" + id);
|
||||||
|
if (!doc) return;
|
||||||
|
|
||||||
|
if (Date.now() > doc.lastUpdated + CACHE_INTERVAL) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return doc.content;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function setDocument(id: string, content: string) {
|
||||||
|
await cache.set(
|
||||||
|
CACHE_KEY + "/" + id,
|
||||||
|
JSON.stringify({
|
||||||
|
lastUpdated: Date.now(),
|
||||||
|
content,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
63
lib/cache/image.ts
vendored
Normal file
63
lib/cache/image.ts
vendored
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
import { hash } from "@lib/hash.ts";
|
||||||
|
import * as cache from "@lib/cache/cache.ts";
|
||||||
|
|
||||||
|
type ImageCacheOptions = {
|
||||||
|
url: string;
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
mediaType?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const CACHE_KEY = "images";
|
||||||
|
|
||||||
|
function getCacheKey({ url: _url, width, height }: ImageCacheOptions) {
|
||||||
|
const url = new URL(_url);
|
||||||
|
|
||||||
|
return `${CACHE_KEY}/${url.hostname}/${url.pathname}/${width}/${height}`
|
||||||
|
.replace(
|
||||||
|
"//",
|
||||||
|
"/",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getImage({ url, width, height }: ImageCacheOptions) {
|
||||||
|
const cacheKey = getCacheKey({ url, width, height });
|
||||||
|
|
||||||
|
const pointerCacheRaw = await cache.get<string>(cacheKey);
|
||||||
|
if (!pointerCacheRaw) return;
|
||||||
|
|
||||||
|
const pointerCache = typeof pointerCacheRaw === "string"
|
||||||
|
? JSON.parse(pointerCacheRaw)
|
||||||
|
: pointerCacheRaw;
|
||||||
|
|
||||||
|
const imageContent = await cache.get(pointerCache.id, true);
|
||||||
|
if (!imageContent) return;
|
||||||
|
|
||||||
|
return {
|
||||||
|
...pointerCache,
|
||||||
|
buffer: imageContent,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function setImage(
|
||||||
|
buffer: Uint8Array,
|
||||||
|
{ url, width, height, mediaType }: ImageCacheOptions,
|
||||||
|
) {
|
||||||
|
const clone = new Uint8Array(buffer);
|
||||||
|
|
||||||
|
const cacheKey = getCacheKey({ url, width, height });
|
||||||
|
const pointerId = await hash(cacheKey);
|
||||||
|
|
||||||
|
await cache.set(pointerId, clone);
|
||||||
|
|
||||||
|
await cache.set(
|
||||||
|
cacheKey,
|
||||||
|
JSON.stringify({
|
||||||
|
id: pointerId,
|
||||||
|
url,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
mediaType,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
@ -2,6 +2,7 @@ import { unified } from "npm:unified";
|
|||||||
import remarkParse from "npm:remark-parse";
|
import remarkParse from "npm:remark-parse";
|
||||||
import remarkFrontmatter from "https://esm.sh/remark-frontmatter@4";
|
import remarkFrontmatter from "https://esm.sh/remark-frontmatter@4";
|
||||||
import { parse } from "https://deno.land/std@0.194.0/yaml/mod.ts";
|
import { parse } from "https://deno.land/std@0.194.0/yaml/mod.ts";
|
||||||
|
import * as cache from "@lib/cache/documents.ts";
|
||||||
|
|
||||||
const SILVERBULLET_SERVER = Deno.env.get("SILVERBULLET_SERVER");
|
const SILVERBULLET_SERVER = Deno.env.get("SILVERBULLET_SERVER");
|
||||||
|
|
||||||
@ -18,6 +19,9 @@ export function parseFrontmatter(yaml: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function getDocuments(): Promise<Document[]> {
|
export async function getDocuments(): Promise<Document[]> {
|
||||||
|
const cachedDocuments = await cache.getDocuments();
|
||||||
|
if (cachedDocuments) return cachedDocuments;
|
||||||
|
|
||||||
const headers = new Headers();
|
const headers = new Headers();
|
||||||
headers.append("Accept", "application/json");
|
headers.append("Accept", "application/json");
|
||||||
|
|
||||||
@ -25,12 +29,22 @@ export async function getDocuments(): Promise<Document[]> {
|
|||||||
headers: headers,
|
headers: headers,
|
||||||
});
|
});
|
||||||
|
|
||||||
return response.json();
|
const documents = await response.json();
|
||||||
|
cache.setDocuments(documents);
|
||||||
|
|
||||||
|
return documents;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getDocument(name: string): Promise<string> {
|
export async function getDocument(name: string): Promise<string> {
|
||||||
|
const cachedDocument = await cache.getDocument(name);
|
||||||
|
if (cachedDocument) return cachedDocument;
|
||||||
|
|
||||||
const response = await fetch(SILVERBULLET_SERVER + "/" + name);
|
const response = await fetch(SILVERBULLET_SERVER + "/" + name);
|
||||||
return await response.text();
|
const text = await response.text();
|
||||||
|
|
||||||
|
cache.setDocument(name, text);
|
||||||
|
|
||||||
|
return text;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function parseDocument(doc: string) {
|
export function parseDocument(doc: string) {
|
||||||
|
9
lib/hash.ts
Normal file
9
lib/hash.ts
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
export async function hash(message: string) {
|
||||||
|
const data = new TextEncoder().encode(message);
|
||||||
|
const hashBuffer = await crypto.subtle.digest("SHA-256", data);
|
||||||
|
const hashArray = Array.from(new Uint8Array(hashBuffer));
|
||||||
|
const hashHex = hashArray.map((b) => b.toString(16).padStart(2, "0")).join(
|
||||||
|
"",
|
||||||
|
);
|
||||||
|
return hashHex;
|
||||||
|
}
|
@ -4,7 +4,7 @@ import {
|
|||||||
getTextOfRange,
|
getTextOfRange,
|
||||||
parseDocument,
|
parseDocument,
|
||||||
parseFrontmatter,
|
parseFrontmatter,
|
||||||
} from "./documents.ts";
|
} from "@lib/documents.ts";
|
||||||
|
|
||||||
import { parseIngredient } from "npm:parse-ingredient";
|
import { parseIngredient } from "npm:parse-ingredient";
|
||||||
|
|
||||||
@ -161,10 +161,7 @@ export function parseRecipe(original: string, id: string): Recipe {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
id,
|
id,
|
||||||
meta: {
|
meta,
|
||||||
...meta,
|
|
||||||
image: meta?.image?.replace(/^Recipes\/images/, "/api/recipes/images"),
|
|
||||||
},
|
|
||||||
name,
|
name,
|
||||||
description,
|
description,
|
||||||
ingredients,
|
ingredients,
|
||||||
|
@ -5,7 +5,7 @@ import {
|
|||||||
MagickGeometry,
|
MagickGeometry,
|
||||||
} from "https://deno.land/x/imagemagick_deno@0.0.14/mod.ts";
|
} from "https://deno.land/x/imagemagick_deno@0.0.14/mod.ts";
|
||||||
import { parseMediaType } from "https://deno.land/std@0.175.0/media_types/parse_media_type.ts";
|
import { parseMediaType } from "https://deno.land/std@0.175.0/media_types/parse_media_type.ts";
|
||||||
import * as cache from "@lib/cache.ts";
|
import * as cache from "@lib/cache/image.ts";
|
||||||
|
|
||||||
await initializeImageMagick();
|
await initializeImageMagick();
|
||||||
|
|
||||||
@ -56,6 +56,11 @@ function modifyImage(
|
|||||||
}
|
}
|
||||||
|
|
||||||
function parseParams(reqUrl: URL) {
|
function parseParams(reqUrl: URL) {
|
||||||
|
const image = reqUrl.searchParams.get("image")?.replace(/^\//, "");
|
||||||
|
if (image == null) {
|
||||||
|
return "Missing 'image' query parameter.";
|
||||||
|
}
|
||||||
|
|
||||||
const height = Number(reqUrl.searchParams.get("height")) || 0;
|
const height = Number(reqUrl.searchParams.get("height")) || 0;
|
||||||
const width = Number(reqUrl.searchParams.get("width")) || 0;
|
const width = Number(reqUrl.searchParams.get("width")) || 0;
|
||||||
if (height === 0 && width === 0) {
|
if (height === 0 && width === 0) {
|
||||||
@ -69,50 +74,36 @@ function parseParams(reqUrl: URL) {
|
|||||||
return `Width and height cannot exceed ${maxDimension}.`;
|
return `Width and height cannot exceed ${maxDimension}.`;
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
|
image,
|
||||||
height,
|
height,
|
||||||
width,
|
width,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getImageResponse(
|
|
||||||
remoteImage: { buffer: Uint8Array; mediaType: string },
|
|
||||||
params: { width: number; height: number },
|
|
||||||
): Promise<Response> {
|
|
||||||
const modifiedImage = await modifyImage(remoteImage.buffer, {
|
|
||||||
...params,
|
|
||||||
mode: "resize",
|
|
||||||
});
|
|
||||||
|
|
||||||
const response = new Response(modifiedImage, {
|
|
||||||
headers: {
|
|
||||||
"Content-Type": remoteImage.mediaType,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return response;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const handler = async (
|
export const handler = async (
|
||||||
_req: Request,
|
_req: Request,
|
||||||
_ctx: HandlerContext,
|
_ctx: HandlerContext,
|
||||||
): Promise<Response> => {
|
): Promise<Response> => {
|
||||||
const imageUrl = Deno.env.get("SILVERBULLET_SERVER") + "/Recipes/images/" +
|
|
||||||
_ctx.params.image;
|
|
||||||
|
|
||||||
const url = new URL(_req.url);
|
const url = new URL(_req.url);
|
||||||
|
|
||||||
const params = parseParams(url);
|
const params = parseParams(url);
|
||||||
|
|
||||||
if (typeof params === "string") {
|
if (typeof params === "string") {
|
||||||
return new Response(params, { status: 400 });
|
return new Response(params, { status: 400 });
|
||||||
}
|
}
|
||||||
|
|
||||||
const imageId = `${imageUrl}.${params.width}.${params.height}`;
|
const imageUrl = Deno.env.get("SILVERBULLET_SERVER") + "/" + params.image;
|
||||||
const cachedResponse = await cache.get(imageId);
|
|
||||||
|
const cachedResponse = await cache.getImage({
|
||||||
|
url: imageUrl,
|
||||||
|
width: params.width,
|
||||||
|
height: params.height,
|
||||||
|
});
|
||||||
if (cachedResponse) {
|
if (cachedResponse) {
|
||||||
const _r = await cachedResponse;
|
return new Response(cachedResponse.buffer.slice(), {
|
||||||
console.log({ cachedResponse, _r });
|
headers: {
|
||||||
return (await cachedResponse).clone();
|
"Content-Type": cachedResponse.mediaType,
|
||||||
|
},
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const remoteImage = await getRemoteImage(imageUrl);
|
const remoteImage = await getRemoteImage(imageUrl);
|
||||||
@ -120,9 +111,21 @@ export const handler = async (
|
|||||||
return new Response(remoteImage, { status: 400 });
|
return new Response(remoteImage, { status: 400 });
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = getImageResponse(remoteImage, params);
|
const modifiedImage = await modifyImage(remoteImage.buffer, {
|
||||||
|
...params,
|
||||||
|
mode: "resize",
|
||||||
|
});
|
||||||
|
|
||||||
await cache.set(imageId, response);
|
cache.setImage(modifiedImage, {
|
||||||
|
url: imageUrl,
|
||||||
|
width: params.width,
|
||||||
|
height: params.height,
|
||||||
|
mediaType: remoteImage.mediaType,
|
||||||
|
});
|
||||||
|
|
||||||
return response;
|
return new Response(modifiedImage, {
|
||||||
|
headers: {
|
||||||
|
"Content-Type": remoteImage.mediaType,
|
||||||
|
},
|
||||||
|
});
|
||||||
};
|
};
|
@ -1,6 +1,8 @@
|
|||||||
import { Head } from "$fresh/runtime.ts";
|
import { Head } from "$fresh/runtime.ts";
|
||||||
import { useSignal } from "@preact/signals";
|
import { useSignal } from "@preact/signals";
|
||||||
import Counter from "@islands/Counter.tsx";
|
import Counter from "@islands/Counter.tsx";
|
||||||
|
import { MainLayout } from "@components/layouts/main.tsx";
|
||||||
|
import { Card } from "@components/Card.tsx";
|
||||||
|
|
||||||
export default function Home() {
|
export default function Home() {
|
||||||
const count = useSignal(3);
|
const count = useSignal(3);
|
||||||
@ -9,23 +11,15 @@ export default function Home() {
|
|||||||
<Head>
|
<Head>
|
||||||
<title>app</title>
|
<title>app</title>
|
||||||
</Head>
|
</Head>
|
||||||
<div class="px-4 py-8 mx-auto bg-[#86efac]">
|
<MainLayout>
|
||||||
<div class="max-w-screen-md mx-auto flex flex-col items-center justify-center">
|
<div class="flex flex-wrap justify-center items-center gap-4 px-4">
|
||||||
<img
|
<Card
|
||||||
class="my-6"
|
title="Recipes"
|
||||||
src="/logo.svg"
|
image="/api/images?image=Recipes/images/placeholder.jpg&width=200&height=200"
|
||||||
width="128"
|
link="/recipes"
|
||||||
height="128"
|
|
||||||
alt="the fresh logo: a sliced lemon dripping with juice"
|
|
||||||
/>
|
/>
|
||||||
<h1 class="text-4xl font-bold">Welcome to fresh</h1>
|
|
||||||
<p class="my-4">
|
|
||||||
Try updating this message in the
|
|
||||||
<code class="mx-2">./routes/index.tsx</code> file, and refresh.
|
|
||||||
</p>
|
|
||||||
<Counter count={count} />
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</MainLayout>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -3,7 +3,7 @@ import { RecipeCard } from "@components/RecipeCard.tsx";
|
|||||||
import { MainLayout } from "@components/layouts/main.tsx";
|
import { MainLayout } from "@components/layouts/main.tsx";
|
||||||
import { Recipe } from "@lib/recipes.ts";
|
import { Recipe } from "@lib/recipes.ts";
|
||||||
import { getRecipes } from "../api/recipes/index.ts";
|
import { getRecipes } from "../api/recipes/index.ts";
|
||||||
|
import IconArrowLeft from "https://deno.land/x/tabler_icons_tsx@0.0.3/tsx/arrow-left.tsx";
|
||||||
export const handler: Handlers<Recipe[] | null> = {
|
export const handler: Handlers<Recipe[] | null> = {
|
||||||
async GET(_, ctx) {
|
async GET(_, ctx) {
|
||||||
const recipes = await getRecipes();
|
const recipes = await getRecipes();
|
||||||
@ -14,6 +14,17 @@ export const handler: Handlers<Recipe[] | null> = {
|
|||||||
export default function Greet(props: PageProps<Recipe[] | null>) {
|
export default function Greet(props: PageProps<Recipe[] | null>) {
|
||||||
return (
|
return (
|
||||||
<MainLayout>
|
<MainLayout>
|
||||||
|
<header class="flex gap-4 items-center mb-5">
|
||||||
|
<a
|
||||||
|
class="px-4 ml-4 py-2 bg-gray-300 text-gray-800 rounded-lg flex items-center gap-1"
|
||||||
|
href="/"
|
||||||
|
>
|
||||||
|
<IconArrowLeft class="w-5 h-5" />
|
||||||
|
Back
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<h3 class="text-2xl text-white font-light">Recipes</h3>
|
||||||
|
</header>
|
||||||
<div class="flex flex-wrap justify-center items-center gap-4 px-4">
|
<div class="flex flex-wrap justify-center items-center gap-4 px-4">
|
||||||
{props.data?.map((doc) => {
|
{props.data?.map((doc) => {
|
||||||
return <RecipeCard recipe={doc} />;
|
return <RecipeCard recipe={doc} />;
|
||||||
|
@ -1,3 +1,17 @@
|
|||||||
body {
|
body {
|
||||||
background: #1F1F1F
|
background: #1F1F1F;
|
||||||
|
padding: 0px 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.noisy-gradient::after {
|
||||||
|
content: "";
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
z-index:0;
|
||||||
|
position: absolute;
|
||||||
|
opacity: 0.7;
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
background: url(/grainy-gradient.png);
|
||||||
|
background-size: contain;
|
||||||
}
|
}
|
||||||
|
BIN
static/grainy-gradient.png
Normal file
BIN
static/grainy-gradient.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 30 KiB |
Loading…
x
Reference in New Issue
Block a user