-
-
Save ioxua/1cb9cece7aa0ac13100bec23a9580b04 to your computer and use it in GitHub Desktop.
Unsplash cache for Astro (or any other SSG really)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
UNSPLASH_ACCESS_KEY=<your_key> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
.astro/* | |
# this file should be checked to git | |
!.astro/unsplash.json |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { readFile, writeFile } from 'node:fs/promises' | |
import { createApi } from 'unsplash-js'; | |
type FullImage = NonNullable<Awaited<ReturnType<ReturnType<typeof createApi>['photos']['get']>>['response']> | |
export interface UnsplashImageInternal { | |
id: string; | |
description: string; | |
urls: { | |
raw: string; | |
full: string; | |
regular: string; | |
small: string; | |
thumb: string; | |
} | |
author: { name: string; username: string; } | |
} | |
export interface UnsplashImage extends UnsplashImageInternal { | |
attribution: { | |
authorProfileUrl: string | |
unsplashUrl: string | |
} | |
} | |
type Options = { | |
appName: string, | |
cacheFile: string, | |
} | |
class UnsplashCache { | |
#cache: Map<string, UnsplashImageInternal> = new Map() | |
#cacheLoaded: boolean = false | |
readonly #options: Options | |
readonly #api: ReturnType<typeof createApi> = createApi({ | |
accessKey: import.meta.env.UNSPLASH_ACCESS_KEY ?? '', | |
}) | |
constructor(options: Options) { | |
this.#options = options | |
} | |
async init(): Promise<void> { | |
return readFile(this.#options.cacheFile, { | |
encoding: 'utf-8', | |
}) | |
.then(res => { | |
console.log('[Unsplash] cache loaded') | |
this.#cache = new Map(Object.entries(JSON.parse(res))) | |
}) | |
// we ignore the error in case the file doesn't exist since it will be created later | |
.catch(() => {}) | |
.finally(() => this.#cacheLoaded = true) | |
} | |
async get(imageId: string): Promise<UnsplashImage> { | |
if (!this.#checkInit()) throw new Error('cache not initialized') | |
const cached = this.#cache.get(imageId) | |
// console.log(`[unsplash] is ${imageId} in cache?`, !!cached) | |
// console.log(`[unsplash] current cache`, this.#cache) | |
if (cached) return this.#addExtraInfo(cached) | |
const res = await this.#api.photos.get({ photoId: imageId }) | |
if (res.type === 'success') { | |
const photo = res.response; | |
this.#api.photos.trackDownload({ | |
downloadLocation: photo.links.download_location, | |
}); | |
return this.#addExtraInfo(this.#set(photo)) | |
} | |
console.warn('An error occurred when requesting the API:', res.errors[0]) | |
throw new Error(res.errors[0]) | |
} | |
#checkInit(): boolean { | |
if (!this.#cacheLoaded) { | |
console.warn('[Unsplash] cache not initialized') | |
} | |
return this.#cacheLoaded | |
} | |
#addExtraInfo(image: UnsplashImageInternal): UnsplashImage { | |
const query = `?utm_source=${this.#options.appName}&utm_medium=referral` | |
return { ...image, attribution: { | |
authorProfileUrl: `https://unsplash.com/@${image.author.username}${query}`, | |
unsplashUrl: `https://unsplash.com${query}`, | |
} } | |
} | |
#set(image: FullImage): UnsplashImageInternal { | |
const converted: UnsplashImageInternal = { | |
id: image.id, | |
description: image.alt_description ?? image.description ?? '', | |
urls: image.urls, | |
author: { name: image.user.name, username: image.user.username }, | |
} | |
this.#cache.set(image.id, converted) | |
this.#persist() | |
return converted | |
} | |
#persist() { | |
writeFile(this.#options.cacheFile, JSON.stringify(Object.fromEntries(this.#cache)), { | |
encoding: 'utf-8' | |
}) | |
.then(() => console.log('[Unsplash] updated cache in disk')) | |
} | |
} | |
let cacheInstance: UnsplashCache | undefined | |
export async function useUnsplash() { | |
if (!cacheInstance) { | |
cacheInstance = new UnsplashCache({ | |
cacheFile: '.astro/unsplash.json', | |
appName: 'your_app_name', | |
}) | |
await cacheInstance.init() | |
} | |
return cacheInstance | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
--- | |
import { getLangFromUrl } from '../i18n'; | |
import { useUnsplash } from '../utils'; | |
import Link from './Link.astro'; | |
export interface Props { | |
id: string | |
quality?: 'raw' | 'full' | 'regular' | 'small' | 'thumb' | |
/** | |
* @see https://unsplash.com/documentation#supported-parameters | |
*/ | |
imgix?: { | |
fit?: 'clamp' | 'clip' | 'crop' | 'facearea' | 'fill' | 'fillmax' | 'max' | 'min' | 'scale' | |
crop?: 'top' | 'bottom' | 'left' | 'right' | 'faces' | 'focalpoint' | 'edges' | 'entropy' | |
w?: number | |
h?: number | |
} | |
} | |
// hook usage | |
const unsplash = await useUnsplash() | |
const image = await unsplash.get(Astro.props.id) | |
// i18n | |
const lang = getLangFromUrl(Astro.url) | |
const trans = { | |
'br.part1': 'Foto por', | |
'br.part2': 'no', | |
'en.part1': 'Photo by', | |
'en.part2': 'on', | |
} | |
const quality = Astro.props.quality ?? 'full' | |
const url = new URL(Astro.props.imgix ? image.urls.raw : image.urls[quality]) | |
// I _could_ generalize this, but is it worth it the effort? | |
if (Astro.props.imgix) { | |
if (Astro.props.imgix.crop) url.searchParams.append('crop', Astro.props.imgix.crop) | |
if (Astro.props.imgix.fit) url.searchParams.append('fit', Astro.props.imgix.fit) | |
if (Astro.props.imgix.h) url.searchParams.append('h', Astro.props.imgix.h.toString()) | |
if (Astro.props.imgix.w) url.searchParams.append('w', Astro.props.imgix.w.toString()) | |
} | |
--- | |
<figure> | |
<img src={url.toString()} alt={image.description} /> | |
<figcaption> | |
{trans[`${lang}.part1`]} | |
<Link | |
href={image.attribution.authorProfileUrl} | |
title={image.author.name} | |
rel="nofollow" | |
>{image.author.name}</Link> | |
{trans[`${lang}.part2`]} | |
<Link | |
href={image.attribution.unsplashUrl} | |
title='Unsplash' | |
rel="nofollow" | |
>Unsplash</Link> | |
</figcaption> | |
</figure> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment