Skip to content

Commit cf3f1e4

Browse files
committed
home page image api route in progress
1 parent e0165b2 commit cf3f1e4

File tree

7 files changed

+245
-0
lines changed

7 files changed

+245
-0
lines changed

docs/working-notes/todo4.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,17 @@ use picture with srcSet for Home image
6161
solve loading fallback for async client image
6262
astro image as react child, wont work because of props
6363
https://docs.astro.build/en/guides/images/#images-in-ui-framework-components
64+
------------
65+
https://github.com/RafidMuhymin/astro-imagetools // not needed actually, placeholder...
66+
load on scroll?
67+
------------
68+
git checkout feat/new-gallery
69+
70+
use api route same as og-image, for gallery and home
71+
placeholder - blur, bg-color, single // extract line
72+
img with srcset
73+
74+
rename all-images to gallery-images
6475
```
6576
6677

src/constants/gallery.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export const EXCLUDE_IMAGES = ['avatar1.jpg'];

src/constants/routes.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,5 +31,6 @@ export const ROUTES = {
3131
OG_IMAGES: '/api/open-graph/',
3232
FEED_JSON: '/api/feed.json',
3333
FEED_RSS: '/api/feed.xml',
34+
GALLERY: '/api/gallery/',
3435
},
3536
} as const;

src/libs/api/gallery/image-path.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import { getImage } from 'astro:assets';
2+
3+
import { IMAGE_SIZES } from '@/constants/image';
4+
5+
import type { ImageMetadata } from 'astro';
6+
7+
// Todo: add blur, thumb, full size images
8+
9+
// move to types
10+
export interface ImageSources {
11+
blur: string;
12+
color: string;
13+
thumbnail: string;
14+
fullSize: string;
15+
}
16+
17+
// only for Home for now
18+
export const imageMetadataToImageSources = async (
19+
imageMetadata: ImageMetadata
20+
): Promise<string> => {
21+
const astroImageProps = {
22+
src: imageMetadata,
23+
format: 'webp',
24+
};
25+
26+
const fullSizeAstroImageProps = {
27+
...astroImageProps,
28+
// must use picture tag with srcSet
29+
// ...IMAGE_SIZES.RESPONSIVE.POST_HERO,
30+
...IMAGE_SIZES.FIXED.MDX_XL_16_9,
31+
alt: 'Hero image',
32+
};
33+
34+
const optimizedImageProps = await getImage(fullSizeAstroImageProps);
35+
const { src: imageSrc } = optimizedImageProps;
36+
37+
return imageSrc;
38+
};

src/libs/api/gallery/images.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { EXCLUDE_IMAGES } from '@/constants/gallery';
2+
3+
import type { ImageMetadata } from 'astro';
4+
5+
// must use {path: image} object instead of array for getStaticPaths()
6+
7+
export type GalleryImages = Record<string, ImageMetadata>;
8+
9+
export const getGalleryImages = async (): Promise<GalleryImages> => {
10+
const imageModules = import.meta.glob<{ default: ImageMetadata }>(
11+
// cant be even variable
12+
'/src/assets/images/all-images/*.jpg',
13+
{ eager: true }
14+
);
15+
16+
const imagesTupleArray = Object.entries(imageModules)
17+
// filter excluded filenames
18+
.filter(([path]) => !EXCLUDE_IMAGES.some((excludedFileName) => path.endsWith(excludedFileName)))
19+
// create [path, imageMetadata] tuple array
20+
.map(([path, imageModule]) => {
21+
// src/assets/images/all-images/riverside1.jpg -> 'gallery/riverside1'
22+
const fileName = path.replace(/^\/src\/assets\/images\/all-images\/|\.jpe?g|\.png$/g, '');
23+
const imagePath = `gallery/${fileName}`;
24+
25+
return [imagePath, imageModule.default];
26+
});
27+
28+
const imagesObject: GalleryImages = Object.fromEntries(imagesTupleArray);
29+
30+
return imagesObject;
31+
};

src/pages/api/gallery/[...route].ts

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
import fs from 'fs/promises';
2+
import path from 'path';
3+
4+
import satori from 'satori';
5+
import sharp from 'sharp';
6+
7+
import { FILE_PATHS } from '@/constants/file-paths';
8+
import { CONFIG_CLIENT } from '@/config/client';
9+
import { getPages } from '@/libs/api/open-graph/pages';
10+
import templateHtml from '@/libs/api/open-graph/template-html';
11+
import { removeTrailingSlash } from '@/utils/paths';
12+
import { trimHttpProtocol } from '@/utils/strings';
13+
14+
import type { APIContext, APIRoute } from 'astro';
15+
16+
const { SITE_URL } = CONFIG_CLIENT;
17+
const { FONTS_FOLDER, OG_FOLDER, IMAGE_404, AVATAR } = FILE_PATHS;
18+
19+
export const getStaticPaths = async () => {
20+
const pages = await getPages();
21+
22+
// object to array of tuples
23+
const paths = Object.entries(pages).map(([path, page]) => ({
24+
params: { route: path },
25+
props: { page },
26+
}));
27+
28+
return paths;
29+
};
30+
31+
export const GET: APIRoute = async ({ props }: APIContext) => {
32+
// limit number of chars
33+
const { title, heroImage, pageId } = props.page;
34+
35+
// resize images in template in CSS only, not in sharp
36+
37+
// avatarImage
38+
const avatarImageBase64Url = await getBase64Image(AVATAR);
39+
40+
// heroImage
41+
let heroImagePath: string;
42+
43+
switch (true) {
44+
case Boolean(heroImage?.fsPath):
45+
heroImagePath = heroImage?.fsPath;
46+
break;
47+
// hardcoded in 404.mdx frontmatter
48+
case pageId === 'page404':
49+
heroImagePath = IMAGE_404;
50+
break;
51+
52+
// fallback to random default image
53+
default:
54+
heroImagePath = await getRandomImage(OG_FOLDER);
55+
break;
56+
}
57+
58+
const heroImageBase64Url = await getBase64Image(heroImagePath);
59+
60+
const templateProps = {
61+
title,
62+
heroImageUrl: heroImageBase64Url,
63+
avatarImageUrl: avatarImageBase64Url,
64+
siteUrl: trimHttpProtocol(SITE_URL),
65+
};
66+
67+
const fontData = await fs.readFile(`${FONTS_FOLDER}inter-regular.woff`);
68+
69+
const svg = await satori(templateHtml(templateProps) as React.ReactNode, {
70+
width: 1200,
71+
height: 630,
72+
fonts: [
73+
{
74+
name: 'Inter',
75+
data: fontData,
76+
weight: 400,
77+
style: 'normal',
78+
},
79+
],
80+
});
81+
82+
const pngBuffer = await sharp(Buffer.from(svg)).png().toBuffer();
83+
84+
return new Response(pngBuffer);
85+
};
86+
87+
/*-------------------------------- utils ------------------------------*/
88+
89+
const getBase64Image = async (imagePath: string): Promise<string> => {
90+
const imageData = await fs.readFile(imagePath);
91+
92+
const imageType = getImageType(imagePath);
93+
const imageBase64 = Buffer.from(imageData).toString('base64');
94+
const imageBase64Url = `data:image/${imageType};base64,${imageBase64}`;
95+
96+
return imageBase64Url;
97+
};
98+
99+
const getImageType = (imagePath: string) => {
100+
const extension = path.extname(imagePath).toLowerCase();
101+
102+
let imageType: string;
103+
switch (extension) {
104+
case '.png':
105+
imageType = 'png';
106+
break;
107+
case '.jpg':
108+
case '.jpeg':
109+
imageType = 'jpeg';
110+
break;
111+
default:
112+
throw new Error('Unsupported heroImage file extension');
113+
}
114+
115+
return imageType;
116+
};
117+
118+
const getRandomImage = async (folderPath: string): Promise<string> => {
119+
const trimmedFolderPath = removeTrailingSlash(folderPath);
120+
121+
const files = await fs.readdir(trimmedFolderPath);
122+
123+
// omit ./, ../
124+
const imageFiles = files.filter((file) => {
125+
const ext = path.extname(file).toLowerCase();
126+
return ext === '.jpg' || ext === '.jpeg' || ext === '.png';
127+
});
128+
129+
if (imageFiles.length === 0) throw new Error(`No default og images found in: ${folderPath}`);
130+
131+
const randomIndex = Math.floor(Math.random() * imageFiles.length);
132+
const randomImage = imageFiles[randomIndex];
133+
134+
const randomImageWithPath = `${trimmedFolderPath}/${randomImage}`;
135+
136+
return randomImageWithPath;
137+
};

src/pages/api/home/[...route].ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { imageMetadataToImageSources } from '@/libs/api/gallery/image-path';
2+
import { getGalleryImages } from '@/libs/api/gallery/images';
3+
4+
import type { APIContext, APIRoute } from 'astro';
5+
6+
export const getStaticPaths = async () => {
7+
const images = await getGalleryImages();
8+
9+
// object to array of tuples
10+
const paths = Object.entries(images).map(([path, image]) => ({
11+
// matches file name [...route].ts
12+
params: { route: path },
13+
props: { image },
14+
}));
15+
16+
return paths;
17+
};
18+
19+
export const GET: APIRoute = async ({ props }: APIContext) => {
20+
const { image } = props;
21+
const imageSrc = await imageMetadataToImageSources(image);
22+
23+
// generate blur, thumb and full size images, extract into separate function
24+
25+
return new Response(imageSrc); // should be json
26+
};

0 commit comments

Comments
 (0)