Developer Guide

Conventions every page and component in this repo follows. Read this before adding a new route or component.

1. Folder Structure

Colocation vs. global — pick based on how many routes need the component. Reach for the tiers below top to bottom: start in a route's own _components/, and only promote a component to components/ once a second route actually needs it.

iskcon-website/
├── next.config.js               # Image remotePatterns (S3), Sass include paths
├── package.json
│
├── app/                         # App Router — all routing happens here
│   ├── layout.jsx               # Root layout: persistent global Header & Footer
│   ├── page.jsx                 # Homepage: lean orchestrator, composes sections
│   │
│   ├── _components/             # PRIVATE — used only by app/page.jsx
│   │   └── homepage/
│   │       ├── HomeBanner.jsx   # One file per homepage section
│   │       ├── AboutSection.jsx
│   │       └── ...              # 12 sections total, same pattern
│   │
│   └── products/                # Dynamic product routes
│       ├── page.jsx             # Product catalog page
│       ├── _components/
│       │   └── ProductCatalog.jsx
│       └── [id]/
│           ├── page.jsx         # Product detail page
│           └── _components/
│               └── ProductDetail.jsx
│
├── components/                  # GLOBAL — reusable across 2+ routes
│   ├── Header.jsx                # Renders once, in app/layout.jsx
│   ├── Footer.jsx                 # Renders once, in app/layout.jsx
│   ├── SectionHeading.jsx        # Shared heading block, used by most sections
│   ├── DynamicCTA.jsx
│   ├── ProductCard.jsx            # + CourseCard, DonationCard, FestivalCard, ...
│   │
│   └── ui/                       # Atomic primitives — no business logic
│       ├── CustomButton.jsx      # The button component
│       ├── OptimizedImage.jsx    # next/image + the Zero-Blink pipeline
│       └── Skeleton.jsx
│
├── lib/                          # Server-side data/logic, shared across routes
│   ├── products.js
│   └── templeData.js
│
└── styles/
    ├── style.scss                # Imports Bootstrap source + every partial below
    ├── _variables.scss           # Theme colors/fonts/spacing — edit here, not ad hoc CSS
    ├── _mixins.scss              # Breakpoint mixins
    └── _*.scss                   # One partial per homepage section (e.g. _join-us.scss)

2. Fonts

Three typefaces, each scoped to one role. All are registered once in app/layout.jsx via next/font/google and exposed globally as CSS variables on the <html> element — never load a font anywhere else.

  • Montserrat (var(--font-montserrat)) — the global default, applied to * and body in styles/_init.scss. Every element inherits it unless overridden.
  • Cinzel (var(--font-cinzel)) — the small eyebrow .tag label above section headings.
  • EB Garamond (var(--font-eb-garamond)) — the heading font: every h1/h2/h3 inside a .heading block (rendered via SectionHeading), and also the footer link column headings.
Montserrat — Sri Sri Radha MadhavaCinzel — ABOUT ISKCON TEMPLEEB Garamond — Discover the Joy of Krishna Consciousness

Importing a new font

Three steps, all in app/layout.jsx — never install a font package or add a <link> tag.

// 1. import + configure
import { Poppins } from "next/font/google";

const poppins = Poppins({
  variable: "--font-poppins",
  subsets: ["latin"],
  weight: ["400", "600"],
});

// 2. add its variable to the <html> className
<html className={`${montserrat.variable} ${poppins.variable} ...`}>

// 3. reference it in any .scss file
h4 {
  font-family: var(--font-poppins);
}

3. Colors

8 solid colors and 10 gradients, defined once in styles/_variables.scss. Change the site's look by editing values here — never hardcode a hex code in component SCSS.

$white-color#fff
$primary-color#a8282f
$dark-primary#430609
$secondary-color#f3b11e
$light-secondary#fdebb4
$light-bg#fcf6eb
$text-color#6e6559
$bold-color#55413e

Gradients

$light-primary-gradient
$dark-primary-gradient
$dark2-primary-gradient
$dark2-primary-gradient-inverse
$light-secondary-gradient
$primary-yellow-button
$dark-secondary-gradient
$bg-gradient-left-right
$bg-gradient-top-bottom
$text-gradient

4. Using OptimizedImage

Every image passes blurDataURL in from a server-side fetch or a precomputed constant — never generated on the client — and picks one of two sizing modes.

aspectRatio (fluid fill mode) — vs. — width/height (fixed mode)

Both are supported by components/ui/OptimizedImage.jsx, but they solve different problems — pick based on whether the image's box is defined by its content or by its container.

  • aspectRatio — use for photos where the layout dictates the box (hero banners, cards, carousel slides). The image stretches with fill to whatever box the ratio class describes (e.g. "ratio-16x9", "ratio-4x3", "ratio-1x1") and is cropped with objectFit="cover" (the default) to fill it. Always pass a real sizes matching the rendered width per breakpoint — the browser can't know how big the box will be otherwise, and a wrong sizes means an oversized download.
  • width / height — use for anything with a genuine fixed pixel size that must not stretch or crop: icons, logos, avatars. Pass the real intrinsic (or intended) pixel dimensions. If the icon isn't square, also set objectFit="contain" (fixed mode itself doesn't crop, but a mismatched box can still squash a non-square source) — sizes is irrelevant here since the box never changes size.
One caveat if you reuse aspectRatio fill mode inside a container that already has its own non-standard CSS aspect ratio (not a plain ratio-Nx N): the wrapper always carries Bootstrap's position-relative utility, which compiles with !important. If your own CSS needs the wrapper to be position: absolute instead (e.g. to fill an already-sized ancestor), your override needs !important too, or it silently loses.

Above-the-fold hero banner

Full-bleed, likely the Largest Contentful Paint element: set priority to skip lazy-loading, and sizes="100vw" since it always spans the viewport width.

<OptimizedImage
  src={hero.imageUrl}
  alt="Devotees gathered for the Janmashtami celebration"
  blurDataURL={hero.blurDataURL} // fetched/computed server-side
  aspectRatio="ratio-21x9"
  sizes="100vw"
  priority
  className="w-100"
/>

Fixed-size icon

No blurDataURL needed for small static art — the blur-in fade only makes sense when there's a real photo underneath.

<OptimizedImage
  src={tickIcon}
  alt="Tick"
  width={19}
  height={19}
/>

5. CustomButton

components/ui/CustomButton.jsx is the one button component — every button on the site should use it instead of a raw <button> or Bootstrap's btn classes directly.

  • variant — one of primary, secondary, primaryOutline, secondaryOutline, transparent. Anything else passed is used as-is for a one-off style.
  • type defaults to "button" — set type="submit" explicitly inside forms.
  • All other props (onClick, disabled, …) pass straight through to the underlying <button>.

Live sample

<CustomButton variant="primary">Register</CustomButton>
<CustomButton variant="secondary" onClick={handleClick}>
  Support the festival
</CustomButton>
<CustomButton type="submit" variant="primary" className="w-100">
  Submit
</CustomButton>

6. SectionHeading

components/SectionHeading.jsx is the shared heading block for every section — the small eyebrow tag, the <h2>, and the supporting paragraph underneath it.

  • tag — optional small eyebrow label above the heading (e.g. "ABOUT ISKCON TEMPLE").
  • heading — the actual heading element, passed in already wrapped (usually an <h2>) so callers control the tag and any inline <span> styling.
  • description — supporting copy underneath, also passed in already wrapped (<p> or a fragment of several).
  • classes — extra class names merged onto the wrapping .heading div for section-specific spacing tweaks.

Live sample

ABOUT ISKCON TEMPLE

Discover the Joy of Krishna Consciousness

Welcome to the temple of Sri Sri Radha Madhava…

<SectionHeading
  tag="ABOUT ISKCON TEMPLE"
  heading={<h2>Discover the Joy of Krishna Consciousness</h2>}
  description={<p>Welcome to the temple of Sri Sri Radha Madhava…</p>}
/>

7. Creating a New Section

Every homepage-style section follows the same shell. Copy this, rename the class, and fill in the content — don't invent a new outer structure.


import SectionHeading from "@/components/SectionHeading";

const YourSection = () => {
  return (
    <section className="your-section-name">
      <div className="container">
        <SectionHeading
          tag="OPTIONAL EYEBROW TAG"
          heading={<h2>Your Section Heading</h2>}
          description={<p>One or two lines of supporting copy.</p>}
        />

        {/* section content goes here */}
      </div>
    </section>
  );
};

export default YourSection;

Then add a matching styles/_your-section-name.scss partial scoped under .your-section-name, and add one @import "your-section-name"; line to styles/style.scss — following the same pattern as the existing sections (e.g. _join-us.scss, _upcoming-festival.scss).