Added post processing for currency related content + cleaned up middleware
This commit is contained in:
parent
cc79e51841
commit
de9ad38835
|
@ -0,0 +1,22 @@
|
|||
import { getLogger } from "src/utils/logger";
|
||||
|
||||
class PageCache {
|
||||
private readonly logger = getLogger("[PageCache]");
|
||||
private readonly cache = new Map<string, Response>();
|
||||
|
||||
get(url: string): Response | undefined {
|
||||
const cachedPage = this.cache.get(url);
|
||||
if (cachedPage) {
|
||||
this.logger.log("Retrieved cached page for", url);
|
||||
return cachedPage?.clone();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
set(url: string, response: Response) {
|
||||
this.cache.set(url, response.clone());
|
||||
this.logger.log("Cached response for", url);
|
||||
}
|
||||
}
|
||||
|
||||
export const pageCache = new PageCache();
|
|
@ -8,6 +8,7 @@ import type {
|
|||
EndpointVideo,
|
||||
} from "src/shared/payload/payload-sdk";
|
||||
import { contextCache } from "src/cache/contextCache";
|
||||
import { PostProcessingTags } from "src/middleware/postProcessing";
|
||||
|
||||
interface Props {
|
||||
openGraph?:
|
||||
|
@ -47,7 +48,7 @@ const isIOS = parser.getOS().name === "iOS";
|
|||
<html
|
||||
lang={currentLocale}
|
||||
class:list={{
|
||||
POST_PROCESS_HTML_CLASS: true,
|
||||
[PostProcessingTags.HTML_CLASS]: true,
|
||||
"texture-dots": !isIOS,
|
||||
"font-m": true,
|
||||
"debug-lang": false,
|
||||
|
|
|
@ -4,6 +4,10 @@ import Tooltip from "components/Tooltip.astro";
|
|||
import { getI18n } from "src/i18n/i18n";
|
||||
import { contextCache } from "src/cache/contextCache";
|
||||
import { formatCurrency } from "src/utils/currencies";
|
||||
import {
|
||||
PostProcessingTags,
|
||||
prepareClassForSelectedCurrencyPostProcessing,
|
||||
} from "src/middleware/postProcessing";
|
||||
|
||||
interface Props {
|
||||
withTitle?: boolean | undefined;
|
||||
|
@ -12,8 +16,6 @@ interface Props {
|
|||
|
||||
const { withTitle, ...otherProps } = Astro.props;
|
||||
const { t } = await getI18n(Astro.locals.currentLocale);
|
||||
|
||||
const { currentCurrency } = Astro.locals;
|
||||
---
|
||||
|
||||
{/* ------------------------------------------- HTML ------------------------------------------- */}
|
||||
|
@ -23,10 +25,10 @@ const { currentCurrency } = Astro.locals;
|
|||
{
|
||||
contextCache.currencies.map((id) => (
|
||||
<a
|
||||
class:list={{
|
||||
current: currentCurrency === id,
|
||||
"pressable-link": true,
|
||||
}}
|
||||
class:list={[
|
||||
"pressable-link",
|
||||
prepareClassForSelectedCurrencyPostProcessing({ currency: id }),
|
||||
]}
|
||||
href={`?action-currency=${id}`}>
|
||||
{`${id} (${formatCurrency(id)})`}
|
||||
</a>
|
||||
|
@ -35,7 +37,7 @@ const { currentCurrency } = Astro.locals;
|
|||
</div>
|
||||
<Button
|
||||
icon="material-symbols:currency-exchange"
|
||||
title={withTitle ? currentCurrency.toUpperCase() : undefined}
|
||||
title={withTitle ? PostProcessingTags.PREFERRED_CURRENCY : undefined}
|
||||
ariaLabel={t("header.topbar.currency.tooltip")}
|
||||
/>
|
||||
</Tooltip>
|
||||
|
|
|
@ -1,16 +1,16 @@
|
|||
---
|
||||
import GenericPreview from "components/Previews/GenericPreview.astro";
|
||||
import { getI18n } from "src/i18n/i18n";
|
||||
import { formatPriceForPostProcessing } from "src/middleware/postProcessing";
|
||||
import type { EndpointCollectiblePreview } from "src/shared/payload/payload-sdk";
|
||||
import type { Attribute } from "src/utils/attributes";
|
||||
import { convert } from "src/utils/currencies";
|
||||
import { formatLocale } from "src/utils/format";
|
||||
|
||||
interface Props {
|
||||
collectible: EndpointCollectiblePreview;
|
||||
}
|
||||
|
||||
const { getLocalizedMatch, getLocalizedUrl, t, formatPrice, formatDate } = await getI18n(
|
||||
const { getLocalizedMatch, getLocalizedUrl, t, formatDate } = await getI18n(
|
||||
Astro.locals.currentLocale
|
||||
);
|
||||
|
||||
|
@ -41,18 +41,13 @@ if (releaseDate) {
|
|||
}
|
||||
|
||||
if (price) {
|
||||
const preferredCurrency = Astro.locals.currentCurrency;
|
||||
|
||||
const convertedPrice = {
|
||||
amount: convert(price.currency, preferredCurrency, price.amount),
|
||||
currency: preferredCurrency,
|
||||
};
|
||||
|
||||
additionalAttributes.push({
|
||||
title: t("collectibles.price"),
|
||||
icon: "material-symbols:sell",
|
||||
values: [
|
||||
{ name: price.amount === 0 ? t("collectibles.price.free") : formatPrice(convertedPrice) },
|
||||
{
|
||||
name: formatPriceForPostProcessing(price, "short"),
|
||||
},
|
||||
],
|
||||
withBorder: false,
|
||||
});
|
||||
|
|
|
@ -4,7 +4,6 @@
|
|||
declare namespace App {
|
||||
interface Locals {
|
||||
currentLocale: string;
|
||||
currentCurrency: string;
|
||||
notFound: boolean;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,258 +1,28 @@
|
|||
import { defineMiddleware, sequence } from "astro:middleware";
|
||||
import acceptLanguage from "accept-language";
|
||||
import type { AstroCookies } from "astro";
|
||||
import { z } from "astro:content";
|
||||
import { trackRequest, trackEvent } from "src/shared/analytics/analytics";
|
||||
import { defaultLocale } from "src/i18n/i18n";
|
||||
import { contextCache } from "src/cache/contextCache";
|
||||
import { sequence } from "astro:middleware";
|
||||
import { postProcessingMiddleware } from "src/middleware/postProcessing";
|
||||
import { localeNegotiationMiddleware } from "src/middleware/languageNegotiation";
|
||||
import { cookieRefreshingMiddleware } from "src/middleware/cookieRefreshing";
|
||||
import { addCommonHeadersMiddleware } from "src/middleware/commonHeaders";
|
||||
import { actionsHandlingMiddleware } from "src/middleware/actionsHandling";
|
||||
import { requestTrackingMiddleware } from "src/middleware/requestTracking";
|
||||
import { pageCachingMiddleware } from "src/middleware/pageCaching";
|
||||
import { setAstroLocalsMiddleware } from "src/middleware/setAstroLocals";
|
||||
|
||||
const ninetyDaysInSeconds = 60 * 60 * 24 * 90;
|
||||
|
||||
const getAbsoluteLocaleUrl = (locale: string, url: string) => `/${locale}${url}`;
|
||||
|
||||
const redirect = (redirectURL: string, headers: Record<string, string> = {}): Response => {
|
||||
return new Response(undefined, {
|
||||
headers: { ...headers, Location: redirectURL },
|
||||
status: 302,
|
||||
statusText: "Found",
|
||||
});
|
||||
};
|
||||
|
||||
const localeAgnosticPaths = ["/api/"];
|
||||
|
||||
const localeNegotiator = defineMiddleware(({ cookies, url, request }, next) => {
|
||||
if (localeAgnosticPaths.some((prefix) => url.pathname.includes(prefix))) {
|
||||
return next();
|
||||
}
|
||||
|
||||
const currentLocale = getCurrentLocale(url.pathname);
|
||||
const acceptedLocale = getBestAcceptedLanguage(request);
|
||||
const cookieLocale = getCookieLocale(cookies);
|
||||
const bestMatchingLocale = cookieLocale ?? acceptedLocale ?? currentLocale ?? defaultLocale;
|
||||
|
||||
if (!currentLocale) {
|
||||
const redirectURL = getAbsoluteLocaleUrl(bestMatchingLocale, url.pathname);
|
||||
trackEvent("locale-redirect");
|
||||
return redirect(redirectURL);
|
||||
}
|
||||
|
||||
if (currentLocale !== bestMatchingLocale) {
|
||||
const pathnameWithoutLocale = url.pathname.substring(currentLocale.length + 1);
|
||||
const redirectURL = getAbsoluteLocaleUrl(bestMatchingLocale, pathnameWithoutLocale);
|
||||
trackEvent("locale-redirect");
|
||||
return redirect(redirectURL);
|
||||
}
|
||||
|
||||
return next();
|
||||
});
|
||||
|
||||
const handleActionsSearchParams = defineMiddleware(
|
||||
async ({ url: { pathname, searchParams }, cookies }, next) => {
|
||||
const language = searchParams.get("action-lang");
|
||||
if (isValidLocale(language)) {
|
||||
const currentLocale = getCurrentLocale(pathname);
|
||||
const pathnameWithoutLocale = currentLocale
|
||||
? pathname.substring(currentLocale.length + 1)
|
||||
: pathname;
|
||||
const redirectURL = getAbsoluteLocaleUrl(language, pathnameWithoutLocale);
|
||||
trackEvent("action-lang");
|
||||
cookies.set(CookieKeys.Language, language, {
|
||||
maxAge: ninetyDaysInSeconds,
|
||||
path: "/",
|
||||
sameSite: "strict",
|
||||
});
|
||||
return redirect(redirectURL);
|
||||
}
|
||||
|
||||
const currency = searchParams.get("action-currency");
|
||||
if (isValidCurrency(currency)) {
|
||||
trackEvent("action-currency");
|
||||
cookies.set(CookieKeys.Currency, currency, {
|
||||
maxAge: ninetyDaysInSeconds,
|
||||
path: "/",
|
||||
sameSite: "strict",
|
||||
});
|
||||
return redirect(pathname);
|
||||
}
|
||||
|
||||
const theme = searchParams.get("action-theme");
|
||||
if (isValidTheme(theme)) {
|
||||
trackEvent("action-theme");
|
||||
cookies.set(CookieKeys.Theme, theme, {
|
||||
maxAge: theme === "auto" ? 0 : ninetyDaysInSeconds,
|
||||
path: "/",
|
||||
sameSite: "strict",
|
||||
});
|
||||
return redirect(pathname);
|
||||
}
|
||||
|
||||
return next();
|
||||
}
|
||||
);
|
||||
|
||||
const refreshCookiesMaxAge = defineMiddleware(async ({ cookies }, next) => {
|
||||
const response = await next();
|
||||
|
||||
const theme = cookies.get(CookieKeys.Theme)?.value;
|
||||
if (isValidTheme(theme) && theme !== "auto") {
|
||||
cookies.set(CookieKeys.Theme, theme, {
|
||||
maxAge: ninetyDaysInSeconds,
|
||||
path: "/",
|
||||
sameSite: "strict",
|
||||
});
|
||||
} else if (theme) {
|
||||
cookies.set(CookieKeys.Theme, theme, {
|
||||
maxAge: 0,
|
||||
path: "/",
|
||||
sameSite: "strict",
|
||||
});
|
||||
}
|
||||
|
||||
const currency = cookies.get(CookieKeys.Currency)?.value;
|
||||
if (isValidCurrency(currency)) {
|
||||
cookies.set(CookieKeys.Currency, currency, {
|
||||
maxAge: ninetyDaysInSeconds,
|
||||
path: "/",
|
||||
sameSite: "strict",
|
||||
});
|
||||
} else if (currency) {
|
||||
cookies.set(CookieKeys.Currency, currency, {
|
||||
maxAge: 0,
|
||||
path: "/",
|
||||
sameSite: "strict",
|
||||
});
|
||||
}
|
||||
|
||||
const language = cookies.get(CookieKeys.Language)?.value;
|
||||
if (isValidLocale(language)) {
|
||||
cookies.set(CookieKeys.Language, language, {
|
||||
maxAge: ninetyDaysInSeconds,
|
||||
path: "/",
|
||||
sameSite: "strict",
|
||||
});
|
||||
} else if (language) {
|
||||
cookies.set(CookieKeys.Language, language, {
|
||||
maxAge: 0,
|
||||
path: "/",
|
||||
sameSite: "strict",
|
||||
});
|
||||
}
|
||||
|
||||
return response;
|
||||
});
|
||||
|
||||
const addCommonHeaders = defineMiddleware(async ({ url }, next) => {
|
||||
const currentLocale = getCurrentLocale(url.pathname);
|
||||
|
||||
const response = await next();
|
||||
if (response.ok && currentLocale) {
|
||||
response.headers.set("Content-Language", currentLocale);
|
||||
}
|
||||
|
||||
response.headers.set("Vary", "Cookie");
|
||||
|
||||
return response;
|
||||
});
|
||||
|
||||
const provideLocalsToRequest = defineMiddleware(async ({ url, locals, cookies }, next) => {
|
||||
locals.currentLocale = getCurrentLocale(url.pathname) ?? "en";
|
||||
locals.currentCurrency = getCookieCurrency(cookies) ?? "USD";
|
||||
return next();
|
||||
});
|
||||
|
||||
const analytics = defineMiddleware(async (context, next) => {
|
||||
const { request, params, locals, clientAddress } = context;
|
||||
const response = await next();
|
||||
trackRequest(request, { params, locals, clientAddress });
|
||||
return response;
|
||||
});
|
||||
|
||||
const postProcess = defineMiddleware(async ({ cookies }, next) => {
|
||||
const response = await next();
|
||||
if (!response.ok) {
|
||||
return response;
|
||||
}
|
||||
|
||||
let html = await response.text();
|
||||
|
||||
const currentTheme = getCookieTheme(cookies) ?? "auto";
|
||||
html = html.replace(
|
||||
"POST_PROCESS_HTML_CLASS",
|
||||
currentTheme === "dark" ? "dark-theme" : currentTheme === "light" ? "light-theme" : ""
|
||||
);
|
||||
return new Response(html, response);
|
||||
});
|
||||
|
||||
export const onRequest = sequence(
|
||||
// Possible redirect
|
||||
handleActionsSearchParams,
|
||||
localeNegotiator,
|
||||
actionsHandlingMiddleware,
|
||||
localeNegotiationMiddleware,
|
||||
|
||||
addCommonHeaders,
|
||||
addCommonHeadersMiddleware,
|
||||
|
||||
// Get a response
|
||||
analytics,
|
||||
refreshCookiesMaxAge,
|
||||
provideLocalsToRequest,
|
||||
postProcess
|
||||
requestTrackingMiddleware,
|
||||
cookieRefreshingMiddleware,
|
||||
setAstroLocalsMiddleware,
|
||||
|
||||
// Generate body
|
||||
postProcessingMiddleware,
|
||||
pageCachingMiddleware
|
||||
);
|
||||
|
||||
/* LOCALE */
|
||||
|
||||
const getCurrentLocale = (pathname: string): string | undefined => {
|
||||
for (const locale of contextCache.locales) {
|
||||
if (pathname.split("/")[1] === locale.id) {
|
||||
return locale.id;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
const getBestAcceptedLanguage = (request: Request): string | undefined => {
|
||||
const header = request.headers.get("Accept-Language");
|
||||
if (!header) return;
|
||||
|
||||
acceptLanguage.languages(contextCache.locales.map(({ id }) => id));
|
||||
|
||||
return acceptLanguage.get(request.headers.get("Accept-Language")) ?? undefined;
|
||||
};
|
||||
|
||||
/* COOKIES */
|
||||
|
||||
export enum CookieKeys {
|
||||
Currency = "al_pref_currency",
|
||||
Theme = "al_pref_theme",
|
||||
Language = "al_pref_language",
|
||||
}
|
||||
|
||||
const themeSchema = z.enum(["dark", "light", "auto"]);
|
||||
|
||||
export const getCookieLocale = (cookies: AstroCookies): string | undefined => {
|
||||
const cookieValue = cookies.get(CookieKeys.Language)?.value;
|
||||
return isValidLocale(cookieValue) ? cookieValue : undefined;
|
||||
};
|
||||
|
||||
export const getCookieCurrency = (cookies: AstroCookies): string | undefined => {
|
||||
const cookieValue = cookies.get(CookieKeys.Currency)?.value;
|
||||
return isValidCurrency(cookieValue) ? cookieValue : undefined;
|
||||
};
|
||||
|
||||
export const getCookieTheme = (cookies: AstroCookies): z.infer<typeof themeSchema> | undefined => {
|
||||
const cookieValue = cookies.get(CookieKeys.Theme)?.value;
|
||||
return isValidTheme(cookieValue) ? cookieValue : undefined;
|
||||
};
|
||||
|
||||
export const isValidCurrency = (currency: string | null | undefined): currency is string =>
|
||||
currency !== null && currency != undefined && contextCache.currencies.includes(currency);
|
||||
|
||||
export const isValidLocale = (locale: string | null | undefined): locale is string =>
|
||||
locale !== null &&
|
||||
locale != undefined &&
|
||||
contextCache.locales.map(({ id }) => id).includes(locale);
|
||||
|
||||
export const isValidTheme = (
|
||||
theme: string | null | undefined
|
||||
): theme is z.infer<typeof themeSchema> => {
|
||||
const result = themeSchema.safeParse(theme);
|
||||
return result.success;
|
||||
};
|
||||
|
|
|
@ -0,0 +1,57 @@
|
|||
import { defineMiddleware } from "astro:middleware";
|
||||
import {
|
||||
CookieKeys,
|
||||
getAbsoluteLocaleUrl,
|
||||
getCurrentLocale,
|
||||
isValidCurrency,
|
||||
isValidLocale,
|
||||
isValidTheme,
|
||||
redirect,
|
||||
} from "src/middleware/utils";
|
||||
import { trackEvent } from "src/shared/analytics/analytics";
|
||||
|
||||
const ninetyDaysInSeconds = 60 * 60 * 24 * 90;
|
||||
|
||||
export const actionsHandlingMiddleware = defineMiddleware(
|
||||
async ({ url: { pathname, searchParams }, cookies }, next) => {
|
||||
const language = searchParams.get("action-lang");
|
||||
if (isValidLocale(language)) {
|
||||
const currentLocale = getCurrentLocale(pathname);
|
||||
const pathnameWithoutLocale = currentLocale
|
||||
? pathname.substring(currentLocale.length + 1)
|
||||
: pathname;
|
||||
const redirectURL = getAbsoluteLocaleUrl(language, pathnameWithoutLocale);
|
||||
trackEvent("action-lang");
|
||||
cookies.set(CookieKeys.Language, language, {
|
||||
maxAge: ninetyDaysInSeconds,
|
||||
path: "/",
|
||||
sameSite: "strict",
|
||||
});
|
||||
return redirect(redirectURL);
|
||||
}
|
||||
|
||||
const currency = searchParams.get("action-currency");
|
||||
if (isValidCurrency(currency)) {
|
||||
trackEvent("action-currency");
|
||||
cookies.set(CookieKeys.Currency, currency, {
|
||||
maxAge: ninetyDaysInSeconds,
|
||||
path: "/",
|
||||
sameSite: "strict",
|
||||
});
|
||||
return redirect(pathname);
|
||||
}
|
||||
|
||||
const theme = searchParams.get("action-theme");
|
||||
if (isValidTheme(theme)) {
|
||||
trackEvent("action-theme");
|
||||
cookies.set(CookieKeys.Theme, theme, {
|
||||
maxAge: theme === "auto" ? 0 : ninetyDaysInSeconds,
|
||||
path: "/",
|
||||
sameSite: "strict",
|
||||
});
|
||||
return redirect(pathname);
|
||||
}
|
||||
|
||||
return next();
|
||||
}
|
||||
);
|
|
@ -0,0 +1,16 @@
|
|||
import { defineMiddleware } from "astro:middleware";
|
||||
import { getCurrentLocale } from "src/middleware/utils";
|
||||
|
||||
export const addCommonHeadersMiddleware = defineMiddleware(async ({ url }, next) => {
|
||||
const response = await next();
|
||||
|
||||
const currentLocale = getCurrentLocale(url.pathname);
|
||||
if (currentLocale) {
|
||||
response.headers.set("Content-Language", currentLocale);
|
||||
}
|
||||
|
||||
response.headers.set("Vary", "Cookie");
|
||||
response.headers.set("Cache-Control", "max-age=3600, stale-while-revalidate=3600");
|
||||
|
||||
return response;
|
||||
});
|
|
@ -0,0 +1,55 @@
|
|||
import { defineMiddleware } from "astro:middleware";
|
||||
import { CookieKeys, isValidCurrency, isValidLocale, isValidTheme } from "src/middleware/utils";
|
||||
|
||||
const ninetyDaysInSeconds = 60 * 60 * 24 * 90;
|
||||
|
||||
export const cookieRefreshingMiddleware = defineMiddleware(async ({ cookies }, next) => {
|
||||
const response = await next();
|
||||
|
||||
const theme = cookies.get(CookieKeys.Theme)?.value;
|
||||
if (isValidTheme(theme) && theme !== "auto") {
|
||||
cookies.set(CookieKeys.Theme, theme, {
|
||||
maxAge: ninetyDaysInSeconds,
|
||||
path: "/",
|
||||
sameSite: "strict",
|
||||
});
|
||||
} else if (theme) {
|
||||
cookies.set(CookieKeys.Theme, theme, {
|
||||
maxAge: 0,
|
||||
path: "/",
|
||||
sameSite: "strict",
|
||||
});
|
||||
}
|
||||
|
||||
const currency = cookies.get(CookieKeys.Currency)?.value;
|
||||
if (isValidCurrency(currency)) {
|
||||
cookies.set(CookieKeys.Currency, currency, {
|
||||
maxAge: ninetyDaysInSeconds,
|
||||
path: "/",
|
||||
sameSite: "strict",
|
||||
});
|
||||
} else if (currency) {
|
||||
cookies.set(CookieKeys.Currency, currency, {
|
||||
maxAge: 0,
|
||||
path: "/",
|
||||
sameSite: "strict",
|
||||
});
|
||||
}
|
||||
|
||||
const language = cookies.get(CookieKeys.Language)?.value;
|
||||
if (isValidLocale(language)) {
|
||||
cookies.set(CookieKeys.Language, language, {
|
||||
maxAge: ninetyDaysInSeconds,
|
||||
path: "/",
|
||||
sameSite: "strict",
|
||||
});
|
||||
} else if (language) {
|
||||
cookies.set(CookieKeys.Language, language, {
|
||||
maxAge: 0,
|
||||
path: "/",
|
||||
sameSite: "strict",
|
||||
});
|
||||
}
|
||||
|
||||
return response;
|
||||
});
|
|
@ -0,0 +1,38 @@
|
|||
import { defineMiddleware } from "astro:middleware";
|
||||
import { trackEvent } from "src/shared/analytics/analytics";
|
||||
import { defaultLocale } from "src/i18n/i18n";
|
||||
import {
|
||||
getAbsoluteLocaleUrl,
|
||||
getBestAcceptedLanguage,
|
||||
getCookieLocale,
|
||||
getCurrentLocale,
|
||||
redirect,
|
||||
} from "src/middleware/utils";
|
||||
|
||||
const localeAgnosticPaths = ["/api/"];
|
||||
|
||||
export const localeNegotiationMiddleware = defineMiddleware(({ cookies, url, request }, next) => {
|
||||
if (localeAgnosticPaths.some((prefix) => url.pathname.includes(prefix))) {
|
||||
return next();
|
||||
}
|
||||
|
||||
const currentLocale = getCurrentLocale(url.pathname);
|
||||
const acceptedLocale = getBestAcceptedLanguage(request);
|
||||
const cookieLocale = getCookieLocale(cookies);
|
||||
const bestMatchingLocale = cookieLocale ?? acceptedLocale ?? currentLocale ?? defaultLocale;
|
||||
|
||||
if (!currentLocale) {
|
||||
const redirectURL = getAbsoluteLocaleUrl(bestMatchingLocale, url.pathname);
|
||||
trackEvent("locale-redirect");
|
||||
return redirect(redirectURL);
|
||||
}
|
||||
|
||||
if (currentLocale !== bestMatchingLocale) {
|
||||
const pathnameWithoutLocale = url.pathname.substring(currentLocale.length + 1);
|
||||
const redirectURL = getAbsoluteLocaleUrl(bestMatchingLocale, pathnameWithoutLocale);
|
||||
trackEvent("locale-redirect");
|
||||
return redirect(redirectURL);
|
||||
}
|
||||
|
||||
return next();
|
||||
});
|
|
@ -0,0 +1,27 @@
|
|||
import { defineMiddleware } from "astro:middleware";
|
||||
import { pageCache } from "src/cache/pageCache";
|
||||
|
||||
export const pageCachingMiddleware = defineMiddleware(async ({ url, request }, next) => {
|
||||
const pathname = url.pathname;
|
||||
const cachedPage = pageCache.get(pathname);
|
||||
|
||||
if (cachedPage) {
|
||||
const modifiedSince = request.headers.get("If-Modified-Since");
|
||||
const lastModified = cachedPage.headers.get("Last-Modified");
|
||||
|
||||
if (modifiedSince && lastModified && new Date(modifiedSince) <= new Date(lastModified)) {
|
||||
return new Response(null, { status: 304, statusText: "Not Modified" });
|
||||
}
|
||||
|
||||
return cachedPage;
|
||||
}
|
||||
|
||||
const response = await next();
|
||||
|
||||
if (response.ok) {
|
||||
response.headers.set("Last-Modified", new Date().toUTCString());
|
||||
pageCache.set(pathname, response);
|
||||
}
|
||||
|
||||
return response;
|
||||
});
|
|
@ -0,0 +1,108 @@
|
|||
import { defineMiddleware } from "astro:middleware";
|
||||
import { getI18n } from "src/i18n/i18n";
|
||||
import { getCookieCurrency, getCookieTheme } from "src/middleware/utils";
|
||||
import { convert } from "src/utils/currencies";
|
||||
|
||||
export enum PostProcessingTags {
|
||||
HTML_CLASS = "POST_PROCESS_HTML_CLASS",
|
||||
PRICE_START = "POST_PROCESS_PRICE_START",
|
||||
PRICE_END = "POST_PROCESS_PRICE_END",
|
||||
PREFERRED_CURRENCY = "POST_PROCESS_PREFERRED_CURRENCY",
|
||||
CURRENCY_UNDERLINE_START = "POST_PROCESS_CURRENCY_UNDERLINE_START",
|
||||
CURRENCY_UNDERLINE_END = "POST_PROCESS_CURRENCY_UNDERLINE_END",
|
||||
}
|
||||
|
||||
const priceRegex = new RegExp(
|
||||
`${PostProcessingTags.PRICE_START}(.*?)${PostProcessingTags.PRICE_END}`,
|
||||
"g"
|
||||
);
|
||||
|
||||
const selectedCurrencyRegex = new RegExp(
|
||||
`${PostProcessingTags.CURRENCY_UNDERLINE_START}(.*?)${PostProcessingTags.CURRENCY_UNDERLINE_END}`,
|
||||
"g"
|
||||
);
|
||||
|
||||
type PostProcessingCurrency = {
|
||||
currency: string;
|
||||
};
|
||||
|
||||
export const prepareClassForSelectedCurrencyPostProcessing = (currency: PostProcessingCurrency) =>
|
||||
`${PostProcessingTags.CURRENCY_UNDERLINE_START}${JSON.stringify(currency)}${PostProcessingTags.CURRENCY_UNDERLINE_END}`;
|
||||
|
||||
type PostProcessingPrice = {
|
||||
amount: number;
|
||||
currency: string;
|
||||
format: "short" | "long";
|
||||
};
|
||||
|
||||
export const formatPriceForPostProcessing = (
|
||||
price: Omit<PostProcessingPrice, "format">,
|
||||
format: "short" | "long"
|
||||
): string =>
|
||||
`${PostProcessingTags.PRICE_START}${JSON.stringify({ ...price, format })}${PostProcessingTags.PRICE_END}`;
|
||||
|
||||
export const postProcessingMiddleware = defineMiddleware(async ({ cookies, locals }, next) => {
|
||||
const { formatPrice, t } = await getI18n(locals.currentLocale);
|
||||
const currentCurrency = getCookieCurrency(cookies) ?? "USD";
|
||||
|
||||
const response = await next();
|
||||
|
||||
if (!response.ok) {
|
||||
return response;
|
||||
}
|
||||
|
||||
let html = await response.text();
|
||||
|
||||
const t0 = performance.now();
|
||||
|
||||
// HTML CLASS
|
||||
const currentTheme = getCookieTheme(cookies) ?? "auto";
|
||||
html = html.replace(
|
||||
PostProcessingTags.HTML_CLASS,
|
||||
currentTheme === "dark" ? "dark-theme" : currentTheme === "light" ? "light-theme" : ""
|
||||
);
|
||||
|
||||
// PRICES
|
||||
html = html.replaceAll(priceRegex, (_, priceString) => {
|
||||
const unescapedString = priceString.replaceAll(""", '"');
|
||||
const price = JSON.parse(unescapedString) as PostProcessingPrice;
|
||||
|
||||
if (price.amount === 0) {
|
||||
return t("collectibles.price.free");
|
||||
}
|
||||
|
||||
if (currentCurrency === price.currency) {
|
||||
return formatPrice(price);
|
||||
}
|
||||
|
||||
const convertedPrice = {
|
||||
amount: convert(price.currency, currentCurrency, price.amount),
|
||||
currency: currentCurrency,
|
||||
};
|
||||
|
||||
if (price.format === "long") {
|
||||
return `${formatPrice(price)} (${formatPrice(convertedPrice)})`;
|
||||
} else {
|
||||
return formatPrice(convertedPrice);
|
||||
}
|
||||
});
|
||||
|
||||
// PREFERRED_CURRENCY
|
||||
html = html.replace(PostProcessingTags.PREFERRED_CURRENCY, currentCurrency.toUpperCase());
|
||||
|
||||
// SELECTED CURRENCY CLASS
|
||||
html = html.replaceAll(selectedCurrencyRegex, (_, selectedCurrency) => {
|
||||
const unescapedString = selectedCurrency.replaceAll(""", '"');
|
||||
const currency = JSON.parse(unescapedString) as PostProcessingCurrency;
|
||||
|
||||
if (currentCurrency === currency.currency) {
|
||||
return "current";
|
||||
} else {
|
||||
return "";
|
||||
}
|
||||
});
|
||||
|
||||
console.log("Post-processing:", performance.now() - t0, "ms");
|
||||
|
||||
return new Response(html, response);
|
||||
});
|
|
@ -0,0 +1,9 @@
|
|||
import { defineMiddleware } from "astro/middleware";
|
||||
import { trackRequest } from "src/shared/analytics/analytics";
|
||||
|
||||
export const requestTrackingMiddleware = defineMiddleware(async (context, next) => {
|
||||
const { request, params, locals, clientAddress } = context;
|
||||
const response = await next();
|
||||
trackRequest(request, { params, locals, clientAddress });
|
||||
return response;
|
||||
});
|
|
@ -0,0 +1,7 @@
|
|||
import { defineMiddleware } from "astro:middleware";
|
||||
import { getCurrentLocale } from "src/middleware/utils";
|
||||
|
||||
export const setAstroLocalsMiddleware = defineMiddleware(async ({ url, locals }, next) => {
|
||||
locals.currentLocale = getCurrentLocale(url.pathname) ?? "en";
|
||||
return next();
|
||||
});
|
|
@ -0,0 +1,74 @@
|
|||
import type { AstroCookies } from "astro";
|
||||
import { z } from "astro:content";
|
||||
import { contextCache } from "src/cache/contextCache";
|
||||
import acceptLanguage from "accept-language";
|
||||
|
||||
export const getAbsoluteLocaleUrl = (locale: string, url: string) => `/${locale}${url}`;
|
||||
|
||||
export const redirect = (redirectURL: string, headers: Record<string, string> = {}): Response => {
|
||||
return new Response(undefined, {
|
||||
headers: { ...headers, Location: redirectURL },
|
||||
status: 302,
|
||||
statusText: "Found",
|
||||
});
|
||||
};
|
||||
|
||||
/* LOCALE */
|
||||
|
||||
export const getCurrentLocale = (pathname: string): string | undefined => {
|
||||
for (const locale of contextCache.locales) {
|
||||
if (pathname.split("/")[1] === locale.id) {
|
||||
return locale.id;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
export const getBestAcceptedLanguage = (request: Request): string | undefined => {
|
||||
const header = request.headers.get("Accept-Language");
|
||||
if (!header) return;
|
||||
|
||||
acceptLanguage.languages(contextCache.locales.map(({ id }) => id));
|
||||
|
||||
return acceptLanguage.get(request.headers.get("Accept-Language")) ?? undefined;
|
||||
};
|
||||
|
||||
/* COOKIES */
|
||||
|
||||
export enum CookieKeys {
|
||||
Currency = "al_pref_currency",
|
||||
Theme = "al_pref_theme",
|
||||
Language = "al_pref_language",
|
||||
}
|
||||
|
||||
const themeSchema = z.enum(["dark", "light", "auto"]);
|
||||
|
||||
export const getCookieLocale = (cookies: AstroCookies): string | undefined => {
|
||||
const cookieValue = cookies.get(CookieKeys.Language)?.value;
|
||||
return isValidLocale(cookieValue) ? cookieValue : undefined;
|
||||
};
|
||||
|
||||
export const getCookieCurrency = (cookies: AstroCookies): string | undefined => {
|
||||
const cookieValue = cookies.get(CookieKeys.Currency)?.value;
|
||||
return isValidCurrency(cookieValue) ? cookieValue : undefined;
|
||||
};
|
||||
|
||||
export const getCookieTheme = (cookies: AstroCookies): z.infer<typeof themeSchema> | undefined => {
|
||||
const cookieValue = cookies.get(CookieKeys.Theme)?.value;
|
||||
return isValidTheme(cookieValue) ? cookieValue : undefined;
|
||||
};
|
||||
|
||||
export const isValidCurrency = (currency: string | null | undefined): currency is string =>
|
||||
currency !== null && currency != undefined && contextCache.currencies.includes(currency);
|
||||
|
||||
export const isValidLocale = (locale: string | null | undefined): locale is string =>
|
||||
locale !== null &&
|
||||
locale != undefined &&
|
||||
contextCache.locales.map(({ id }) => id).includes(locale);
|
||||
|
||||
export const isValidTheme = (
|
||||
theme: string | null | undefined
|
||||
): theme is z.infer<typeof themeSchema> => {
|
||||
const result = themeSchema.safeParse(theme);
|
||||
return result.success;
|
||||
};
|
|
@ -1,7 +1,7 @@
|
|||
---
|
||||
import Metadata from "components/Metadata.astro";
|
||||
import { getI18n } from "src/i18n/i18n";
|
||||
import { convert } from "src/utils/currencies";
|
||||
import { formatPriceForPostProcessing } from "src/middleware/postProcessing";
|
||||
|
||||
interface Props {
|
||||
price: {
|
||||
|
@ -11,20 +11,7 @@ interface Props {
|
|||
}
|
||||
|
||||
const { price } = Astro.props;
|
||||
const { formatPrice, t } = await getI18n(Astro.locals.currentLocale);
|
||||
|
||||
const preferredCurrency = Astro.locals.currentCurrency;
|
||||
|
||||
const convertedPrice = {
|
||||
amount: convert(price.currency, preferredCurrency, price.amount),
|
||||
currency: preferredCurrency,
|
||||
};
|
||||
|
||||
let priceText = price.amount === 0 ? t("collectibles.price.free") : formatPrice(price);
|
||||
|
||||
if (price.amount > 0 && price.currency !== convertedPrice.currency) {
|
||||
priceText += ` (${formatPrice(convertedPrice)})`;
|
||||
}
|
||||
const { t } = await getI18n(Astro.locals.currentLocale);
|
||||
---
|
||||
|
||||
{/* ------------------------------------------- HTML ------------------------------------------- */}
|
||||
|
@ -32,6 +19,10 @@ if (price.amount > 0 && price.currency !== convertedPrice.currency) {
|
|||
<Metadata
|
||||
icon="material-symbols:sell-outline"
|
||||
title={t("collectibles.price")}
|
||||
values={[{ name: priceText }]}
|
||||
values={[
|
||||
{
|
||||
name: formatPriceForPostProcessing(price, "long"),
|
||||
},
|
||||
]}
|
||||
withBorder={false}
|
||||
/>
|
||||
|
|
|
@ -5,8 +5,9 @@ import { getI18n } from "src/i18n/i18n";
|
|||
import { contextCache } from "src/cache/contextCache";
|
||||
import { formatCurrency } from "src/utils/currencies";
|
||||
import { formatLocale } from "src/utils/format";
|
||||
import { prepareClassForSelectedCurrencyPostProcessing } from "src/middleware/postProcessing";
|
||||
|
||||
const { currentLocale, currentCurrency } = Astro.locals;
|
||||
const { currentLocale } = Astro.locals;
|
||||
const { t } = await getI18n(currentLocale);
|
||||
---
|
||||
|
||||
|
@ -50,7 +51,10 @@ const { t } = await getI18n(currentLocale);
|
|||
{
|
||||
contextCache.currencies.map((id) => (
|
||||
<a
|
||||
class:list={{ current: currentCurrency === id, "pressable-link": true }}
|
||||
class:list={[
|
||||
"pressable-link",
|
||||
prepareClassForSelectedCurrencyPostProcessing({ currency: id }),
|
||||
]}
|
||||
href={`?action-currency=${id}`}>
|
||||
{`${id} (${formatCurrency(id)})`}
|
||||
</a>
|
||||
|
|
|
@ -29,7 +29,6 @@ export const trackRequest = (request: Request, { clientAddress, locals }: TrackR
|
|||
address: clientAddress,
|
||||
attributes: {
|
||||
locale: locals.currentLocale,
|
||||
currency: locals.currentCurrency,
|
||||
},
|
||||
},
|
||||
request: {
|
||||
|
|
Loading…
Reference in New Issue