Skip to content

Commit

Permalink
feat: proxy and optimize media for Premium users
Browse files Browse the repository at this point in the history
  • Loading branch information
AlejandroAkbal committed Mar 9, 2024
1 parent fde230c commit 86d7cd8
Show file tree
Hide file tree
Showing 4 changed files with 174 additions and 68 deletions.
84 changes: 84 additions & 0 deletions assets/js/nuxt-image/imgproxy.provider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import { joinURL } from "ufo";
import { createOperationsGenerator } from "@nuxt/image/dist/runtime/utils/index";
import type { ProviderGetImage } from "@nuxt/image";
import { Buffer } from "buffer";

// https://docs.imgproxy.net/
const operationsGenerator = createOperationsGenerator({
keyMap: {
resize: "rs",
size: "s",
fit: "rt",
width: "w",
height: "h",
dpr: "dpr",
enlarge: "el",
extend: "ex",
gravity: "g",
crop: "c",
padding: "pd",
trim: "t",
rotate: "rot",
quality: "q",
maxBytes: "mb",
background: "bg",
backgroundAlpha: "bga",
blur: "bl",
sharpen: "sh",
watermark: "wm",
preset: "pr",
cacheBuster: "cb",
stripMetadata: "sm",
stripColorProfile: "scp",
autoRotate: "ar",
filename: "fn",
format: "f",
},
formatter: (key, value) => `${key}:${value}`,
});

function urlSafeBase64(string: string) {
return Buffer.from(string, "utf8")
.toString("base64")
.replace(/=/g, "")
.replace(/\+/g, "-")
.replace(/\//g, "_");
}

const defaultModifiers = {
// fit: "fill",
// width: 0,
// height: 0,
// gravity: "no",
// enlarge: 1,
// format: "webp",
};

/**
*
* @see https://github.com/nuxt/image/issues/378
*/
export const getImage: ProviderGetImage = (src, options) => {

// Skip if src is a relative URL
if (src.startsWith("/")) {
return { url: src };
}

// Skip GIFs, since imgproxy doesn't support them
if (src.endsWith(".gif")) {
return { url: src };
}

const { modifiers, baseURL } = options;

const mergeModifiers = { ...defaultModifiers, ...modifiers };

const encodedUrl = urlSafeBase64(src);

const path = joinURL("/", operationsGenerator(mergeModifiers), encodedUrl);

return {
url: joinURL(baseURL, path),
};
};
80 changes: 48 additions & 32 deletions components/pages/posts/post/PostMedia.vue
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
<script lang="ts" setup>
import {vIntersectionObserver} from '@vueuse/components'
import type {IPost} from 'assets/js/post'
import { vIntersectionObserver } from '@vueuse/components'
import type { IPost } from 'assets/js/post'
const { isPremium } = useUserData()
const { isPremium } = useUserData()
export interface PostMediaProps {
mediaSrc: string | null
Expand All @@ -16,6 +16,7 @@ const { isPremium } = useUserData()
const props = defineProps<PostMediaProps>()
const localSrc = shallowRef(props.mediaSrc)
const localPosterSrc = shallowRef(props.mediaPosterSrc)
const mediaHasLoaded = ref(false)
Expand All @@ -32,20 +33,15 @@ const { isPremium } = useUserData()
return
}
// Skip if no src
// @see onIntersectionObserver method
if (!event.target?.src) {
return
}
if (!triedToLoadWithProxy.value && isPremium.value) {
// Proxy videos, images are already proxied
if (isVideo.value && !triedToLoadWithProxy.value && isPremium.value) {
triedToLoadWithProxy.value = true
const { proxiedUrl } = useProxyHelper(localSrc.value)
const { proxiedUrl: proxiedPosterUrl } = useProxyHelper(props.mediaPosterSrc)
localSrc.value = proxiedUrl.value
// Fix: Inmediately set src to proxied url, since onIntersectionObserver method will not be called until out of viewport
event.target.src = proxiedUrl.value
localPosterSrc.value = proxiedPosterUrl.value
return
}
Expand All @@ -59,12 +55,11 @@ const { isPremium } = useUserData()
error.value = null
// Reload media
localSrc.value = ''
localSrc.value = props.mediaSrc
localPosterSrc.value = props.mediaPosterSrc
}
function onMediaIntersectionObserver(entries: IntersectionObserverEntry[]) {
// Skip on fullscreen
if (document.fullscreenElement) {
return
Expand All @@ -87,7 +82,7 @@ const { isPremium } = useUserData()
const newSrc = entry.isIntersecting
? //
(mediaElement.getAttribute('data-src') as string)
localSrc.value
: smallestMedia
mediaElement.src = newSrc
Expand All @@ -97,7 +92,6 @@ const { isPremium } = useUserData()
* Stops videos when they are out of the viewport
*/
function onVideoIntersectionObserver(entries: IntersectionObserverEntry[]) {
// Skip on fullscreen
if (document.fullscreenElement) {
return
Expand Down Expand Up @@ -170,25 +164,47 @@ const { isPremium } = useUserData()
</template>

<!-- Image -->
<!-- TODO: Fix very large images not being on screen so not loaded -->
<div
v-else-if="isImage"
v-intersection-observer="[onMediaIntersectionObserver, { rootMargin: '1200px' }]"
class="transition-opacity duration-700 ease-in-out"
:class="mediaHasLoaded ? 'opacity-100' : 'opacity-0'"
>
<!-- TODO: Fix very large images not being on screen so not loaded -->
<!-- Fix(rounded borders): add the same rounded borders that the parent has -->
<img
:alt="mediaAlt"
:class="[mediaHasLoaded ? 'opacity-100' : 'opacity-0']"
:data-src="localSrc"
:height="mediaSrcHeight"
:style="`aspect-ratio: ${mediaSrcWidth}/${mediaSrcHeight};`"
:width="mediaSrcWidth"
class="h-auto w-full rounded-t-md transition-opacity duration-700 ease-in-out"
decoding="async"
loading="lazy"
@error="onMediaError"
@load="onMediaLoad"
/>
<template v-if="!isPremium">
<img
:alt="mediaAlt"
:src="localSrc"
:height="mediaSrcHeight"
:style="`aspect-ratio: ${mediaSrcWidth}/${mediaSrcHeight};`"
:width="mediaSrcWidth"
class="h-auto w-full rounded-t-md"
decoding="async"
loading="lazy"
@load="onMediaLoad"
@error="onMediaError"
/>
</template>

<!-- Premium users get their media proxied and optimized -->
<template v-else>
<!-- Fix(rounded borders): add the same rounded borders that the parent has -->
<NuxtPicture
:alt="mediaAlt"
:src="localSrc"
:height="mediaSrcHeight"
:width="mediaSrcWidth"
decoding="async"
loading="lazy"
:imgAttrs="{
class: 'h-auto w-full rounded-t-md',
style: 'aspect-ratio: ' + mediaSrcWidth + '/' + mediaSrcHeight
}"
@load="onMediaLoad"
@error="onMediaError"
/>
</template>
</div>

<!-- Video -->
Expand All @@ -200,9 +216,9 @@ const { isPremium } = useUserData()
<!-- Fix(rounded borders): add the same rounded borders that the parent has -->
<video
v-intersection-observer="[onVideoIntersectionObserver, { rootMargin: '100px' }]"
:data-src="localSrc"
:src="localSrc"
:height="mediaSrcHeight"
:poster="mediaPosterSrc"
:poster="localPosterSrc"
:style="`aspect-ratio: ${mediaSrcWidth}/${mediaSrcHeight};`"
:width="mediaSrcWidth"
class="h-auto w-full rounded-t-md"
Expand Down
60 changes: 34 additions & 26 deletions nuxt.config.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {sentryVitePlugin} from '@sentry/vite-plugin'
import { sentryVitePlugin } from '@sentry/vite-plugin'

export default defineNuxtConfig({
// TODO: Enable when SSR is enabled
Expand All @@ -17,24 +17,24 @@ export default defineNuxtConfig({
},

// Static pages are prerendered
'/': {prerender: true},
'/other-sites': {prerender: true},
'/legal': {prerender: true},
'/': { prerender: true },
'/other-sites': { prerender: true },
'/legal': { prerender: true },

'/settings': {ssr: false},
'/settings': { ssr: false },

// TODO: Remove when A/B testing is finished @see 040.matomo.client.ts
'/premium': {ssr: false},
'/premium': { ssr: false },
// '/premium': {prerender: true},
'/premium/sign-in': {prerender: true},
'/premium/sign-in': { prerender: true },

// All premium pages are client-side rendered
'/premium/dashboard': {ssr: false},
'/premium/saved-posts/*': {ssr: false},
'/premium/tag-collections': {ssr: false},
'/premium/additional-boorus': {ssr: false},
'/premium/backup': {ssr: false},
'/premium/migrate-old-data': {ssr: false},
'/premium/dashboard': { ssr: false },
'/premium/saved-posts/*': { ssr: false },
'/premium/tag-collections': { ssr: false },
'/premium/additional-boorus': { ssr: false },
'/premium/backup': { ssr: false },
'/premium/migrate-old-data': { ssr: false },

// Public assets
'/img/**': {
Expand Down Expand Up @@ -101,19 +101,21 @@ export default defineNuxtConfig({

css: ['~/assets/css/main.css', '~/assets/css/cookieconsent.css'],

components: [{path: '~/components', pathPrefix: false}],
components: [{ path: '~/components', pathPrefix: false }],

site: {
url: `https://${process.env.APP_DOMAIN}`
},

modules: [
'@nuxt-alt/auth',

'@nuxt/image',

'nuxt-headlessui',

'@headlessui-float/nuxt',

'@nuxt-alt/auth',

'@formkit/auto-animate/nuxt',

'@vite-pwa/nuxt',
Expand All @@ -124,13 +126,19 @@ export default defineNuxtConfig({
],

image: {
domains: [
//
process.env.APP_DOMAIN,
'localhost',
'localhost:8081',
'www.google.com'
]
provider: 'imgproxy',

providers: {
imgproxy: {
name: 'imgproxy',
provider: '~~/assets/js/nuxt-image/imgproxy.provider',
options: {
baseURL: 'https://imgproxy.r34.app'
}
}
},

format: ['avif', 'webp']
},

/** @type {import('@nuxt-alt/auth').ModuleOptions} */
Expand Down Expand Up @@ -159,9 +167,9 @@ export default defineNuxtConfig({
property: false
},
endpoints: {
login: {url: process.env.API_URL + '/auth/log-in', method: 'post'},
refresh: {url: process.env.API_URL + '/auth/refresh', method: 'post'},
user: {url: process.env.API_URL + '/auth/profile', method: 'get'},
login: { url: process.env.API_URL + '/auth/log-in', method: 'post' },
refresh: { url: process.env.API_URL + '/auth/refresh', method: 'post' },
user: { url: process.env.API_URL + '/auth/profile', method: 'get' },
logout: false
}
}
Expand Down
Loading

0 comments on commit 86d7cd8

Please sign in to comment.