Skip to content

perf: performance optimisations#3

Merged
onerandomdevv merged 2 commits into
devfrom
feature/performance
May 3, 2026
Merged

perf: performance optimisations#3
onerandomdevv merged 2 commits into
devfrom
feature/performance

Conversation

@onerandomdevv
Copy link
Copy Markdown
Contributor

@onerandomdevv onerandomdevv commented May 3, 2026

What does this PR do?

  • Cloudinary getOptimisedUrl() helper with context transforms
  • ISR revalidate=3600 on all public pages
  • Parallel DB queries with Promise.all()
  • Selective column fetching on all list pages
  • Full OG and Twitter metadata on all public pages
  • robots.ts blocking /admin and /api from crawlers
  • sitemap.ts exposing all public and dynamic routes
  • All content images using next/image

Type of change

  • Feature
  • Bug fix
  • Config / setup
  • Refactor
  • Docs

Checklist

  • I have read CLAUDE.md
  • pnpm build passes locally with no errors
  • No TypeScript errors (pnpm tsc --noEmit)
  • No hardcoded secrets or API keys
  • All new API routes check for admin session before executing
  • No UI libraries were installed
  • Fonts are loaded via next/font/google only
  • pnpm was used (not npm or yarn)

Summary by CodeRabbit

Release Notes

New Features

  • Added robots.txt and sitemap.xml support to the website
  • Added comprehensive page metadata including titles, descriptions, Open Graph and Twitter card fields to all public pages

Performance

  • Implemented image URL optimization for Cloudinary uploads

- Cloudinary getOptimisedUrl() helper with context transforms
- ISR revalidate=3600 on all public pages
- Parallel DB queries with Promise.all()
- Selective column fetching on all list pages
- Full OG and Twitter metadata on all public pages
- robots.ts blocking /admin and /api from crawlers
- sitemap.ts exposing all public and dynamic routes
- All content images using next/image
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 3, 2026

📝 Walkthrough

Walkthrough

This PR adds ISR configuration and metadata exports across public pages, optimizes database queries with explicit column projections, introduces image URL optimization via getOptimisedUrl, creates new SEO routes (robots.ts, sitemap.ts), and narrows component prop types via new Pick-based types. Documentation in AGENTS.md and CLAUDE.md is substantially rewritten with design constraints, folder structure, and Home page layout specifications.

Changes

ISR, Metadata, Query Optimization & Image URL Enhancement

Layer / File(s) Summary
Type System & Data Shapes
src/components/sections/ProductsSection.tsx, src/components/sections/LatestReleasesSection.tsx, src/components/blog/UpdatesList.tsx, src/app/(public)/careers/page.tsx, src/app/(public)/team/page.tsx
New Pick-based types (ProductSummary, LatestReleasePost, TeamMemberSummary, OpenRole) and expanded UpdateListPost with cover_url field to define reduced data shapes for components and fetch results.
Image URL Optimization
src/lib/cloudinary.ts, src/components/admin/ImageUpload.tsx
Added exported getOptimisedUrl(url) helper to rewrite Cloudinary URLs with auto format/quality; moved Cloudinary SDK import to runtime inside uploadToCloudinary. ImageUpload component updated to use optimized URLs when rendering uploaded images.
ISR & Metadata Configuration
src/app/(public)/page.tsx, src/app/(public)/about/page.tsx, src/app/(public)/blog/page.tsx, src/app/(public)/blog/[slug]/page.tsx, src/app/(public)/products/page.tsx, src/app/(public)/products/[slug]/page.tsx, src/app/(public)/careers/page.tsx, src/app/(public)/contact/page.tsx, src/app/(public)/team/page.tsx
All public pages now export revalidate = 3600 for ISR and define page title/description constants used to export typed metadata objects with OpenGraph and Twitter card configuration; removed prior force-dynamic exports.
Database Query Optimization
src/app/(public)/page.tsx, src/app/(public)/products/page.tsx, src/app/(public)/team/page.tsx, src/app/(public)/careers/page.tsx, src/app/(public)/blog/page.tsx, src/app/(public)/blog/[slug]/page.tsx, src/app/(public)/products/[slug]/page.tsx, src/app/admin/blog/page.tsx, src/app/admin/careers/page.tsx, src/app/admin/dashboard/page.tsx, src/app/admin/products/page.tsx, src/app/admin/team/page.tsx
Database queries changed from bare .select() to explicit column projections; error handling simplified to return empty arrays or null without logging; product/post fetches include optimized image selection.
Image Optimization in Pages
src/app/(public)/blog/[slug]/page.tsx, src/app/(public)/products/[slug]/page.tsx, src/app/(public)/team/page.tsx, src/app/(public)/blog/page.tsx
Pages now use getOptimisedUrl(cover_url) or getOptimisedUrl(photo_url) for image rendering; generateMetadata helpers use optimized URLs for OpenGraph and Twitter card images.
Component Prop Updates
src/components/sections/ProductsSection.tsx, src/components/sections/LatestReleasesSection.tsx
Components now accept narrowed prop types (ProductSummary[], LatestReleasePost[]) instead of full entity types.
SEO Routes
src/app/robots.ts, src/app/sitemap.ts
Added robots.ts with allow/disallow rules and sitemap URL; added sitemap.ts with revalidate = 3600 that concurrently fetches products and published blog posts, constructs dynamic sitemap entries with lastModified from updated_at, and falls back to static routes on query failure.
Documentation Updates
AGENTS.md, CLAUDE.md
Rewrote design-system rules (light-theme-only, no animations/gradients, professional/minimal), clarified logo and folder structure guidance, expanded database schema documentation with explicit column lists and enum defaults, specified API endpoint categories, and detailed Home page layout (Hero, Products, Latest Releases, Recognition, About Teaser); removed hackathon content references and added security policy bullets.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • feat: complete frontend build #2: Introduces public pages and components (about/page.tsx, careers/page.tsx, blog/, products/[slug]/page.tsx, sections) that this PR refines with metadata/revalidate exports, query optimization, and type narrowing across the same files.
  • Feature/backend setup #1: Establishes the schema and initial component structure for products/team/blog that this PR depends on; both PRs coordinate frontend page and database query changes in overlapping modules.

Poem

🐰 A rabbit hops through metadata fields,
ISR caches all the yields,
Images optimized, queries lean,
Best-performing site you've seen!
Sitemap and robots dance in place,
SEO blooms with grace. 🌿✨

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title 'perf: performance optimisations' is vague and generic, using non-descriptive language that does not convey specific information about the actual changes in the changeset. Replace with a specific title highlighting the main change, such as 'perf: add ISR revalidation, image optimization, and sitemap generation' or 'perf: enable ISR and optimize images with Cloudinary helper'.
✅ Passed checks (3 passed)
Check name Status Explanation
Description check ✅ Passed The description provides a bulleted list of changes and completes all required checklist items, but lacks detail about the motivation, impact, and rationale behind the performance optimizations.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/performance

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
Review rate limit: 0/1 reviews remaining, refill in 60 minutes.

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🧹 Nitpick comments (5)
AGENTS.md (1)

392-399: 💤 Low value

Add language identifier to fenced code block.

The code block showing blog post layout structure lacks a language identifier, which affects syntax highlighting and linter compliance.

📝 Proposed fix
-```
+```text
 [Cover image — full width, 1200x630px]
 CATEGORY BADGE
 Title (JetBrains Mono, H1)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@AGENTS.md` around lines 392 - 399, The fenced code block that shows the blog
post layout lacks a language identifier; update the opening triple-backtick for
that block (the snippet starting with "[Cover image — full width, 1200x630px]")
to include a language tag such as "text" (i.e., change ``` to ```text) so
linters and syntax highlighters recognize the block.
src/app/sitemap.ts (1)

17-57: ⚡ Quick win

Add export const revalidate = 3600 to cache the sitemap.

Without it, sitemap() is dynamically rendered — every crawler request (and Next.js ISR revalidation of other pages) triggers a fresh DB round-trip. All other public pages in this PR export revalidate = 3600; the sitemap should do the same.

⚡ Proposed fix
 import type { MetadataRoute } from "next";
 import { desc, eq } from "drizzle-orm";
 import { blogPosts, db, products } from "@/db";
+
+export const revalidate = 3600;

 const baseUrl = "https://codeddevs.com";

As per coding guidelines: "ISR — add to all public pages: export const revalidate = 3600".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/sitemap.ts` around lines 17 - 57, Add a top-level export for ISR by
declaring export const revalidate = 3600 in this module so the sitemap()
function is cached for 1 hour; update src/app/sitemap.ts to include the export
alongside the existing default export (sitemap) to prevent dynamic rendering and
avoid repeated DB queries on each crawler request.
src/lib/cloudinary.ts (1)

36-36: ⚡ Quick win

getOptimisedUrl applied at upload time bakes transformation strings into the DB — prefer applying it only at render time.

resolve(getOptimisedUrl(result.secure_url)) stores …/upload/f_auto,q_auto/… in the database. Every render-side caller already applies getOptimisedUrl independently, so the DB write is redundant. The bigger concern is forward compatibility: if you later want to add dpr_auto or swap quality settings, every stored URL would require a data migration rather than a one-line code change.

Returning the raw secure_url here and relying solely on render-time transformation keeps the DB agnostic to Cloudinary delivery parameters.

♻️ Proposed fix
-        resolve(getOptimisedUrl(result.secure_url));
+        resolve(result.secure_url);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/cloudinary.ts` at line 36, The code currently calls
getOptimisedUrl(result.secure_url) before resolving the upload promise, which
bakes Cloudinary transformation params into stored URLs; change the upload flow
so the promise resolves with the raw result.secure_url instead (i.e.,
resolve(result.secure_url) in the upload handler) and remove any stored
application of getOptimisedUrl in the upload path so that render-time callers
can apply getOptimisedUrl when needed; update any references to the upload
promise return value accordingly if they assumed transformed URLs.
src/app/(public)/products/[slug]/page.tsx (1)

38-50: ⚡ Quick win

Deduplicate the DB call shared between generateMetadata and ProductPage using React.cache().

Same pattern as src/app/(public)/blog/[slug]/page.tsx: getProductBySlug is invoked in both generateMetadata (line 72) and ProductPage (line 108), resulting in two identical Drizzle queries per render. To deduplicate database calls, extract the function and wrap it with React.cache() — this applies equally to the generateMetadata / page component pair.

♻️ Proposed fix
+import { cache } from "react";
 ...
-async function getProductBySlug(slug: string) {
+const getProductBySlug = cache(async (slug: string) => {
   try {
     const [product] = await db
       .select()
       .from(products)
       .where(eq(products.slug, slug))
       .limit(1);

     return product ?? null;
   } catch {
     return null;
   }
-}
+});
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/`(public)/products/[slug]/page.tsx around lines 38 - 50, The
getProductBySlug function is called twice (in generateMetadata and ProductPage)
causing duplicate DB queries — wrap this function with React.cache and use the
cached version in both generateMetadata and ProductPage to dedupe calls;
specifically, import React if needed, create a cached function (e.g., const
getProductBySlugCached = React.cache(async function getProductBySlug(...){...}))
or wrap the existing getProductBySlug with React.cache, keep the same signature
and error handling, and replace both usages (generateMetadata and ProductPage)
to call the cached function.
src/app/(public)/blog/[slug]/page.tsx (1)

36-48: ⚡ Quick win

Deduplicate the DB call shared between generateMetadata and UpdatePage using React.cache().

getPublishedPostBySlug is called twice per render pass — once in generateMetadata (line 71) and again in UpdatePage (line 107). Because this uses Drizzle (not fetch), automatic deduplication does not apply to ORM/database calls; React cache can be used if fetch is unavailable. Without this, every ISR regeneration or on-demand render fires two identical queries.

♻️ Proposed fix
+import { cache } from "react";
 ...
-async function getPublishedPostBySlug(slug: string) {
+const getPublishedPostBySlug = cache(async (slug: string) => {
   try {
     const [post] = await db
       .select()
       .from(blogPosts)
       .where(and(eq(blogPosts.slug, slug), eq(blogPosts.is_published, true)))
       .limit(1);

     return post ?? null;
   } catch {
     return null;
   }
-}
+});
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/`(public)/blog/[slug]/page.tsx around lines 36 - 48, Wrap the
existing getPublishedPostBySlug function with React.cache so the same DB query
is deduplicated between generateMetadata and UpdatePage; import React if needed
and replace/export the cached version (e.g. const getPublishedPostBySlug =
React.cache(async function getPublishedPostBySlug(slug: string) { ... })) so
both generateMetadata and UpdatePage call the cached function without changing
call sites, leaving the internal Drizzle query unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/app/`(public)/blog/[slug]/page.tsx:
- Line 10: Replace the direct import of getOptimisedUrl from
"@/lib/cloudinary-url" with the canonical re-export from "@/lib/cloudinary";
locate the import statement that currently imports getOptimisedUrl in the Blog
page component (page.tsx) and update its source to "@/lib/cloudinary" so the
component uses the vetted helper re-export.

In `@src/app/`(public)/contact/page.tsx:
- Around line 1-6: This page is missing the ISR revalidate export; add an
exported constant `revalidate` set to 3600 in this module (page.tsx) alongside
the existing top-level symbols such as `title`, `description`, and the
`ContactForm` import so the page uses 1-hour incremental static regeneration
(export const revalidate = 3600).

In `@src/app/`(public)/products/[slug]/page.tsx:
- Line 8: Replace the direct import of getOptimisedUrl from
"@/lib/cloudinary-url" with the sanctioned helper from "@/lib/cloudinary":
update the import statement that currently references getOptimisedUrl to import
it from "@/lib/cloudinary" instead, ensuring any usages of getOptimisedUrl in
this file (page component for product slug) remain unchanged and continue to
resolve correctly against the new module.

In `@src/app/`(public)/team/page.tsx:
- Line 6: The import is pulling getOptimisedUrl from "@/lib/cloudinary-url"
instead of the canonical helper; update the import in
src/app/(public)/team/page.tsx to import getOptimisedUrl from "@/lib/cloudinary"
(replace the existing import statement that references "@/lib/cloudinary-url")
so the component uses the shared helper function getOptimisedUrl from
cloudinary.ts.

In `@src/components/admin/ImageUpload.tsx`:
- Line 6: Update the import in ImageUpload.tsx to use the centralized helper:
replace the current import of getOptimisedUrl from "@/lib/cloudinary-url" with
an import from "@/lib/cloudinary" so the component uses the canonical
getOptimisedUrl helper; locate the import statement that references
getOptimisedUrl and change its module specifier accordingly (no other code
changes needed).

---

Nitpick comments:
In `@AGENTS.md`:
- Around line 392-399: The fenced code block that shows the blog post layout
lacks a language identifier; update the opening triple-backtick for that block
(the snippet starting with "[Cover image — full width, 1200x630px]") to include
a language tag such as "text" (i.e., change ``` to ```text) so linters and
syntax highlighters recognize the block.

In `@src/app/`(public)/blog/[slug]/page.tsx:
- Around line 36-48: Wrap the existing getPublishedPostBySlug function with
React.cache so the same DB query is deduplicated between generateMetadata and
UpdatePage; import React if needed and replace/export the cached version (e.g.
const getPublishedPostBySlug = React.cache(async function
getPublishedPostBySlug(slug: string) { ... })) so both generateMetadata and
UpdatePage call the cached function without changing call sites, leaving the
internal Drizzle query unchanged.

In `@src/app/`(public)/products/[slug]/page.tsx:
- Around line 38-50: The getProductBySlug function is called twice (in
generateMetadata and ProductPage) causing duplicate DB queries — wrap this
function with React.cache and use the cached version in both generateMetadata
and ProductPage to dedupe calls; specifically, import React if needed, create a
cached function (e.g., const getProductBySlugCached = React.cache(async function
getProductBySlug(...){...})) or wrap the existing getProductBySlug with
React.cache, keep the same signature and error handling, and replace both usages
(generateMetadata and ProductPage) to call the cached function.

In `@src/app/sitemap.ts`:
- Around line 17-57: Add a top-level export for ISR by declaring export const
revalidate = 3600 in this module so the sitemap() function is cached for 1 hour;
update src/app/sitemap.ts to include the export alongside the existing default
export (sitemap) to prevent dynamic rendering and avoid repeated DB queries on
each crawler request.

In `@src/lib/cloudinary.ts`:
- Line 36: The code currently calls getOptimisedUrl(result.secure_url) before
resolving the upload promise, which bakes Cloudinary transformation params into
stored URLs; change the upload flow so the promise resolves with the raw
result.secure_url instead (i.e., resolve(result.secure_url) in the upload
handler) and remove any stored application of getOptimisedUrl in the upload path
so that render-time callers can apply getOptimisedUrl when needed; update any
references to the upload promise return value accordingly if they assumed
transformed URLs.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 15cc8d01-5583-4a9f-877a-00d2203dbc44

📥 Commits

Reviewing files that changed from the base of the PR and between 7c1cc16 and 4d529f4.

📒 Files selected for processing (24)
  • AGENTS.md
  • CLAUDE.md
  • src/app/(public)/about/page.tsx
  • src/app/(public)/blog/[slug]/page.tsx
  • src/app/(public)/blog/page.tsx
  • src/app/(public)/careers/page.tsx
  • src/app/(public)/contact/page.tsx
  • src/app/(public)/page.tsx
  • src/app/(public)/products/[slug]/page.tsx
  • src/app/(public)/products/page.tsx
  • src/app/(public)/team/page.tsx
  • src/app/admin/blog/page.tsx
  • src/app/admin/careers/page.tsx
  • src/app/admin/dashboard/page.tsx
  • src/app/admin/products/page.tsx
  • src/app/admin/team/page.tsx
  • src/app/robots.ts
  • src/app/sitemap.ts
  • src/components/admin/ImageUpload.tsx
  • src/components/blog/UpdatesList.tsx
  • src/components/sections/LatestReleasesSection.tsx
  • src/components/sections/ProductsSection.tsx
  • src/lib/cloudinary-url.ts
  • src/lib/cloudinary.ts

Comment thread src/app/(public)/blog/[slug]/page.tsx Outdated
Comment thread src/app/(public)/contact/page.tsx
Comment thread src/app/(public)/products/[slug]/page.tsx Outdated
Comment thread src/app/(public)/team/page.tsx Outdated
Comment thread src/components/admin/ImageUpload.tsx Outdated
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Nitpick comments (1)
src/lib/cloudinary.ts (1)

21-31: 💤 Low value

Consider splitting getOptimisedUrl into its own module instead of using new Function for bundler isolation.

new Function("return import('cloudinary')") correctly prevents webpack from statically analysing the cloudinary import (so it isn't bundled into server-component chunks that only need getOptimisedUrl). However, moving getOptimisedUrl — a zero-dependency pure string function — to a separate file (e.g. src/lib/cloudinary-url.ts) would achieve the same goal more cleanly, allow uploadToCloudinary to use a direct import { v2 as cloudinary } from 'cloudinary', and remove the need for the type cast.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/cloudinary.ts` around lines 21 - 31, The current use of new
Function("return import('cloudinary')") to avoid bundling should be replaced by
moving the pure string helper getOptimisedUrl into its own module so cloudinary
can be imported normally; create a new file (e.g., export getOptimisedUrl from
src/lib/cloudinary-url.ts) and update call sites to import that helper directly,
then in the uploadToCloudinary function import { v2 as cloudinary } from
'cloudinary' and remove the new Function wrapper and the type cast, keeping
cloudinary.config(...) and publicId computation as-is and updating any
references to getOptimisedUrl to the new module.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@AGENTS.md`:
- Around line 299-302: Add a blank line immediately before the "### Admin (401
if no session)" heading's table so the Markdown table starting with the row "|
Method | Route | Description |" is surrounded by a blank line to satisfy MD058;
locate the "### Admin (401 if no session)" heading and insert one empty line
above the table that lists POST `/api/upload`.
- Around line 444-458: Documentation inaccurately states getOptimisedUrl(appends
context-specific transforms) but the implementation in src/lib/cloudinary.ts
only inserts f_auto/q_auto and its signature is (url: string | null | undefined)
with no context; either update AGENTS.md to reflect current behavior or change
getOptimisedUrl to accept an optional context parameter and apply the right
transforms. To fix in code: modify getOptimisedUrl to (url: string | null |
undefined, context?: string | undefined), add a small mapping of context keys
(e.g., "blogCover","recognitionCard","blogThumbnail","teamPhoto","productCover")
to transform strings using slash-separated format (e.g.,
"f_auto/q_auto/w_1200,h_630,c_fill" and include "g_face" for teamPhoto), then
inject the chosen transform into Cloudinary delivery URLs where getOptimisedUrl
currently injects f_auto/q_auto; alternatively, if you choose the documentation
route, update AGENTS.md to state the helper only inserts f_auto/q_auto (use
slash-separated syntax) and mark context-specific transforms as not yet applied.

In `@src/lib/cloudinary.ts`:
- Around line 1-15: getOptimisedUrl currently only applies f_auto,q_auto and
lacks context-specific transforms; modify getOptimisedUrl to accept an optional
context parameter (e.g., "team-photo", "blog-cover", "product-cover"), map each
context to the Cloudinary transformation strings documented in AGENTS.md
(team-photo -> f_auto,q_auto,w_400,h_400,c_fill,g_face; blog-cover/product-cover
-> f_auto,q_auto,w_1200,h_630,c_fill), apply the full transform when replacing
"/upload/" (while preserving existing early-return checks for non-Cloudinary
URLs and already-optimised URLs), and update all call sites (e.g.,
team/page.tsx, blog/[slug]/page.tsx) to pass the appropriate context so images
receive the correct crop/size/gravity.
- Line 14: The current getOptimisedUrl function returns url.replace("/upload/",
"/upload/f_auto,q_auto/") which uses invalid Cloudinary syntax and lacks
context-specific transforms; update getOptimisedUrl to accept a context
parameter (e.g., "blogCover" | "productCover" | "teamPhoto"), replace the comma
with a slash so the base optimization is "/upload/f_auto/q_auto/", and append
context-specific transformation strings per AGENTS.md (for example for
blog/product covers add "w_1200,h_630,c_fill" and for team photos add
"w_400,h_400,c_fill,g_face"); ensure the function composes the final replacement
segment (e.g., "/upload/{transformations}/f_auto/q_auto/") and update any
callers of getOptimisedUrl to pass the appropriate context.

---

Nitpick comments:
In `@src/lib/cloudinary.ts`:
- Around line 21-31: The current use of new Function("return
import('cloudinary')") to avoid bundling should be replaced by moving the pure
string helper getOptimisedUrl into its own module so cloudinary can be imported
normally; create a new file (e.g., export getOptimisedUrl from
src/lib/cloudinary-url.ts) and update call sites to import that helper directly,
then in the uploadToCloudinary function import { v2 as cloudinary } from
'cloudinary' and remove the new Function wrapper and the type cast, keeping
cloudinary.config(...) and publicId computation as-is and updating any
references to getOptimisedUrl to the new module.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 63783876-3a98-435c-b5c4-23a95bbb6fb8

📥 Commits

Reviewing files that changed from the base of the PR and between 4d529f4 and a33e02e.

📒 Files selected for processing (8)
  • AGENTS.md
  • src/app/(public)/blog/[slug]/page.tsx
  • src/app/(public)/contact/page.tsx
  • src/app/(public)/products/[slug]/page.tsx
  • src/app/(public)/team/page.tsx
  • src/app/sitemap.ts
  • src/components/admin/ImageUpload.tsx
  • src/lib/cloudinary.ts
✅ Files skipped from review due to trivial changes (1)
  • src/components/admin/ImageUpload.tsx
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/app/sitemap.ts
  • src/app/(public)/products/[slug]/page.tsx

Comment thread AGENTS.md
Comment on lines +299 to +302
### Admin (401 if no session)
| Method | Route | Description |
|---|---|---|
| POST | `/api/upload` | Upload image to Cloudinary |
| POST | `/api/upload` | Upload to Cloudinary |
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Add a blank line before the Admin table to satisfy MD058.

The static analysis tool (markdownlint-cli2) flags this table as missing surrounding blank lines, which can cause some Markdown renderers to fail to parse it as a table.

✏️ Proposed fix
 ### Admin (401 if no session)
+
 | Method | Route | Description |
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
### Admin (401 if no session)
| Method | Route | Description |
|---|---|---|
| POST | `/api/upload` | Upload image to Cloudinary |
| POST | `/api/upload` | Upload to Cloudinary |
### Admin (401 if no session)
| Method | Route | Description |
|---|---|---|
| POST | `/api/upload` | Upload to Cloudinary |
🧰 Tools
🪛 markdownlint-cli2 (0.22.1)

[warning] 300-300: Tables should be surrounded by blank lines

(MD058, blanks-around-tables)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@AGENTS.md` around lines 299 - 302, Add a blank line immediately before the
"### Admin (401 if no session)" heading's table so the Markdown table starting
with the row "| Method | Route | Description |" is surrounded by a blank line to
satisfy MD058; locate the "### Admin (401 if no session)" heading and insert one
empty line above the table that lists POST `/api/upload`.

Comment thread AGENTS.md
Comment on lines +444 to +458
### Cloudinary URL transformations
The `getOptimisedUrl()` helper in `src/lib/cloudinary.ts` appends
transformations automatically. Never use raw Cloudinary URLs directly.

```ts
// Context-specific transformations:
Blog cover banner: f_auto,q_auto,w_1200,h_630,c_fill
Recognition card: f_auto,q_auto,w_600,h_315,c_fill
Blog list thumbnail: f_auto,q_auto,w_800,h_420,c_fill
Team photo: f_auto,q_auto,w_400,h_400,c_fill,g_face
Product cover: f_auto,q_auto,w_1200,h_630,c_fill
```

`g_face` on team photos tells Cloudinary to focus the crop on the face.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Documentation claims context-specific transforms are applied by getOptimisedUrl, but the implementation doesn't support them.

The section states the helper "appends transformations automatically" and shows context-specific transform strings (w_1200,h_630,c_fill, w_400,h_400,c_fill,g_face, etc.), implying the function selects transforms based on call-site context. The actual getOptimisedUrl signature is (url: string | null | undefined): string — no context parameter — and it inserts only f_auto,q_auto regardless of where it's called.

Additionally, the transform strings shown throughout this block use the comma-separated form f_auto,q_auto, which is the same invalid syntax flagged in src/lib/cloudinary.ts.

This documentation should be corrected to accurately describe what the helper currently does, or updated in sync with the context-parameter fix proposed for cloudinary.ts.

✏️ Corrected documentation (minimal fix, no implementation change)
-### Cloudinary URL transformations
-The `getOptimisedUrl()` helper in `src/lib/cloudinary.ts` appends
-transformations automatically. Never use raw Cloudinary URLs directly.
-
-```ts
-// Context-specific transformations:
-Blog cover banner:      f_auto,q_auto,w_1200,h_630,c_fill
-Recognition card:       f_auto,q_auto,w_600,h_315,c_fill
-Blog list thumbnail:    f_auto,q_auto,w_800,h_420,c_fill
-Team photo:             f_auto,q_auto,w_400,h_400,c_fill,g_face
-Product cover:          f_auto,q_auto,w_1200,h_630,c_fill
-```
+### Cloudinary URL transformations
+The `getOptimisedUrl()` helper in `src/lib/cloudinary.ts` inserts `f_auto/q_auto`
+into Cloudinary delivery URLs. Never use raw Cloudinary URLs directly.
+
+Context-specific dimension and crop transforms (`w_`, `h_`, `c_fill`, `g_face`)
+are **not yet applied** — tracked for a follow-up.
+
+Target transforms per context (implement via an optional `context` parameter):
+```ts
+Blog cover banner:      f_auto/q_auto/w_1200,h_630,c_fill
+Recognition card:       f_auto/q_auto/w_600,h_315,c_fill
+Blog list thumbnail:    f_auto/q_auto/w_800,h_420,c_fill
+Team photo:             f_auto/q_auto/w_400,h_400,c_fill,g_face
+Product cover:          f_auto/q_auto/w_1200,h_630,c_fill
+```
🧰 Tools
🪛 LanguageTool

[grammar] ~445-~445: Ensure spelling is correct
Context: ... ### Cloudinary URL transformations The getOptimisedUrl() helper in src/lib/cloudinary.ts ap...

(QB_NEW_EN_ORTHOGRAPHY_ERROR_IDS_1)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@AGENTS.md` around lines 444 - 458, Documentation inaccurately states
getOptimisedUrl(appends context-specific transforms) but the implementation in
src/lib/cloudinary.ts only inserts f_auto/q_auto and its signature is (url:
string | null | undefined) with no context; either update AGENTS.md to reflect
current behavior or change getOptimisedUrl to accept an optional context
parameter and apply the right transforms. To fix in code: modify getOptimisedUrl
to (url: string | null | undefined, context?: string | undefined), add a small
mapping of context keys (e.g.,
"blogCover","recognitionCard","blogThumbnail","teamPhoto","productCover") to
transform strings using slash-separated format (e.g.,
"f_auto/q_auto/w_1200,h_630,c_fill" and include "g_face" for teamPhoto), then
inject the chosen transform into Cloudinary delivery URLs where getOptimisedUrl
currently injects f_auto/q_auto; alternatively, if you choose the documentation
route, update AGENTS.md to state the helper only inserts f_auto/q_auto (use
slash-separated syntax) and mark context-specific transforms as not yet applied.

Comment thread src/lib/cloudinary.ts
Comment on lines +1 to +15
export function getOptimisedUrl(url: string | null | undefined): string {
if (!url) {
return "";
}

cloudinary.config({
cloud_name: process.env.NEXT_PUBLIC_CLOUDINARY_CLOUD_NAME,
api_key: process.env.CLOUDINARY_API_KEY,
api_secret: process.env.CLOUDINARY_API_SECRET,
});
if (!url.includes("res.cloudinary.com") || !url.includes("/upload/")) {
return url;
}

if (url.includes("/upload/f_auto,q_auto/")) {
return url;
}

return url.replace("/upload/", "/upload/f_auto,q_auto/");
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

getOptimisedUrl applies only f_auto/q_auto — context-specific dimension and crop transforms documented in AGENTS.md are not implemented.

AGENTS.md (updated in this same PR, lines 448–455) documents that the helper applies context-specific transforms:

Team photo:     f_auto,q_auto,w_400,h_400,c_fill,g_face
Blog cover:     f_auto,q_auto,w_1200,h_630,c_fill
Product cover:  f_auto,q_auto,w_1200,h_630,c_fill

The current implementation ignores all dimension/crop/gravity parameters. Concretely:

  • Team photos will not get face-focused cropping (g_face) — profile photos may be cropped at arbitrary positions.
  • All images are served at original resolution without a width/height constraint from Cloudinary's CDN layer (Next.js Image handles display-side resizing, but the bytes transferred from Cloudinary to Vercel are unoptimised).

The function signature accepts no context parameter, so callers have no mechanism to request context-specific transforms.

🛠️ Proposed fix — add an optional context parameter
+type CloudinaryContext =
+  | "blog-cover"
+  | "blog-thumbnail"
+  | "recognition"
+  | "team-photo"
+  | "product-cover";
+
+const contextTransforms: Record<CloudinaryContext, string> = {
+  "blog-cover":     "f_auto/q_auto/w_1200,h_630,c_fill",
+  "blog-thumbnail": "f_auto/q_auto/w_800,h_420,c_fill",
+  "recognition":    "f_auto/q_auto/w_600,h_315,c_fill",
+  "team-photo":     "f_auto/q_auto/w_400,h_400,c_fill,g_face",
+  "product-cover":  "f_auto/q_auto/w_1200,h_630,c_fill",
+};
+
-export function getOptimisedUrl(url: string | null | undefined): string {
+export function getOptimisedUrl(
+  url: string | null | undefined,
+  context?: CloudinaryContext,
+): string {
   if (!url) {
     return "";
   }

   if (!url.includes("res.cloudinary.com") || !url.includes("/upload/")) {
     return url;
   }

+  const transforms = context ? contextTransforms[context] : "f_auto/q_auto";
-  if (url.includes("/upload/f_auto,q_auto/")) {
+  if (url.includes(`/upload/${transforms}/`)) {
     return url;
   }

-  return url.replace("/upload/", "/upload/f_auto,q_auto/");
+  return url.replace("/upload/", `/upload/${transforms}/`);
 }

All call sites would then pass context:

// team/page.tsx
src={getOptimisedUrl(member.photo_url, "team-photo")}

// blog/[slug]/page.tsx
src={getOptimisedUrl(post.cover_url, "blog-cover")}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/cloudinary.ts` around lines 1 - 15, getOptimisedUrl currently only
applies f_auto,q_auto and lacks context-specific transforms; modify
getOptimisedUrl to accept an optional context parameter (e.g., "team-photo",
"blog-cover", "product-cover"), map each context to the Cloudinary
transformation strings documented in AGENTS.md (team-photo ->
f_auto,q_auto,w_400,h_400,c_fill,g_face; blog-cover/product-cover ->
f_auto,q_auto,w_1200,h_630,c_fill), apply the full transform when replacing
"/upload/" (while preserving existing early-return checks for non-Cloudinary
URLs and already-optimised URLs), and update all call sites (e.g.,
team/page.tsx, blog/[slug]/page.tsx) to pass the appropriate context so images
receive the correct crop/size/gravity.

Comment thread src/lib/cloudinary.ts
return url;
}

return url.replace("/upload/", "/upload/f_auto,q_auto/");
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🌐 Web query:

Cloudinary transformation rules: is "f_auto,q_auto" (comma-separated) valid in a single transformation component, or must format and quality always be in separate slash-separated components?

💡 Result:

“f_auto,q_auto” is not the correct syntax for Cloudinary’s single transformation component when you want automatic format and automatic quality together. Cloudinary’s transformation URL grammar is: parameters within a single component are comma-separated, and chained transformation components are separated by forward slashes. For automatic optimization, Cloudinary explicitly requires format and quality to be in separate slash-separated components: use “f_auto/q_auto” (correct) and “f_auto,q_auto” (incorrect). Example (correct): /f_auto/q_auto Example (incorrect): /f_auto,q_auto

Citations:


🏁 Script executed:

# Check the cloudinary.ts file
cat -n src/lib/cloudinary.ts

Repository: coded-devs/website

Length of output: 1874


🏁 Script executed:

# Find all usages of getOptimisedUrl across the codebase
rg "getOptimisedUrl" --type ts --type tsx -A 2 -B 2

Repository: coded-devs/website

Length of output: 90


🏁 Script executed:

# Check if AGENTS.md exists and what it says about transforms
fd -i agents.md

Repository: coded-devs/website

Length of output: 72


🏁 Script executed:

# Try searching for getOptimisedUrl usage with different approach
rg "getOptimisedUrl" -A 2 -B 2

Repository: coded-devs/website

Length of output: 4515


🏁 Script executed:

# Check AGENTS.md for context about transforms
head -500 AGENTS.md | tail -100

Repository: coded-devs/website

Length of output: 4147


🏁 Script executed:

# Or search for the specific section mentioned in original comment (lines 448-457)
sed -n '440,460p' AGENTS.md

Repository: coded-devs/website

Length of output: 904


f_auto,q_auto comma-separated is invalid Cloudinary syntax — must be f_auto/q_auto.

Cloudinary's official documentation explicitly requires format and quality to be in separate slash-separated components: use f_auto/q_auto, never f_auto,q_auto. Comma-separated syntax causes the quality parameter to be treated as a qualifier to format rather than as a standalone optimization, silently delivering unoptimized images.

This affects all image renders: blog covers, team photos, and product covers across the site.

Additionally, getOptimisedUrl() implements only generic f_auto,q_auto but AGENTS.md documents context-specific transformations (dimensions, crop gravity, etc.) that should be applied per context:

  • Blog/product covers: w_1200,h_630,c_fill
  • Team photos: w_400,h_400,c_fill,g_face (face-focused crop)

The function accepts no context parameter, so these transformations are never applied. Images are served at original resolution without context-specific sizing.

Fix required
  1. Change syntax from comma to slash:
-  if (url.includes("/upload/f_auto,q_auto/")) {
+  if (url.includes("/upload/f_auto/q_auto/")) {
     return url;
   }

-  return url.replace("/upload/", "/upload/f_auto,q_auto/");
+  return url.replace("/upload/", "/upload/f_auto/q_auto/");
  1. Extend getOptimisedUrl() to accept context parameter and apply documented context-specific transforms.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/cloudinary.ts` at line 14, The current getOptimisedUrl function
returns url.replace("/upload/", "/upload/f_auto,q_auto/") which uses invalid
Cloudinary syntax and lacks context-specific transforms; update getOptimisedUrl
to accept a context parameter (e.g., "blogCover" | "productCover" |
"teamPhoto"), replace the comma with a slash so the base optimization is
"/upload/f_auto/q_auto/", and append context-specific transformation strings per
AGENTS.md (for example for blog/product covers add "w_1200,h_630,c_fill" and for
team photos add "w_400,h_400,c_fill,g_face"); ensure the function composes the
final replacement segment (e.g., "/upload/{transformations}/f_auto/q_auto/") and
update any callers of getOptimisedUrl to pass the appropriate context.

@onerandomdevv onerandomdevv merged commit a05dcc9 into dev May 3, 2026
4 checks passed
@onerandomdevv onerandomdevv deleted the feature/performance branch May 6, 2026 17:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant