Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions www/blog/2025-02-02-welcome-to-effection-blog/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ title: "Welcome to the Effection Blog"
description: "Introducing the new Effection blog - your source for tutorials, release announcements, and insights about structured concurrency in JavaScript."
author: "Taras Mankovski"
tags: ["announcement", "effection"]
image: meta-effection.png
---

Welcome to the official Effection blog! This is where we'll share tutorials,
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 2 additions & 1 deletion www/deno.json
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@
"hast-util-select": "npm:hast-util-select@6.0.1",
"unist-util-visit": "npm:unist-util-visit@5.0.0",
"vfile": "npm:vfile@6.0.3",
"zod": "npm:zod@3.23.8"
"zod": "npm:zod@3.23.8",
"@resvg/resvg-wasm": "npm:@resvg/resvg-wasm@2.6.2"
}
}
6 changes: 6 additions & 0 deletions www/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,12 @@ import { initJSRClient } from "./context/jsr.ts";
import { initWorktrees } from "./lib/worktrees.ts";
import { initGuides } from "./resources/guides.ts";
import { initBlog } from "./resources/blog.ts";
import { initFonts } from "./resources/fonts.ts";
import { initImageStore } from "./resources/image-store.ts";
import { apiIndexRoute } from "./routes/api-index-route.tsx";
import { blogIndexRoute } from "./routes/blog-index-route.tsx";
import { blogPostRoute } from "./routes/blog-post-route.tsx";
import { blogImageRoute } from "./routes/blog-image-route.ts";
import { blogTagRoute } from "./routes/blog-tag-route.tsx";
import { blogFeedRoute } from "./routes/blog-feed-route.tsx";
import { llmsTxtRoute } from "./routes/llms-txt-route.ts";
Expand Down Expand Up @@ -51,6 +54,8 @@ if (import.meta.main) {
});

yield* initBlog();
yield* initFonts();
yield* initImageStore();

yield* initJSRClient();
yield* initFetch();
Expand Down Expand Up @@ -86,6 +91,7 @@ if (import.meta.main) {
route("/llms.txt", llmsTxtRoute()),
route("/blog/tags/:tag", blogTagRoute({ search: true })),
route("/blog/:id", blogPostRoute({ search: true })),
route("/blog/:id/:name.png", blogImageRoute()),
route("/blog{/*path}", assetsRoute("blog")),
route(
"/pagefind{/*path}",
Expand Down
14 changes: 5 additions & 9 deletions www/plugins/current-request.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,16 +25,12 @@ export function* useAbsoluteUrl(path: string = "/"): Operation<string> {
export function* useAbsoluteUrlFactory(): Operation<(path: string) => string> {
let request = yield* CurrentRequest.expect();

let origin = new URL(request.url).origin;

return (path) => {
let normalizedPath = posixNormalize(path);
if (normalizedPath.startsWith("/")) {
let url = new URL(request.url);
url.pathname = normalizedPath;
url.search = "";
return url.toString();
} else {
return new URL(path, request.url).toString();
}
let url = new URL(path, origin);
url.pathname = posixNormalize(url.pathname);
return url.toString();
};
}

Expand Down
21 changes: 11 additions & 10 deletions www/resources/blog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import rehypePrismPlus from "rehype-prism-plus";
import rehypeSlug from "rehype-slug";
import rehypeAddClasses from "rehype-add-classes";
import rehypeAutolinkHeadings from "rehype-autolink-headings";
import z from "zod";

export interface Blog {
get(id: string): BlogPost | undefined;
Expand All @@ -23,20 +24,20 @@ export interface BlogPost {
id: string;
title: string;
description: string;
image: string | undefined;
image: string;
date: Date;
author: string;
tags: string[];
content: () => JSXElement;
}

interface Frontmatter {
title: string;
description: string;
author: string;
tags: string[];
image?: string;
}
let Frontmatter = z.object({
title: z.string(),
description: z.string(),
author: z.string(),
tags: z.array(z.string()).default([]),
image: z.string(),
});

const BlogContext = createContext<Blog>("blog");

Expand Down Expand Up @@ -129,14 +130,14 @@ function* loadBlog(): Operation<Blog> {
})
);

let frontmatter = mod.frontmatter as Frontmatter;
let frontmatter = Frontmatter.parse(mod.frontmatter);
let post: BlogPost = {
id,
date,
title: frontmatter.title,
description: frontmatter.description,
author: frontmatter.author,
tags: frontmatter.tags ?? [],
tags: frontmatter.tags,
image: frontmatter.image,
content: () => mod.default({}) as JSXElement,
};
Expand Down
47 changes: 47 additions & 0 deletions www/resources/fonts.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { all, createContext, type Operation, until } from "effection";

export interface Fonts {
buffers: Uint8Array[];
defaultFamily: string;
monospaceFamily: string;
}

const FontsContext = createContext<Fonts>("fonts");

export function* initFonts(): Operation<void> {
let buffers = yield* all([
// Proxima Nova: 400, 700, 800
fetchFont(
"https://use.typekit.net/af/efe4a5/00000000000000007735e609/30/l?primer=7cdcb44be4a7db8877ffa5c0007b8dd865b3bbc383831fe2ea177f62257a9191&fvd=n4&v=3",
),
fetchFont(
"https://use.typekit.net/af/2555e1/00000000000000007735e603/30/l?primer=7cdcb44be4a7db8877ffa5c0007b8dd865b3bbc383831fe2ea177f62257a9191&fvd=n7&v=3",
),
fetchFont(
"https://use.typekit.net/af/8738d8/00000000000000007735e611/30/l?primer=7cdcb44be4a7db8877ffa5c0007b8dd865b3bbc383831fe2ea177f62257a9191&fvd=n8&v=3",
),
// JetBrains Mono: 400, 600
fetchFont(
"https://fonts.gstatic.com/s/jetbrainsmono/v24/tDbY2o-flEEny0FZhsfKu5WU4zr3E_BX0PnT8RD8yKxjPQ.ttf",
),
fetchFont(
"https://fonts.gstatic.com/s/jetbrainsmono/v24/tDbY2o-flEEny0FZhsfKu5WU4zr3E_BX0PnT8RD8FqtjPQ.ttf",
),
]);

yield* FontsContext.set({
buffers,
defaultFamily: "proxima-nova",
monospaceFamily: "JetBrains Mono",
});
}

export function* useFonts(): Operation<Fonts> {
return yield* FontsContext.expect();
}

function* fetchFont(url: string): Operation<Uint8Array> {
let response = yield* until(fetch(url));
let buffer = yield* until(response.arrayBuffer());
return new Uint8Array(buffer);
}
52 changes: 52 additions & 0 deletions www/resources/image-store.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { createContext, type Operation, until } from "effection";
import { initWasm, Resvg } from "@resvg/resvg-wasm";
import { readFile } from "node:fs/promises";
import { createRequire } from "node:module";
import { useFonts } from "./fonts.ts";

export interface ImageStore {
render(svg: string, width: number, key: string): Uint8Array;
}

const ImageStoreContext = createContext<ImageStore>("image-store");

export function* initImageStore(): Operation<void> {
let existing = yield* ImageStoreContext.get();
if (existing) {
throw new Error(
"initImageStore() called more than once — resvg wasm can only be initialized once per process",
);
}

let require = createRequire(import.meta.url);
let wasmPath = require.resolve("@resvg/resvg-wasm/index_bg.wasm");
let wasm = yield* until(readFile(wasmPath));
yield* until(initWasm(wasm));

let fonts = yield* useFonts();
let cache = new Map<string, Uint8Array>();

yield* ImageStoreContext.set({
render(svg, width, key) {
let cached = cache.get(key);
if (cached) {
return cached;
}
let resvg = new Resvg(svg, {
font: {
fontBuffers: fonts.buffers,
defaultFontFamily: fonts.defaultFamily,
monospaceFamily: fonts.monospaceFamily,
},
fitTo: { mode: "width", value: width },
});
let png = resvg.render().asPng();
cache.set(key, png);
return png;
},
});
}

export function* useImageStore(): Operation<ImageStore> {
return yield* ImageStoreContext.expect();
}
10 changes: 5 additions & 5 deletions www/routes/app.html.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export type Options = {
title: string;
description: string;
head?: JSXElement;
image?: string;
} & HeaderProps;

export interface AppHtmlProps {
Expand All @@ -22,10 +23,9 @@ export function* useAppHtml({
description,
hasLeftSidebar,
head,
image = "/assets/images/meta-effection.png",
}: Options): Operation<({ children, search }: AppHtmlProps) => JSX.Element> {
let twitterImageURL = yield* useAbsoluteUrl(
"/assets/images/meta-effection.png",
);
let ogImageURL = yield* useAbsoluteUrl(image);

let canonicalURL = yield* useCanonicalUrl({
base: "https://frontside.com/effection",
Expand All @@ -38,12 +38,12 @@ export function* useAppHtml({
<head>
<meta charset="UTF-8" />
<title>{title}</title>
<meta property="og:image" content={twitterImageURL} />
<meta property="og:image" content={ogImageURL} />
<meta property="og:title" content={title} data-rh="true" />
<meta property="og:url" content={canonicalURL} />
<meta property="og:description" content={description} />
<meta name="description" content={description} />
<meta name="twitter:image" content={twitterImageURL} />
<meta name="twitter:image" content={ogImageURL} />
<link rel="icon" href="/assets/images/favicon-effection.png" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="canonical" href={canonicalURL} />
Expand Down
80 changes: 80 additions & 0 deletions www/routes/blog-image-route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import type { Operation } from "effection";
import { call } from "effection";
import { respondNotFound, useParams } from "revolution";
import { CurrentRequest } from "../context/request.ts";
import { useImageStore } from "../resources/image-store.ts";

export function blogImageRoute(): {
handler(): Operation<Response>;
} {
return {
*handler() {
let { id, name } = yield* useParams<{ id: string; name: string }>();
let request = yield* CurrentRequest.expect();
let url = new URL(request.url);

let blogDir = new URL(`../blog/${id}/`, import.meta.url).pathname;
let pngPath = `${blogDir}${name}.png`;

// if a static .png exists, serve it directly
try {
let png = yield* call(() => Deno.readFile(pngPath));
return pngResponse(png);
} catch {
// no static png, continue
}

let w = url.searchParams.get("w");
let h = url.searchParams.get("h");

// no static png and no dimensions: 404
if (!w || !h) {
return yield* respondNotFound();
}

let width = parseInt(w, 10);
let height = parseInt(h, 10);

if (isNaN(width) || isNaN(height) || width <= 0 || height <= 0) {
return yield* respondNotFound();
}

let svgPath = `${blogDir}${name}.svg`;

let svg: string;
try {
svg = yield* call(() => Deno.readTextFile(svgPath));
} catch {
return yield* respondNotFound();
}

let store = yield* useImageStore();

svg = stripAnimations(svg);

let key = `${id}/${name}/${width}x${height}`;
let png = store.render(svg, width, key);

return pngResponse(png);
},
};
}

function pngResponse(png: Uint8Array): Response {
return new Response(png, {
headers: {
"Content-Type": "image/png",
"Cache-Control": "public, max-age=31536000, immutable",
},
});
}

function stripAnimations(svg: string): string {
return svg.replace(
/\.svg-anim-\w+\s*\{[^}]*opacity:\s*0[^}]*\}/g,
(match) =>
match
.replace(/opacity:\s*0/, "opacity: 1")
.replace(/animation:[^;}]+;?/g, ""),
);
}
3 changes: 3 additions & 0 deletions www/routes/blog-post-route.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,9 @@ export function blogPostRoute({
let AppHtml = yield* useAppHtml({
title: `${post.title} | Blog | Effection`,
description: post.description,
image: `/blog/${post.id}/${
post.image.replace(/\.svg$/, ".png")
}?w=2400&h=1260`,
});

return (
Expand Down
Loading