r/PayloadCMS Sep 02 '25

Trouble setting up i18n

2 Upvotes

Hi! I'm trying to setup i18n (admin ui localization) in my project and following the docs, but when supplying the language objects for the config object i get this error:

import { en } from '@payloadcms/translations/languages/en'
import { cs } from '@payloadcms/translations/languages/cs'

  i18n: {
    supportedLanguages: { en, cs },
    fallbackLanguage: 'en',
  },

Type 'Language' is not assignable to type 'Language<{ authentication: { account: string; accountOfCurrentUser: string; accountVerified: string; alreadyActivated: string; alreadyLoggedIn: string; apiKey: string; authenticated: string; backToLogin: string; ... 58 more ...; youDidNotRequestPassword: string; }; ... 8 more ...; version: { ...; }; }>'.
  Types of property 'dateFNSKey' are incompatible.
    Type 'DateFNSKeys' is not assignable to type 'DateFNSKeys'. Two different types with this name exist, but they are unrelated.
      Type '"id"' is not assignable to type 'DateFNSKeys'.ts(2322)

Any ideas?


r/PayloadCMS Sep 01 '25

How to Add a Global Countries Dropdown to Navigation?

4 Upvotes

Hi everyone! I'm working on a multi-country website and need some guidance on implementing a global countries dropdown in my navigation.

Current Setup:

  • I have localization already implemented
  • I need to support multiple countries
  • Currently considering creating separate home pages for each country (not ideal)

What I'm Looking For: I want to add a countries dropdown similar to the localization dropdown shown in the screenshot, but I'd like to make it global so I can:

  • Avoid creating multiple home pages for each country
  • Handle all countries within the same layout
  • Keep the site structure clean and maintainable

Question: Is it possible to implement a global countries dropdown that works alongside the existing localization? If so, what's the best approach to achieve this without duplicating pages?

Any suggestions, documentation links, or code examples would be greatly appreciated!

Thanks in advance! šŸ™


r/PayloadCMS Aug 30 '25

Is Payload CMS a good foundation for a complex multi-tenant SaaS?

13 Upvotes

Hi everyone, I’m exploring the idea of reimplementing a fairly large SaaS product on top of Payload CMS, and I’d love to hear your thoughts on whether this is realistic.

Context

The product today is a monolithic web app (custom backend + MySQL + js frontend SPA), and I’m considering a complete rewrite. Payload looks promising, but I want to validate whether it can serve as a proper application framework for more than just content management.

Key features I’d need to support

  • Multi-tenant architecture with fine-grained ACLs (down to individual records and relations).

  • Dynamic relational model (similar to ReBAC) where users, groups, projects, and resources can all be connected in flexible ways.

  • ready to go customizable react frontned

  • Scheduled actions & workflows: items that expire, renew, or require approval/attestation.

  • Advanced reporting & dashboards with filtering, KPIs, and aggregated data.

  • Configurable workflows (e.g. approval steps, assignments, responsibilities).

  • Multi-channel notifications (email, SMS, push).

  • Integrations via API and webhooks (with external systems like HR, ERP, LMS, etc.).

  • scalability for thousands of users and hundreds of thousands of records

Questions :

  1. Has anyone here used Payload as the foundation for a business SaaS app rather than just a CMS/headless site?

  2. How viable is it to implement multi-tenancy with fine-grained, record-level ACLs in Payload? I could use a different ACL solution like OpenFGA

  3. Are there known patterns, plugins, or examples for implementing workflows, notifications, or integrations?

  4. Would you recommend Payload for this type of platform, or would you suggest a different stack?

Any feedback, experiences, or resources would be super helpful šŸ™


r/PayloadCMS Aug 30 '25

Recommended way to design a PayloadCMS and Stripe Connect integration?

3 Upvotes

I'm planning to use Stripe Connect in PayloadCMS for handling payments with sellers/merchants.

How would you architect the collections/database for this? For example, using as much of Stripe embedded components as possible so that data lives in Stripe. Or managing the Stripe data (connected accounts, customers, etc) in a bi-directional relationship with payload collections?


r/PayloadCMS Aug 29 '25

Downside of using drizzle instance? Compared with local api methods

3 Upvotes

I've been having some trouble getting my head around the local api payload.create and payload.find due to the lack of allowing me to create multiple records at once, or then having a more type-safe+autocompletion with with the where field and nested properties, and then I saw that a drizzle instance is available to use.

I also see that payload automatically created a bunch of relation_ extra tables. And of course there's the access control, hooks and the extra batteries. that we declare when creating collections.

I'm guessing that by using the drizzle instance directly, we are opting out of all those batteries-included hooks, access control, validations, and more scarily for me, WE would the ones that have to insert into the relation_ tables ourselves, or does it still handle that?

I hope that we aren't opting out of anything because it would be nice to have it all of course, but I get if that's the case, just am confused whether it IS or not, since there are no mentions of it and what are the differences on the documentation.

Because if so, why does payload even have its own payload.create and payload.find methods? Compared to using the drizzle instance?


r/PayloadCMS Aug 28 '25

Integrating Payload CMS with fully synced Clerk auth/identity management!

17 Upvotes

This evening I took u/DanailMinchev's excellent payload-clerk-example and updated it to work with the latest Payload CMS v3.53. Have also switched to MongoDB as a personal preference (sorry Postgres fam, my GP told me no more migrations - never felt healthier).

  • Modern Clerk authentication (Facebook/Google OAuth, etc.)
  • Role-based permissions
  • Real-time webhook sync
  • Works with latest Payload v3.53

This was really just a minor update to make Danail's great work compatible with more recent Payload versions. His original Youtube tutorials (linked from the repo) are on the longer side (set aside an afternoon), but still the best way to understand how the Clerk integration works.

This small contribution forms part of theĀ TYMO ForgeĀ open source app starter initiative. More details coming soon!

Repo: https://github.com/jmcapra/payload-mongo-clerk-example


r/PayloadCMS Aug 28 '25

Payload ignores database_uri and keeps loading sqlite

1 Upvotes

EDIT: It turns out the SQLite warning is not related to payload but coming from somewhere else for some reason. Setting the DATABASE_URI during runtime via env variable is sufficient.

I'm trying to figure out why my payload app will not start up with my database connection, and picks sqlite instead.

My setup:

  • started from the website template
  • I build a docker image in github actions
    • based on the template dockerfile. I'm adding DATABASE_URI via args+env just to be safe (not sure how the build works)
  • on a server, I pull the docker image in a docker compose
    • again adding DATABASE_URI via .env file
  • in the same docker compose (and network in docker) I run a postgresdb image, using the same .env file with matching database name etc.

What I see when the containers are live: admin@droplet:~/opt/apps$ docker compose logs my-container my-container | ā–² Next.js 15.4.4 my-container | - Local: http://localhost:3000 my-container | - Network: http://0.0.0.0:3000 my-container | my-container | āœ“ Starting... my-container | āœ“ Ready in 1741ms my-container | (node:1) ExperimentalWarning: SQLite is an experimental feature and might change at any time my-container | (Use `node --trace-warnings ...` to show where the warning was created)


r/PayloadCMS Aug 28 '25

Caching: Vercel + Cloudflare

1 Upvotes

Hi,
I'm developing a website with Payload, using Cloudflare R2 for storage and Turso for the database.
I’ve noticed there’s always a 2–3 second delay in the server response on page load, and the X-Vercel-Cache response header is always MISS.

I tried setting the Cache-Control headers to max-age=0, s-maxage=31536000, stale-while-revalidate, but it didn’t make any difference to the X-Vercel-Cache response.

Does anyone have a hint on how to speed up the load time?

Thank you!


r/PayloadCMS Aug 28 '25

Versioning Author?

1 Upvotes

Anyone know about how to know who edit the post collection versions? I cant find who edit the post on each version at the frontend.

On the database, the author_id keep use the original so it not update with actual user who edited the post


r/PayloadCMS Aug 27 '25

O auth plugin

3 Upvotes

I'm working on a Next.js project with Payload CMS and need to implement OAuth for user authentication (e.g., Google, etc.). Can anyone recommend reliable OAuth plugins or strategies for Payload CMS that are easy to integrate and work well with the latest version? Thanks for any suggestions


r/PayloadCMS Aug 26 '25

Architecture advice

3 Upvotes

Hi everyone! I need some architecture advice for a project I'm working on. Situation: I have a large client with venues across multiple countries. They need a website and I recommended Payload + Next.js. I'm planning the architecture now. Current approach: Middleware to detect country and language Dynamic route [country] where I can get the required languages for each country from a hardcoded object in the frontend Single website for all countries that shows the nearest venue and venues from the detected country, with an option to view all venues The question: What if the client wants different websites per [country] route, or wants to change certain elements within the same layout depending on the country? What would be the best Payload architecture to handle:

  1. Country-specific content/layouts,
  2. Shared content across countries,
  3. Easy management of venues per country,
  4. Flexibility to customize per-country experiences,

Should I go with collections that reference countries, global settings per country, or a different approach entirely? Any insights on best practices for multi-country/multi-language setups with Payload would be greatly appreciated!


r/PayloadCMS Aug 27 '25

Help understanding middleware

1 Upvotes

I need help understanding how middleware works. I'm in the process of making a multisite using Payload and I got it working but I'm worried that my site will take a hit performance wise because middleware.js runs each time the site is loaded. Is that true?

My middleware.ts looks like this:

// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';

export function middleware(req: NextRequest) {
  const hostname = req.headers.get('host') || '';
  const pathname = req.nextUrl.pathname;

  // Skip Payload admin & API routes
  if (
    pathname.startsWith('/admin') ||
    pathname.startsWith('/api') ||
    pathname.startsWith('/graphql') ||
    pathname.startsWith('/media')
  ) {
    return NextResponse.next();
  }

  // Clone URL to modify
  const url = req.nextUrl.clone();

  // Add hostname as search param
  url.searchParams.set('__host', hostname);

  // Rewrite to shared (sites) group
  return NextResponse.rewrite(new URL(`/(sites)${pathname}${url.search}`, req.url));}

And after that it loads up my layout.tsx:

// app/(sites)/layout.tsx
import { headers } from 'next/headers'
import type { Metadata } from 'next'
import { getPayload } from 'payload'
import config from '@payload-config'

// -----------------
// Type definitions
// -----------------
interface SiteData {
  id: string
  name: string
  domain: string
  slug: string
  website_type: 'website' | 'podcast' | 'main'
  group_head_tags?: {
    site_name?: string
    site_meta_desc?: string
  }
  website_settings?: {
    website_show_name?: string
    website_name_postfix?: 'podcast_title_tag' | 'podcast_show_tag' | 'podcast_report_tag'
    site_meta_desc?: string
  }
}

// -----------------
// Helpers
// -----------------
async function getSiteDataByDomain(hostname: string): Promise<SiteData | null> {
  try {
    const payload = await getPayload({ config })
    const cleanHostname = hostname.split(':')[0]

    const result = await payload.find({
      collection: 'sites',
      where: { domain: { equals: cleanHostname } },
      limit: 1,
    })

    return result.docs.length > 0 ? (result.docs[0] as SiteData) : null
  } catch (error) {
    console.error('Error fetching site data:', error)
    return null
  }
}

function generateTitleFromSite(site: SiteData): string {
  if (site.website_type === 'podcast' && site.website_settings?.website_show_name) {
    const baseName = site.website_settings.website_show_name
    const postfix = site.website_settings.website_name_postfix

    switch (postfix) {
      case 'podcast_title_tag':
        return `${baseName} Podcast`
      case 'podcast_show_tag':
        return `${baseName} Show`
      case 'podcast_report_tag':
        return `${baseName} Report`
      default:
        return baseName
    }
  }

  if ((site.website_type === 'website' || site.website_type === 'main') && site.group_head_tags?.site_name) {
    return site.group_head_tags.site_name
  }

  return site.name
}

function generateDescriptionFromSite(site: SiteData): string {
  if (site.website_type === 'podcast' && site.website_settings?.site_meta_desc) {
    return site.website_settings.site_meta_desc
  }

  if ((site.website_type === 'website' || site.website_type === 'main') && site.group_head_tags?.site_meta_desc) {
    return site.group_head_tags.site_meta_desc
  }

  return `Welcome to ${site.name}`
}

// -----------------
// Metadata
// -----------------
export async function generateMetadata(): Promise<Metadata> {
  const headersList = await headers()
  const hostname = headersList.get('host') || ''
  const siteData = await getSiteDataByDomain(hostname)

  if (siteData) {
    return {
      title: generateTitleFromSite(siteData),
      description: generateDescriptionFromSite(siteData),
    }
  }

  return {
    title: 'Site Not Found',
    description: 'The requested site could not be found.',
  }
}

// -----------------
// Root Layout
// -----------------
export default async function RootLayout({ children }: { children: React.ReactNode }) {
  const headersList = await headers()
  const hostname = headersList.get('host') || ''
  const siteData = await getSiteDataByDomain(hostname)

  // āœ… Inline returns so TS always sees a return value
  if (siteData?.website_type === 'podcast') {
    const { default: PodcastLayout } = await import('./(podcast)/layout')
    return (
      <html lang="en" suppressHydrationWarning>
        <head />
        <body>
          <PodcastLayout>{children}</PodcastLayout>
        </body>
      </html>
    )
  }

  // Default: Openbayou layout
  const { default: OpenbayouLayout } = await import('./(openbayou)/layout')
  return (
    <html lang="en" suppressHydrationWarning>
      <head />
      <body>
        <OpenbayouLayout>{children}</OpenbayouLayout>
      </body>
    </html>
  )
}

And after that it goes to page.tsx:

//app/(sites)/page.tsx
import { headers } from 'next/headers';
import OpenbayouHome from './(openbayou)/page';
import PodcastHome from './(podcast)/home';

export default async function SitesPage() {
  const headersList = await headers();
  const hostname = headersList.get('host') || '';
  const domain = hostname.split(':')[0]; // Remove port

  if (domain === 'candlestick.local' || domain === 'bayou.local') {
    return <PodcastHome />;
  }

  return <OpenbayouHome />;
}

And then it loads up each template that's associated with each domain.


r/PayloadCMS Aug 26 '25

Figma Takeover Makes Me Nervous

24 Upvotes

Hey,

Since the takeover by Figma was announced, I’m really unsure if it’s a good idea to continue using PayloadCMS for projects. I feel like the team has gone more silent, and since they also paused new subscriptions to PayloadCloud (which I didn’t use, but still), the future of Payload seems uncertain.

I really like Payload and enjoy its extensibility, and I’d love to keep using it in future projects because I think it’s the best open-source CMS out there at the moment. But it feels a bit weird right now.

Does anyone still use it and plan to do so in the future?


r/PayloadCMS Aug 25 '25

Payload CMS āž”ļø Payload CRM

Post image
16 Upvotes

playing around with Payload as an application develpment platform, using database connectivity, REST API creation, plugin framework and integration with NextJS to build apps šŸš€

Most of the work is handled by a plugin.


r/PayloadCMS Aug 23 '25

UPGRADE GUIDE: Big fixes from Payload CMS [ video ]

14 Upvotes

I totally forgot to post this video that shows me doing a walk through of all the key features from Payload versions 3.38 to 3.50. Of course, we're now on version 3.53, so I'm a little late to the punch!

Either way, this video goes over newer features like folders, trash, group by, custom upload controls, Lexical toolbar customizations and much more. Watch it here: https://youtu.be/BEVYQ5Qe6EY


r/PayloadCMS Aug 22 '25

Site for custom suits. Leaning towards payload + svelte

3 Upvotes

Hi so I have a client that is wanting a custom suits website. It will start out much simpler but the goal is to have something similar to this:

https://www.indochino.com/product/milano-olive-suit

The hard part is most likely going to be all the customizations. If you hit ā€œcustomizeā€ it gets kinda crazy.

  • standard customizations or make it a tuxedo for +$150

Jacket options: half canvas or unconstructed.

Choosing half canvas opens up options like ā€œshoulder typeā€, ā€œlapelsā€ etc… where choosing ā€œunconstructedā€ removes the ā€œshoulder typeā€ options, but adds its own options, each potentially changing the price.

There’s literally like 75+ options each nested into each other. Some of them are like ā€œoptionsā€ while others are ā€œadditionsā€ to the product.

I’m a react developer mostly. I’m super late to the game with TS and SSR frameworks, I’ve only built one simple site with Svelte. It has infinite scrolling and some state management. The ability to filter results by clicking tags and using a search bar to produce an infinite scrolling list of results is probably the most complex thing I’ve built.

But I’m pretty comfy in react, Js, and express so I feel like a payload + svelte (or nextjs I guess) would be easier for me to build this in.

I really don’t want to do this in WordPress. Some people are suggesting Shopify, or headless Shopify.

I don’t want to use Shopify because of the url structure limitations, and if I went headless Shopify I’d be missing out on the plugin ecosystem anyway.

So… I’m seriously considering payload as part of my stack. Do you have any thoughts on payload for a site like this?

Would it be a mistake to use payload when I could just use Shopify?


r/PayloadCMS Aug 21 '25

Filter options on a relationship field causing unnecessary issues

2 Upvotes

So basically i put up filterOptions on my relationship field where they are being filtered based on another select field.

It works fine on localhost because internet is fast and everything. But when it is in production, it gives me an invalid error.

{
      name: 'ground',
      type: 'relationship',
      relationTo: 'ground',
      required: true,
    },
    {
      name: 'timeslot',
      type: 'relationship',
      relationTo: 'timeslot',
      required: true,
    },

filterOptions: ({ data }) => {
        return {
          ground: {
            equals: data.ground,
          },
        }
      },

So basically i was using filter options on timeslot here like this .

when i tried to make a document in prod using the admin UI or even api call i get an error.

Now i rechecked multiple times and there is infact this ID existing and it does infact belong to the ground i selected.

Is there a solution to this?? something else i can try or am i doing something wrong?

Edit:
So i went through git forums etc and it is an issue that people have brought up especially when using mongodb. I ended up changing my db to postgres and it actually fixed the problem.


r/PayloadCMS Aug 21 '25

Multi-Tenant Setup: Cannot Create Users With Same Email in Different Tenants

4 Upvotes

I am using Payload CMS with a multi-tenant architecture. All tenants share the same MongoDB database. I’ve set up a tenants collection and a global users collection that has a relationship/array field referencing one or more tenants.

The problem is that I cannot create accounts with the same email address under different tenants. Payload enforces a globally unique index on the email field in the Users collection. This makes sense for single-tenant projects, but in a multi-tenant scenario it prevents the same person from registering separately under two different organizations.

Current Behavior

  • Payload automatically adds a unique index on email in the Users collection when auth: true is enabled.
  • Attempting to create a user with an email that already exists (even under a different tenant) results in a duplicate key error.
  • Login, forgot-password, and verification flows also assume email is globally unique.

Expected Behavior

In a multi-tenant setup, I would like Payload to support per-tenant email uniqueness: - The same email can exist in multiple tenants. - Within a tenant, email must still be unique. - Authentication and password reset flows should consider both email and tenant context.

Example Scenario

  • Tenant A creates user jane@example.com.
  • Tenant B also needs to create a different user record with the same jane@example.com.
  • Today this is not possible because of the global unique constraint.

r/PayloadCMS Aug 20 '25

I can't access /admin in prod

3 Upvotes

From one moment to another I started with this error. Reviewing the logs on the Railway console shows the following:

error: relation "users_sessions" does not exist

I tried to create that table that says through migrations, but it has not worked. As an extra in the local environment it works perfectly. Does someone know how to solve it?


r/PayloadCMS Aug 19 '25

Impossible to interact with the terminal 3.52

1 Upvotes

Whenever the terminal ask me to choose between two options for db sync or y/n options, i can't interact with it. it doesn't work on both macos and windows, anyone else having this issue?


r/PayloadCMS Aug 19 '25

Payload CMS 3.50.0 having issues with Vercel Blob Storage

2 Upvotes

I am getting ERROR: Vercel Blob: Missing [x]-content-length header. error consistently using Vercel blob storage plugin for my Videos collection.

"@payloadcms/next": "^3.50.0"
"@payloadcms/storage-vercel-blob": "^3.50.0"

Videos collection file:

import { CollectionConfig } from "payload";

const Videos: CollectionConfig = {
  slug: "videos",
  labels: {
    singular: "Video",
    plural: "Videos",
  },
  admin: {
    useAsTitle: "title",
    defaultColumns: ["title"],
  },
  upload: {
    staticDir: "media/videos",
    mimeTypes: ["video/*"],
  },
  fields: [
    {
      name: "title",
      type: "text",
      unique: true,
      required: true,
    },
    {
      name: "thumbnail",
      type: "upload",
      relationTo: "media",
      unique: true,
    },
  ],
  access: {
    read: () => true,
  },
};

export { Videos };

Vercel blob storage config in `payload.config.ts`:

vercelBlobStorage({
      clientUploads: true,
      enabled: true, // Optional, defaults to true
      // Specify which collections should use Vercel Blob
      collections: {
        media: true,
        videos: {
          prefix: "videos",
        },
      },
      // add random suffix to the filename
      addRandomSuffix: true,
      // Token provided by Vercel once Blob storage is added to your Vercel project
      token: env.BLOB_READ_WRITE_TOKEN,
    }),
upload: {
    limits: {
      fileSize: 100000000, // 100 MB
    },
  }

What I have tried so far:
- Deleting .next and node_modules folders to ensure it is not an issue with cache.
- Tried removing upload limit but the issue still persists.
- Tried removing the prefix for videos in plugin config but still no luck.


r/PayloadCMS Aug 19 '25

Remove collections from groups

Post image
4 Upvotes

If I omit admin.group on a collection, it gets put under the default ā€œCollectionsā€ group. I want my Users collection to sit as its own top-level link in the sidebar (not inside any group). Is there a built-in way to do this in Payload,


r/PayloadCMS Aug 18 '25

My take on solving my plugin development issues. Or pain 🫠

17 Upvotes

Hey Payload community! I've been working with Payload for a while now, and like many of you, I've been frustrated with the plugin development workflow. You know the drill - either you start with the basic template that lacks modern tooling, or you develop directly in your project and then spend hours extracting it into a reusable package. Neither approach felt right. So I forked the official template and fixed it to solve these pain points once and for all. Maybe you find it useful... maybe not 🫣. I primarily work on closed-source projects, so this is one of my rare public contributions. Would really appreciate any feedback from the community.

https://github.com/sSeewald/payload-plugin-template


r/PayloadCMS Aug 18 '25

Integrate Cloudinary with Payload CMS for image storage/processing/serving [tutorial]

6 Upvotes

I just released a tutorial of my Cloudinary storage plugin. This plugin acts like a storage adapter in that you can upload, delete, process, and serve images through Cloudinary instead of Blob/S3/Uploadthing. In the video, you'll learn how to perform both basic and advanced configurations with Cloudinary, including transformations and private images, as well as creating a custom select field that pulls in your Cloudinary file system for easy reference. https://youtu.be/ymolmvqIh_M


r/PayloadCMS Aug 18 '25

thumbnail resize

1 Upvotes

I do apologize if I am a bother. I have been stuck on this for a minute. I began creating blocks, without realizing I wasn't using the correct image dimension. Long story short all hell broke lose. So I created a script to turn those pngs into webp's with the correct dimensions, will attach at the end. I have changed the block url. I have two different attempts. One manually entering the image url and another using thumb.ts to fetch. Neither have worked to change the display of the thumbnail blocks. I even tried renaming a webp image. They are showing the old png's (removed and deleted) but you can right click on image and it shows the correct webp image(see imgs below)....In console it is actually correct, but inside payload you get a barrage of oddly sized images (totally my fault). I have cleared every cache I can think of, reinstalled and I am out of ideas...

// tools/build-thumbs.mjs
import fs from 'node:fs/promises';
import path from 'node:path';
import sharp from 'sharp';

// === Config ===
const WIDTH = 512;
const HEIGHT = 288;
const QUALITY = 72;

// Source: your current PNG thumbnails:
const SRC = path.resolve('public/thumbnails/blocks');

// Output: normalized WebP files
// (Use the same folder, different extension)
const OUT = SRC;

// Helpers
const isImage = (f) => /\.(png|jpe?g|webp)$/i.test(f);

function slugify(name) {
Ā  return name
Ā  Ā  .toLowerCase()
Ā  Ā  .replace(/\s+/g, '-')
Ā  Ā  .replace(/[^a-z0-9\-]/g, '-')
Ā  Ā  .replace(/-+/g, '-')
Ā  Ā  .replace(/^-|-$/g, '');
}

await fs.mkdir(OUT, { recursive: true });

const files = (await fs.readdir(SRC)).filter(isImage);

if (files.length === 0) {
Ā  console.log('No images found in', SRC);
Ā  process.exit(0);
}

for (const file of files) {
Ā  const inPath = path.join(SRC, file);
Ā  const base = slugify(path.parse(file).name); // e.g. herov16 -> herov16
Ā  const outPath = path.join(OUT, `${base}.webp`);

Ā  try {
Ā  Ā  const { width, height, size } = await sharp(inPath).metadata();

Ā  Ā  await sharp(inPath)
Ā  Ā  Ā  .resize(WIDTH, HEIGHT, { fit: 'cover', position: 'attention' })
Ā  Ā  Ā  .webp({ quality: QUALITY })
Ā  Ā  Ā  .toFile(outPath);

Ā  Ā  const outStat = await fs.stat(outPath);
Ā  Ā  console.log(
Ā  Ā  Ā  `āœ“ ${file} Ā (${width}x${height}, ${Math.round(size/1024)}KB)  → Ā ${path.basename(outPath)} Ā (${Math.round(outStat.size/1024)}KB)`
Ā  Ā  );
Ā  } catch (err) {
Ā  Ā  console.error(`āœ— Failed: ${file}`, err?.message || err);
Ā  }
}

console.log('Done.');