Skip to main content

Setup

terminal
pnpm add @dcmx-studio/framalab-sdk
.env.local
FRAMALAB_URL=https://panel.yourdomain.com
FRAMALAB_TOKEN=your-gallery-token

Singleton client

lib/framalab.ts
import { createFramalabClient } from "@dcmx-studio/framalab-sdk"

export const framalab = createFramalabClient({
  baseUrl: process.env.FRAMALAB_URL!,
  token: process.env.FRAMALAB_TOKEN!,
})
app/gallery/page.tsx
import { framalab } from "@/lib/framalab"
import Image from "next/image"

export default async function GalleryPage() {
  const [project, photos] = await Promise.all([
    framalab.getProject(),
    framalab.getPhotos(),
  ])

  return (
    <main>
      <h1>{project.name}</h1>

      <div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-2">
        {photos.map((photo) => {
          const thumb = photo.versions.find(v => v.versionType === "webp_thumb")
          if (!thumb) return null
          return (
            <div key={photo.id} className="aspect-square overflow-hidden">
              <Image
                src={thumb.url}
                alt=""
                width={thumb.width ?? 400}
                height={thumb.height ?? 400}
                className="w-full h-full object-cover"
              />
            </div>
          )
        })}
      </div>
    </main>
  )
}

Collections navigation

app/gallery/collections/page.tsx
import { framalab } from "@/lib/framalab"
import Link from "next/link"

export default async function CollectionsPage() {
  const collections = await framalab.getCollections()

  return (
    <nav>
      <ul className="flex gap-4">
        {collections.map((col) => (
          <li key={col.id}>
            <Link href={`/gallery/collections/${col.id}`}>
              {col.name}
            </Link>
          </li>
        ))}
      </ul>
    </nav>
  )
}

Collection detail page

app/gallery/collections/[id]/page.tsx
import { framalab } from "@/lib/framalab"
import { FramalabError } from "@dcmx-studio/framalab-sdk"
import Image from "next/image"
import { notFound } from "next/navigation"

export default async function CollectionPage({
  params,
}: {
  params: Promise<{ id: string }>
}) {
  const { id } = await params

  let collection
  try {
    collection = await framalab.getCollection(id)
  } catch (err) {
    if (err instanceof FramalabError && err.status === 404) notFound()
    throw err
  }

  return (
    <main>
      <h1>{collection.name}</h1>
      <div className="columns-2 md:columns-3 gap-2">
        {collection.photos.map((photo) => {
          const medium = photo.versions.find(v => v.versionType === "webp_medium")
          if (!medium) return null
          return (
            <Image
              key={photo.id}
              src={medium.url}
              alt=""
              width={medium.width ?? 1200}
              height={medium.height ?? 800}
              className="w-full mb-2"
            />
          )
        })}
      </div>
    </main>
  )
}

next.config.ts

next.config.ts
import type { NextConfig } from "next"

const panelHostname = new URL(process.env.FRAMALAB_URL!).hostname

const nextConfig: NextConfig = {
  images: {
    remotePatterns: [
      {
        protocol: "https",
        hostname: panelHostname,
      },
    ],
  },
}

export default nextConfig
All data fetching happens server-side — the gallery token is never sent to the browser.