docs
    agency-site-kit: Sections and devices
    v0.11.1.md

    Sections and devices

    A case study is a list of sections: a hero, the challenge, the strategy, results, a testimonial and so on. This page covers reading them in the right order, pulling out one section's data with its type, borrowing the client's brand colour, and deciding whether the hero shows one laptop or three devices.

    Everything here is pure and imports from @sonordev/agency-site-kit/portfolio.

    What a section looks like

    PortfolioItemFull.sections is an array of PortfolioSection:

    interface PortfolioSection<T extends PortfolioSectionType = PortfolioSectionType> {
      sectionType: T; // 'portfolioHero', 'portfolioResults', ...
      displayName: string;
      data: T extends keyof PortfolioSectionDataMap ? PortfolioSectionDataMap[T] : PortfolioSectionData;
    }

    In an array of sections, data is the union of every shape, so check sectionType and cast (as the example below does), or use sectionData.

    PORTFOLIO_SECTION_TYPES lists all 21 section types. Eighteen have a typed data shape in PortfolioSectionDataMap (see Types). portfolioSpeedComparison, portfolioSiteArchitecture and portfolioDesignSystem don't yet, so read their data defensively.

    Reading order: orderedSections

    orderedSections(item: Pick<PortfolioItemFull, 'sections' | 'sectionsOrdered'>): PortfolioSection[]
    • If someone reordered the sections in the Sonor dashboard (sectionsOrdered is true), their order is the story, and it's kept. The hero is still pinned first, because it's the top of the page.
    • Otherwise you get the canonical order below. A section type the kit doesn't know sorts last.

    It returns a new array and never changes the item.

    OrderSection typeOrderSection type
    0portfolioHero11portfolioTechStack
    1portfolioChallenges12portfolioDesignSystem
    2portfolioBeforeAfter13portfolioServices
    3portfolioStrategy14portfolioTeam
    4portfolioResults15portfolioTestimonial
    5portfolioPerformance16portfolioMetricsTimeline
    6portfolioSpeedComparison17portfolioConversionFunnel
    7portfolioFeatureSpotlight18portfolioDetails
    8portfolioGallery19portfolioSeo
    9portfolioVideo20portfolioCTA
    10portfolioSiteArchitecture

    The same numbers are exported as PORTFOLIO_SECTION_ORDER, a record keyed by section type.

    A renderer is a loop over the ordered sections with a switch on the type. Return null for types you don't render yet:

    import {
      curatePortfolioProof,
      orderedSections,
      type PortfolioItemFull,
      type PortfolioSection,
      type PortfolioSectionDataMap,
    } from '@sonordev/agency-site-kit/portfolio';
    
    type Data<T extends keyof PortfolioSectionDataMap> = PortfolioSectionDataMap[T];
    
    export function CaseStudy({ item }: { item: PortfolioItemFull }) {
      const curated = curatePortfolioProof(item);
      return (
        <article>
          {orderedSections(curated).map((section, i) => (
            <Section key={`${section.sectionType}-${i}`} section={section} />
          ))}
        </article>
      );
    }
    
    function Section({ section }: { section: PortfolioSection }) {
      switch (section.sectionType) {
        case 'portfolioChallenges': {
          const data = section.data as Data<'portfolioChallenges'>;
          return (
            <section>
              <h2>The challenge</h2>
              {data.items.map((c) => (
                <p key={c.title}>{c.description}</p>
              ))}
            </section>
          );
        }
        // ...one case per section you render
        default:
          return null;
      }
    }

    Run curatePortfolioProof first, so the hero and results already follow the proof rules.

    One section's data: sectionData

    sectionData(item, type): PortfolioSectionDataMap[type] | undefined

    The data of the item's first section of that type, typed for it, or undefined when the case study doesn't have one. Handy when a page needs one section outside the main loop, like a hero above everything else or a testimonial in the sidebar:

    import { sectionData } from '@sonordev/agency-site-kit/portfolio';
    
    const hero = sectionData(item, 'portfolioHero');
    const quote = sectionData(item, 'portfolioTestimonial');

    The client's colour: projectBrandColor

    projectBrandColor(sections: PortfolioSection[] | null | undefined): string | undefined

    The client's own brand colour, read from the case study's Design System section: the colour whose context mentions a button, link, primary or brand, else the first colour listed. It's undefined when there's no Design System section or it has no colours.

    Use it for one thing: the light behind the device stage, so each case study glows in its client's colour. Everything else on the page stays in your agency's colours.

    const glow = projectBrandColor(item.sections);
    
    <div
      className="device-stage"
      style={glow ? ({ '--stage-glow': glow } as React.CSSProperties) : undefined}
    >
      {/* devices */}
    </div>

    One laptop or three devices

    A website case study usually shows a laptop, a tablet and a phone. Some case studies should show one laptop instead: software with no real phone view, or a product behind a login that only has a hand-taken desktop screenshot. Two functions make that call, and Sonor makes the same call when it draws the case study's social card, so the post and the page always show the same picture.

    import { resolveSoloScreenshot, shouldRenderSoloDevice } from '@sonordev/agency-site-kit/portfolio';
    
    const input = {
      category: item.category,
      screenshots: item.hero_screenshots,
      heroImage: item.hero_image,
    };
    
    if (shouldRenderSoloDevice(input)) {
      const screen = resolveSoloScreenshot(input); // defined whenever solo is true
      // render one laptop showing `screen`
    } else {
      // render the three-device stage from item.hero_screenshots
    }

    shouldRenderSoloDevice is true when there's something to show and either:

    • the screenshots are desktop only (no tablet and no mobile shot), or
    • the category is software, not a website.

    It's false when there's no image at all: an empty laptop is worse than whatever your fallback is. A live URL doesn't change the answer, because framing a product behind a login shows its sign-in screen.

    resolveSoloScreenshot returns the desktop screenshot, else heroImage, else undefined. Render exactly what it returns: it's the image the decision was made on.

    isSoftwareCategory(category) is the category half on its own. These count as software once lowercased and slugified (so Application Development, application_development and application-development all match): application-development, app-development, software-development, software, saas, mobile-app, mobile-app-development, web-application and platform.

    SoloDeviceInput is the input type: category, screenshots and heroImage, all optional and nullable.

    Case studies that cover several sites

    PortfolioItem.showcase_sites is set when one case study covers more than one site. With two or more entries, each has its own name, url and screenshots, so each device frame can show its own site. Absent, or fewer than two entries, means a single site: use hero_screenshots.

    To show a client's live site inside a frame instead of a screenshot, see Live device frames.