diff --git a/.gitignore b/.gitignore index e4451c4..afc9a28 100644 --- a/.gitignore +++ b/.gitignore @@ -10,6 +10,9 @@ # production /build +/dist +/public/blog/assets/featured +/public/blog/assets/gen # misc .DS_Store @@ -21,4 +24,3 @@ npm-debug.log* yarn-debug.log* yarn-error.log* -dist diff --git a/package.json b/package.json index bf96557..eee4784 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ "classnames": "^2.3.1", "date-fns": "^2.28.0", "framer-motion": "^6.3.15", + "gm": "^1.23.1", "imagen": "github:MKRhere/imagen", "marked": "^4.0.17", "react": "^18.2.0", @@ -27,6 +28,7 @@ }, "devDependencies": { "@svgr/rollup": "^6.2.1", + "@types/gm": "^1.18.12", "@types/marked": "^4.0.3", "@types/node": "^18.0.0", "@types/react": "^18.0.14", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2ab3dc1..0c36e49 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -3,6 +3,7 @@ lockfileVersion: 5.4 specifiers: '@emotion/css': ^11.9.0 '@svgr/rollup': ^6.2.1 + '@types/gm': ^1.18.12 '@types/marked': ^4.0.3 '@types/node': ^18.0.0 '@types/react': ^18.0.14 @@ -12,6 +13,7 @@ specifiers: classnames: ^2.3.1 date-fns: ^2.28.0 framer-motion: ^6.3.15 + gm: ^1.23.1 imagen: github:MKRhere/imagen marked: ^4.0.17 react: ^18.2.0 @@ -26,6 +28,7 @@ dependencies: classnames: 2.3.1 date-fns: 2.28.0 framer-motion: 6.3.15_biqbaboplfbrettd7655fr4n2y + gm: 1.23.1 imagen: github.com/MKRhere/imagen/c9988aeddaf8e03a1b30293842b8a6e3e2065e62 marked: 4.0.17 react: 18.2.0 @@ -34,6 +37,7 @@ dependencies: devDependencies: '@svgr/rollup': 6.2.1 + '@types/gm': 1.18.12 '@types/marked': 4.0.3 '@types/node': 18.0.0 '@types/react': 18.0.14 @@ -1501,7 +1505,7 @@ packages: '@babel/helper-split-export-declaration': 7.16.7 '@babel/parser': 7.18.5 '@babel/types': 7.18.4 - debug: 4.3.2 + debug: 4.3.4 globals: 11.12.0 transitivePeerDependencies: - supports-color @@ -2616,6 +2620,12 @@ packages: '@types/serve-static': 1.13.10 dev: true + /@types/gm/1.18.12: + resolution: {integrity: sha512-k3Gd15Ywy75FiFQUsrqpKD42rhMdHaETyw2zjK9u3xbrH1c7nS+npSb19yjPCqR8KxeL7vw2zJF4Kl2bAca0aQ==} + dependencies: + '@types/node': 18.0.0 + dev: true + /@types/graceful-fs/4.1.5: resolution: {integrity: sha512-anKkLmZZ+xm4p8JWBf4hElkM4XR+EZeA2M9BAkkTldmcyDY4mbdIJnRghDJH3Ov5ooY7/UAoENtmdMSkaAd7Cw==} dependencies: @@ -3298,6 +3308,14 @@ packages: is-string: 1.0.7 dev: true + /array-parallel/0.1.3: + resolution: {integrity: sha512-TDPTwSWW5E4oiFiKmz6RGJ/a80Y91GuLgUYuLd49+XBS75tYo8PNgaT2K/OxuQYqkoI852MDGBorg9OcUSTQ8w==} + dev: false + + /array-series/0.1.5: + resolution: {integrity: sha512-L0XlBwfx9QetHOsbLDrE/vh2t018w9462HM3iaFfxRiK83aJjAt/Ja3NMkOW7FICwWTlQBa3ZbL5FKhuQWkDrg==} + dev: false + /array-union/2.1.0: resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==} engines: {node: '>=8'} @@ -4014,6 +4032,13 @@ packages: yaml: 1.10.2 dev: true + /cross-spawn/4.0.2: + resolution: {integrity: sha512-yAXz/pA1tD8Gtg2S98Ekf/sewp3Lcp3YoFKJ4Hkp5h5yLWnKVTDU0kwjKJ8NDCYcfTLfyGkzTikst+jWypT1iA==} + dependencies: + lru-cache: 4.1.5 + which: 1.3.1 + dev: false + /cross-spawn/7.0.3: resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==} engines: {node: '>= 8'} @@ -4072,7 +4097,7 @@ packages: postcss-modules-scope: 3.0.0_postcss@8.4.14 postcss-modules-values: 4.0.0_postcss@8.4.14 postcss-value-parser: 4.2.0 - semver: 7.3.5 + semver: 7.3.7 webpack: 5.73.0 dev: true @@ -4295,7 +4320,6 @@ packages: optional: true dependencies: ms: 2.1.3 - dev: true /debug/4.3.2: resolution: {integrity: sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==} @@ -5184,7 +5208,7 @@ packages: ajv: 6.12.6 chalk: 4.1.1 cross-spawn: 7.0.3 - debug: 4.3.2 + debug: 4.3.4 doctrine: 3.0.0 escape-string-regexp: 4.0.0 eslint-scope: 7.1.1 @@ -5801,6 +5825,18 @@ packages: slash: 3.0.0 dev: true + /gm/1.23.1: + resolution: {integrity: sha512-wYGVAa8/sh9ggF5qWoOs6eArcAgwEPkDNvf637jHRHkMUznvs7m/Q2vrc0KLN6B8px3nnRJqJcXK4mTK6lLFmg==} + engines: {node: '>= 0.10.0'} + dependencies: + array-parallel: 0.1.3 + array-series: 0.1.5 + cross-spawn: 4.0.2 + debug: 3.2.7 + transitivePeerDependencies: + - supports-color + dev: false + /graceful-fs/4.2.10: resolution: {integrity: sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==} dev: true @@ -6295,7 +6331,6 @@ packages: /isexe/2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} - dev: true /istanbul-lib-coverage/3.2.0: resolution: {integrity: sha512-eOeJ5BHCmHYvQK7xt9GkdHuzuCGS1Y6g9Gvnx3Ym33fz/HpLRYxiS0wHNr+m/MBC8B647Xt608vCDEvhl9c6Mw==} @@ -7171,6 +7206,13 @@ packages: tslib: 2.3.0 dev: true + /lru-cache/4.1.5: + resolution: {integrity: sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==} + dependencies: + pseudomap: 1.0.2 + yallist: 2.1.2 + dev: false + /lru-cache/6.0.0: resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==} engines: {node: '>=10'} @@ -7359,7 +7401,6 @@ packages: /ms/2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} - dev: true /multicast-dns/7.2.5: resolution: {integrity: sha512-2eznPJP8z2BFLX50tf0LuODrpINqP1RVIm/CObbTcBRITQgmC/TjcREF1NeTBzIcR5XO/ukWo+YHOjBbFwIupg==} @@ -8120,7 +8161,7 @@ packages: cosmiconfig: 7.0.1 klona: 2.0.5 postcss: 8.4.14 - semver: 7.3.5 + semver: 7.3.7 webpack: 5.73.0 dev: true @@ -8651,6 +8692,10 @@ packages: ipaddr.js: 1.9.1 dev: true + /pseudomap/1.0.2: + resolution: {integrity: sha512-b/YwNhb8lk1Zz2+bXXpS/LK9OisiZZ1SNsSLxN1x2OXVEhW2Ckr/7mWE5vrC1ZTiJlD9g19jWszTmJsB+oEpFQ==} + dev: false + /psl/1.8.0: resolution: {integrity: sha512-RIdOzyoavK+hA18OGGWDqUTsCLhtA7IcZ/6NCs4fFJaHBDab+pDDmDIByWFRQJq2Cd7r1OoQxBGKOaztq+hjIQ==} dev: true @@ -10440,7 +10485,6 @@ packages: hasBin: true dependencies: isexe: 2.0.0 - dev: true /which/2.0.2: resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} @@ -10689,6 +10733,10 @@ packages: engines: {node: '>=10'} dev: true + /yallist/2.1.2: + resolution: {integrity: sha512-ncTzHV7NvsQZkYe1DW7cbDLm0YpzHmZF5r/iyP3ZnQtMiJ+pjzisCiMNI+Sj+xQF5pXhSHxSB3uDbsBTzY/c2A==} + dev: false + /yallist/4.0.0: resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} diff --git a/scripts/blog.js b/scripts/blog.js index 2bcb214..2485375 100644 --- a/scripts/blog.js +++ b/scripts/blog.js @@ -1,7 +1,14 @@ // @ts-check -import { readdir, readFile, writeFile, mkdir } from "node:fs/promises"; +import { readdir, readFile, writeFile, mkdir, stat } from "node:fs/promises"; import { renderPreviewImg } from "imagen"; +import GM from "gm"; + +function rewriteExtn(filename, extn) { + const split = filename.split("."); + split[split.length - 1] = extn; + return split.join("."); +} const toplevel = (await readdir("public/blog")).filter(x => x !== "assets"); @@ -55,17 +62,15 @@ const data = Object.fromEntries( ), ); -function rewriteExtn(filename, extn) { - const split = filename.split("."); - split[split.length - 1] = extn; - return split.join("."); -} +writeFile("src/blog.json", JSON.stringify(data, null, "\t"), "utf-8").then(() => + console.log("Done"), +); /** * * @param {import("./blog.types").Data} data */ -export async function generateFeaturedImages(data) { +async function generateFeaturedImages(data) { for (const year in data) { const yearData = data[year]; for (const slug in yearData) { @@ -82,10 +87,35 @@ export async function generateFeaturedImages(data) { } } -writeFile("src/blog.json", JSON.stringify(data, null, "\t"), "utf-8").then(() => - console.log("Done"), -); - mkdir("public/blog/assets/featured", { recursive: true }).then(() => generateFeaturedImages(data), ); + +const convert = (gm, img, size, fmt) => + new Promise((resolve, reject) => + gm("public/blog/assets/" + img) + .resize(size) + .noProfile() + .write( + "public/blog/assets/gen/" + rewriteExtn(img, size + "." + fmt), + err => { + if (err) return reject(err); + else resolve(true); + }, + ), + ); + +async function generateOptimisedImages() { + await mkdir("public/blog/assets/gen/", { recursive: true }); + + const imgs = await readdir("public/blog/assets"); + for (const img of imgs) { + if (!(await stat("public/blog/assets/" + img)).isFile()) continue; + const gm = GM.subClass({ imageMagick: true }); + for (const size of [480, 800]) + for (const fmt of ["jpg", "webp", "avif"]) + await convert(gm, img, size, fmt); + } +} + +generateOptimisedImages(); diff --git a/src/pages/blog/components/BlogContent.tsx b/src/pages/blog/components/BlogContent.tsx index c6f2589..5f0011a 100644 --- a/src/pages/blog/components/BlogContent.tsx +++ b/src/pages/blog/components/BlogContent.tsx @@ -7,7 +7,7 @@ import { ArticleSubHeader } from "./ArticleSubHeader"; import { css, cx } from "@emotion/css"; import { ReactComponent as Arrow } from "../../../assets/arrow-thin.svg"; import { ReactComponent as Close } from "../../../assets/close.svg"; -import { ellipses, useNav } from "../../../util"; +import { ellipses, rewriteExtn, useNav } from "../../../util"; const Markdown: React.FC<{ content: string }> = ({ content }) => { return ( @@ -152,6 +152,11 @@ export const BlogPost: React.FC = () => { if (!article || error) return
{error || "Unknown error occurred"}
; + const featured = rewriteExtn( + "/blog/assets/gen/" + article["featured-img"], + "", + ); + return ( <> { width: 100%; max-height: 25rem; `}> - Featured + + {[ + [480, "avif", 600], + [800, "avif"], + [480, "webp", 600], + [800, "webp"], + [480, "jpg", 600], + [800, "jpg"], + ].map(([size, format, query]) => { + return ( + + ); + })} + featured +

{ navigate(link); }; }; + +export function rewriteExtn(filename: string, extn: string) { + const split = filename.split("."); + split[split.length - 1] = extn; + return split.join("."); +}