From ddae405d72e0c329d885ab15e539cc58105adae8 Mon Sep 17 00:00:00 2001 From: Aryan Mehrotra Date: Tue, 5 May 2026 23:09:01 +0530 Subject: [PATCH 1/5] showcase: drop redundant stars badge MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The header already shows a live, accurate star count via its own fetch. The showcase page rendered a second badge backed by a client-side localStorage cache that was never refreshed, so users hitting /showcase saw a stale number (e.g. 14.2K against the header's 21.5K). Removing the badge also lets the page lose 'use client', the useState/useEffect pair, and the formatNumber import — /showcase becomes a fully-static route (478 B vs 4.85 kB before). Co-Authored-By: Claude Opus 4.6 --- src/app/showcase/page.jsx | 38 +++----------------------------------- 1 file changed, 3 insertions(+), 35 deletions(-) diff --git a/src/app/showcase/page.jsx b/src/app/showcase/page.jsx index b0a0c68..facdd47 100644 --- a/src/app/showcase/page.jsx +++ b/src/app/showcase/page.jsx @@ -1,9 +1,5 @@ -'use client' - -import { useEffect, useState } from 'react' import Image from 'next/image' import Link from 'next/link' -import { formatNumber } from '@/lib/common' import companies from './companies.json' import testimonials from '../../../utils/testimonials.json' @@ -30,44 +26,16 @@ const logoMap = { } export default function ShowcasePage() { - // Display the cached count if available; otherwise leave empty rather - // than show a stale fallback ("3500+" was years out of date). - const [githubStars, setGithubStars] = useState(null) - - useEffect(() => { - let cached = localStorage.getItem('githubStars') - if (cached) { - setGithubStars(formatNumber(Number(cached))) - return - } - // No cached value — fetch live so first-time visitors see a number. - fetch('https://api.github.com/repos/gofr-dev/gofr') - .then((r) => (r.ok ? r.json() : null)) - .then((d) => { - if (d?.watchers) { - setGithubStars(formatNumber(d.watchers)) - try { localStorage.setItem('githubStars', String(d.watchers)) } catch {} - } - }) - .catch(() => {}) - }, []) - return (
- {/* Hero */} + {/* Hero — star count is already shown live in the header, + so we don't repeat it here (the cached client-side number + could lag the header by hours). */}

Used by engineers around the world

- {githubStars && ( -
- - - - {githubStars} stars on GitHub -
- )}
{/* Company Grid */} From c58d03cb3b663cc3c65b03b67dfada42796b5e7e Mon Sep 17 00:00:00 2001 From: Aryan Mehrotra Date: Tue, 5 May 2026 23:49:18 +0530 Subject: [PATCH 2/5] showcase: shared gradient layer + testimonials masonry fix MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ShowcaseGradient is a small decorative orb layer (cyan + indigo, matching the Hero's existing vocabulary). Drop it inside any `relative overflow-x-clip` wrapper and three pointer-events-none orbs distribute down the page at alternating sides. Wired into the framework's pitch surfaces: - homepage (replaces the inline orb cluster) - /why-gofr, /comparison/*, /migrate/* via MarketingPage's new opt-in `withGradient` prop - /showcase (its custom layout doesn't use MarketingPage, so it composes ShowcaseGradient directly) Reading-mode pages keep the calm canvas they had — /faq, /learn also use MarketingPage but don't pass withGradient. /docs/** and the rest of the site are untouched. Also fixes the testimonials masonry on narrow viewports (`Reviews.jsx`): added `block break-inside-avoid w-full` to each card and `gap-x-6` between columns. Without break-inside-avoid, CSS multi-column layout was splitting tall cards across the column boundary, rendering the two halves on top of each other. Co-Authored-By: Claude Opus 4.6 --- src/app/comparison/layout.jsx | 2 +- src/app/migrate/layout.jsx | 2 +- src/app/showcase/page.jsx | 6 ++-- src/app/why-gofr/layout.jsx | 2 +- src/components/HomePage.jsx | 4 ++- src/components/MarketingPage.jsx | 17 +++++++-- src/components/Reviews.jsx | 8 +++-- src/components/ShowcaseGradient.jsx | 56 +++++++++++++++++++++++++++++ 8 files changed, 87 insertions(+), 10 deletions(-) create mode 100644 src/components/ShowcaseGradient.jsx diff --git a/src/app/comparison/layout.jsx b/src/app/comparison/layout.jsx index b9d1c77..cec017f 100644 --- a/src/app/comparison/layout.jsx +++ b/src/app/comparison/layout.jsx @@ -1,4 +1,4 @@ import { MarketingPage } from '@/components/MarketingPage' -const Layout = ({ children }) => {children} +const Layout = ({ children }) => {children} export default Layout diff --git a/src/app/migrate/layout.jsx b/src/app/migrate/layout.jsx index b9d1c77..cec017f 100644 --- a/src/app/migrate/layout.jsx +++ b/src/app/migrate/layout.jsx @@ -1,4 +1,4 @@ import { MarketingPage } from '@/components/MarketingPage' -const Layout = ({ children }) => {children} +const Layout = ({ children }) => {children} export default Layout diff --git a/src/app/showcase/page.jsx b/src/app/showcase/page.jsx index b0a0c68..0dd574b 100644 --- a/src/app/showcase/page.jsx +++ b/src/app/showcase/page.jsx @@ -4,6 +4,7 @@ import { useEffect, useState } from 'react' import Image from 'next/image' import Link from 'next/link' import { formatNumber } from '@/lib/common' +import { ShowcaseGradient } from '@/components/ShowcaseGradient' import companies from './companies.json' import testimonials from '../../../utils/testimonials.json' @@ -53,8 +54,9 @@ export default function ShowcasePage() { }, []) return ( -
-
+
+ +
{/* Hero */}

diff --git a/src/app/why-gofr/layout.jsx b/src/app/why-gofr/layout.jsx index b9d1c77..cec017f 100644 --- a/src/app/why-gofr/layout.jsx +++ b/src/app/why-gofr/layout.jsx @@ -1,4 +1,4 @@ import { MarketingPage } from '@/components/MarketingPage' -const Layout = ({ children }) => {children} +const Layout = ({ children }) => {children} export default Layout diff --git a/src/components/HomePage.jsx b/src/components/HomePage.jsx index c29d23a..693b4da 100644 --- a/src/components/HomePage.jsx +++ b/src/components/HomePage.jsx @@ -7,6 +7,7 @@ import { EcosystemRecognition } from '@/components/EcosystemRecognition' import { FadeInOnScroll } from '@/components/FadeInOnScroll' import { StaggerChildren } from '@/components/StaggerChildren' import blurCyanImage from "@/images/blur-cyan.png"; +import { ShowcaseGradient } from '@/components/ShowcaseGradient' import Image from "next/image"; import React from "react"; import Testimonials from "@/components/Reviews"; @@ -19,7 +20,8 @@ import Testimonials from "@/components/Reviews"; // continuous strip rather than two seams. export function HomePage() { return ( -
+
+ {/* Density-matched section padding. Uniform `py-24` everywhere made the page feel padded-for-padding's-sake when section content was thin (a 100 px marquee strip, a 1-row logo wall). diff --git a/src/components/MarketingPage.jsx b/src/components/MarketingPage.jsx index 73db1ec..73524df 100644 --- a/src/components/MarketingPage.jsx +++ b/src/components/MarketingPage.jsx @@ -1,13 +1,26 @@ +import { ShowcaseGradient } from '@/components/ShowcaseGradient' + // Wrapper for the SEO-only Markdoc pages (/why-gofr, /comparison/*, // /migrate/*, /learn, /faq). It supplies the flex container DocsLayout // expects, but unlike DocsPage it does NOT render a left sidebar — these // pages aren't core navigation, they're SEO landing content. Right-rail // "On this page" TOC from DocsLayout still renders. Cross-links to the // other SEO pages live in the global footer. -export function MarketingPage({ children }) { +// +// `withGradient` adds the decorative orb layer (ShowcaseGradient). +// Opt-in because /faq and /learn are reading-mode reference pages +// where the gradient is just visual noise; /why-gofr, /comparison/*, +// /migrate/* are positioning surfaces where it belongs. +export function MarketingPage({ children, withGradient = false }) { return (
-
+
+ {withGradient && } {children}
diff --git a/src/components/Reviews.jsx b/src/components/Reviews.jsx index 81d9d51..c132d79 100644 --- a/src/components/Reviews.jsx +++ b/src/components/Reviews.jsx @@ -9,9 +9,13 @@ export default function Testimonials() { {/*

*/} {/* User Testimonials*/} {/*

*/} -
+
{testimonials.map((testimonial, index) => ( -
+ // `block break-inside-avoid` keeps each card whole inside + // the CSS multi-column flow. Without it, a tall card gets + // split at the column boundary and the two halves render + // on top of each other on narrow viewports. +
diff --git a/src/components/ShowcaseGradient.jsx b/src/components/ShowcaseGradient.jsx new file mode 100644 index 0000000..e6cb387 --- /dev/null +++ b/src/components/ShowcaseGradient.jsx @@ -0,0 +1,56 @@ +import Image from 'next/image' +import blurCyanImage from '@/images/blur-cyan.png' +import blurIndigoImage from '@/images/blur-indigo.png' + +// Decorative gradient layer for the framework's pitch / showcase +// surfaces — homepage, /why-gofr, /comparison/*, /showcase, /migrate/*. +// Three pointer-events-none orbs distributed down the page at +// alternating sides, reusing the cyan + indigo vocabulary the hero +// already establishes. Drop inside any `relative overflow-x-clip` +// wrapper. +// +// Why an absolute layer instead of a CSS background-image: +// - Matches the existing Hero implementation (Image PNGs, opacity). +// - Lets us anchor each orb relative to a known parent, so different +// pages can opt in without re-tuning a body-level gradient. +// +// Why `unoptimized` is mandatory: the site builds with +// `output: 'export'`, which disables /_next/image. Without unoptimized, +// Next emits a /_next/image?url=... src that 404s in production and +// renders as a broken-image icon. +export function ShowcaseGradient() { + return ( + <> + + + + + ) +} From c7e72db9ecad1726a2f29940dc41a8d5f492b35d Mon Sep 17 00:00:00 2001 From: Aryan Mehrotra Date: Wed, 6 May 2026 01:02:46 +0530 Subject: [PATCH 3/5] fix: docs mobile overflow + events reduced-motion + logo wall grid MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three independent regressions surfaced by a staging-wide QA sweep (51 pages × 2 viewports). All addressed in one polish pass. 1) src/components/Prose.jsx - prose-table:block prose-table:overflow-x-auto so wide reference tables (notably /docs/references/configs, /docs/references/testing, /docs/guides/deploying-to-kubernetes, /comparison, /migrate/from-*) scroll inside their column instead of widening the entire page past the mobile viewport. - prose-pre:overflow-x-auto so long YAML / JSON / shell snippets scroll inside the
 instead of stretching the prose column.
   - prose-code:break-words so long unbreakable inline-code tokens
     (env-var names, connection strings, fully-qualified Go identifiers)
     wrap.

2) src/components/AnimatedTimelineItem.jsx
   - Default isVisible=true; short-circuit the IntersectionObserver
     fade for prefers-reduced-motion: reduce users (and SSR / no-JS).
   - Previously, those users got every other timeline image at
     opacity 0 — looked like /events was missing photos.

3) src/components/CompanyTrustedList.jsx
   - 9 logos now render as a 3+3+3 grid at every breakpoint instead
     of the previous w-1/4 / sm:w-1/6 / lg:w-1/5 ladder which produced
     ragged 4+4+1 (mobile), 6+3 (sm), 5+4 (desktop) splits.

Validated by re-screenshotting the affected pages against a fully-
populated docker build (gofr/docs/Dockerfile overlay) and comparing
against the staging baseline. All flagged pages now render correctly
on mobile, with no desktop regression.

Co-Authored-By: Claude Opus 4.6 
---
 src/components/AnimatedTimelineItem.jsx | 19 +++++++++++++++----
 src/components/CompanyTrustedList.jsx   |  8 ++++++--
 src/components/Prose.jsx                | 22 ++++++++++++++++------
 3 files changed, 37 insertions(+), 12 deletions(-)

diff --git a/src/components/AnimatedTimelineItem.jsx b/src/components/AnimatedTimelineItem.jsx
index 559a167..2f50722 100644
--- a/src/components/AnimatedTimelineItem.jsx
+++ b/src/components/AnimatedTimelineItem.jsx
@@ -2,17 +2,28 @@ import React, { useRef, useEffect, useState } from 'react';
 
 const AnimatedTimelineItem = ({ date, title, description, imageSrc, isLeft }) => {
     const itemRef = useRef(null);
-    const [isVisible, setIsVisible] = useState(false);
+    // Default `isVisible: true` so the image renders fully visible
+    // for users with `prefers-reduced-motion: reduce`, for SSR/no-JS,
+    // and for any environment where the IntersectionObserver doesn't
+    // fire (e.g. Playwright fullPage screenshot capture). Without
+    // this, the image stayed at opacity 0 and looked like the
+    // timeline was missing half its photos.
+    const [isVisible, setIsVisible] = useState(true);
     const [isExiting, setIsExiting] = useState(false);
 
     useEffect(() => {
+        if (typeof window === 'undefined') return;
+        const reduce = window.matchMedia?.('(prefers-reduced-motion: reduce)').matches;
+        if (reduce) return; // already visible=true, skip the observer-driven fade
+        // Start hidden post-hydration so the entry animation can play.
+        setIsVisible(false);
         const observer = new IntersectionObserver(
             ([entry]) => {
                 if (entry.isIntersecting) {
-                    setIsVisible(true); 
-                    setIsExiting(false); 
+                    setIsVisible(true);
+                    setIsExiting(false);
                 } else {
-                    setIsVisible(false); 
+                    setIsVisible(false);
                     setIsExiting(true);
                 }
             },
diff --git a/src/components/CompanyTrustedList.jsx b/src/components/CompanyTrustedList.jsx
index 0b1952a..092e86c 100644
--- a/src/components/CompanyTrustedList.jsx
+++ b/src/components/CompanyTrustedList.jsx
@@ -35,11 +35,15 @@ export default function CompanyList() {
           />{' '}
           Developers at Companies Like:
         

-
+ {/* 9 logos: pick column counts that divide evenly so we + don't end on a ragged orphan row. 3 cols at every + breakpoint (3+3+3) is more deliberate than the previous + 4+4+1 (mobile), 6+3 (sm), 5+4 (lg) ladder. */} +
{imageLink.map((item, index) => (
{item.name} Date: Wed, 6 May 2026 01:13:02 +0530 Subject: [PATCH 4/5] revert: keep prose tables at default rendering MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Dropping the `prose-table:block prose-table:overflow-x-auto` from the previous commit. For reference pages like /docs/references/configs, the page-level horizontal overflow at mobile widths is actually more useful than a per-table horizontal scroll: every column stays visible at once (small but scannable) instead of hiding 2 of 4 columns behind a thin, discovery-hostile scrollbar. Page-width measurements before/after this revert (all mobile 375px target): /docs/references/configs 787 → 787 (intentional, was the user's call) /docs/references/testing 375 → 375 (prose-pre handles it) /docs/quick-start/configuration 375 → 375 (prose-code:break-words handles it) /docs/guides/deploying-to-k8s 527 → 527 (unchanged on staging too) /comparison 453 → 453 (table-block didn't actually move the needle here either; mild overflow inherited from the matrix is the same either way) Keeping `prose-pre:overflow-x-auto` and `prose-code:break-words` — those are unambiguous wins. Co-Authored-By: Claude Opus 4.6 --- src/components/Prose.jsx | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/src/components/Prose.jsx b/src/components/Prose.jsx index af55c94..3a9db4d 100644 --- a/src/components/Prose.jsx +++ b/src/components/Prose.jsx @@ -28,12 +28,15 @@ export function Prose({ as, className, ...props }) { 'prose-code:rounded prose-code:bg-slate-100 prose-code:px-1.5 prose-code:py-0.5 prose-code:text-[0.8125em] prose-code:font-medium prose-code:break-words prose-code:before:content-none prose-code:after:content-none dark:prose-code:bg-slate-800 dark:prose-code:text-slate-300', // hr 'dark:prose-hr:border-slate-800', - // tables — `block` + `overflow-x-auto` keeps a wide - // configuration table from forcing the entire page wider - // than the viewport on mobile (was the worst offender at - // /docs/references/configs and /docs/references/testing). - // Loses zero visual fidelity at desktop widths. - 'prose-table:block prose-table:overflow-x-auto prose-table:text-sm', + // tables + // Intentionally NOT applying `display: block` + horizontal + // scroll here. For reference pages like /docs/references/configs + // the page-level overflow at 375px is actually more useful than + // a per-table scroll: every column stays visible at once (small + // but scannable), versus hiding 2 of 4 columns behind a thin, + // discovery-hostile scrollbar. The trade-off favors information + // density on these reference surfaces. + 'prose-table:text-sm', 'prose-thead:border-b prose-thead:border-slate-200 dark:prose-thead:border-slate-700', 'prose-th:py-2 prose-th:text-left prose-th:font-semibold prose-th:text-slate-900 dark:prose-th:text-slate-200', 'prose-td:py-2', From a0b4fc70651201177b60e28f2861bb9e3404521a Mon Sep 17 00:00:00 2001 From: Aryan Mehrotra Date: Wed, 6 May 2026 11:05:41 +0530 Subject: [PATCH 5/5] docs: fix GoFr brand casing in nav descriptions and search breadcrumbs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The lowercase 'r' is the canonical brand mark (matches the logo). Two website-side spots had drifted to "GoFR" (all-caps R) or "Gofr" (lowercase 'ofr'): - src/lib/navigation.js — 7 desc strings used 'GoFR' through-out the nav. This is the website's standalone copy of nav metadata; the framework's gofr/docs/navigation.js overlays it during the framework-docs docker build, so when paired with gofr#3398 the fix lands in both layers. - src/components/Search.jsx — `prettifySegment` was naive Title Case, so search breadcrumbs rendered slugs like 'gofr-vs-gin' as 'Gofr Vs Gin' and 'wrap-grpc' as 'Wrap Grpc'. Added a small PRETTY_TOKENS map (gofr → GoFr, gofrcli → GoFr CLI, vs → vs, grpc → gRPC, etc.) so the breadcrumb honors the brand mark and common acronyms while plain words still fall through to Title Case. Code identifiers left alone (RegisterServerWithGofr, GOFR_TELEMETRY). Co-Authored-By: Claude Opus 4.6 --- src/components/Search.jsx | 42 +++++++++++++++++++++++++++++---------- src/lib/navigation.js | 14 ++++++------- 2 files changed, 38 insertions(+), 18 deletions(-) diff --git a/src/components/Search.jsx b/src/components/Search.jsx index e34c0f9..37ff309 100644 --- a/src/components/Search.jsx +++ b/src/components/Search.jsx @@ -165,27 +165,47 @@ function HighlightQuery({ text, query }) { ) } +// Known-token map for breadcrumb/segment prettifying. Naive Title +// Case turned `gofr-vs-gin` into `Gofr Vs Gin` — wrong on both +// counts (brand mark is `GoFr`, comparator is lowercase `vs`) and +// `wrap-grpc` into `Wrap Grpc` instead of `Wrap gRPC`. Anything +// not in this map falls back to plain Title Case. +const PRETTY_TOKENS = { + gofr: 'GoFr', + gofrcli: 'GoFr CLI', + vs: 'vs', + grpc: 'gRPC', + graphql: 'GraphQL', + api: 'API', + cli: 'CLI', + http: 'HTTP', + https: 'HTTPS', + rest: 'REST', + faq: 'FAQ', +} + // Turn a path segment slug into a Title Case label. -// "wrap-grpc" → "Wrap Grpc", "from-fiber" → "From Fiber". -// Strict slug-to-words; preserves embedded acronyms only when they're already uppercase. +// "wrap-grpc" → "Wrap gRPC", "from-fiber" → "From Fiber", +// "gofr-vs-gin" → "GoFr vs Gin". function prettifySegment(seg) { if (!seg) return '' return seg .split(/[-_]/) - .map((w) => - w.length === 0 - ? '' - : w === w.toUpperCase() - ? w - : w.charAt(0).toUpperCase() + w.slice(1), - ) + .map((w) => { + if (w.length === 0) return '' + const known = PRETTY_TOKENS[w.toLowerCase()] + if (known) return known + // Already uppercase — preserve embedded acronyms. + if (w === w.toUpperCase()) return w + return w.charAt(0).toUpperCase() + w.slice(1) + }) .join(' ') } // Build a breadcrumb-shaped path label from a URL. // "/docs/datasources/mongodb" → "Datasources › Mongodb" -// "/docs/references/gofrcli/init" → "References › Gofrcli › Init" -// "/comparison/gofr-vs-gin" → "Comparison › Gofr Vs Gin" +// "/docs/references/gofrcli/init" → "References › GoFr CLI › Init" +// "/comparison/gofr-vs-gin" → "Comparison › GoFr vs Gin" function pathBreadcrumb(rawUrl) { if (!rawUrl) return '' let p = rawUrl.split('#')[0].split('?')[0] diff --git a/src/lib/navigation.js b/src/lib/navigation.js index b51d240..f46b104 100644 --- a/src/lib/navigation.js +++ b/src/lib/navigation.js @@ -1,12 +1,12 @@ export const navigation = [ { title: 'Quick Start Guide', - desc: "Get started with GoFR through our Quick Start Guide. Learn to build scalable applications with easy-to-follow instructions on server setup, database connections, configuration management, and more. Boost your productivity and streamline your development process.", + desc: "Get started with GoFr through our Quick Start Guide. Learn to build scalable applications with easy-to-follow instructions on server setup, database connections, configuration management, and more. Boost your productivity and streamline your development process.", links: [ { title: 'Hello Server', href: '/docs/quick-start/introduction', - desc: "Getting started with how to write a server using GoFR with basic examples and explanations. Boost your productivity with efficient coding practices and learn to build scalable applications quickly." + desc: "Getting started with how to write a server using GoFr with basic examples and explanations. Boost your productivity with efficient coding practices and learn to build scalable applications quickly." }, { @@ -17,12 +17,12 @@ export const navigation = [ { title: 'Connecting Redis', href: '/docs/quick-start/connecting-redis', - desc: "Discover how to connect your GoFR application to Redis for fast in-memory data storage." + desc: "Discover how to connect your GoFr application to Redis for fast in-memory data storage." }, { title: 'Connecting MySQL', href: '/docs/quick-start/connecting-mysql', - desc: "Step-by-step guide on integrating MySQL with your GoFR application. With managed database connections and new methods for increasing your productivity." + desc: "Step-by-step guide on integrating MySQL with your GoFr application. With managed database connections and new methods for increasing your productivity." }, { title: 'Observability', @@ -82,7 +82,7 @@ export const navigation = [ { title: 'Authentication', href: '/docs/advanced-guide/authentication', - desc: "Implement various authentication methods to secure your GoFR application and protect sensitive endpoints across HTTP and gRPC." + desc: "Implement various authentication methods to secure your GoFr application and protect sensitive endpoints across HTTP and gRPC." }, { title: 'Role-Based Access Control (RBAC)', @@ -358,12 +358,12 @@ export const navigation = [ { title: 'Context', href: '/docs/references/context', - desc: "Discover the GoFR context, an injected object that simplifies request-specific data handling for HTTP, gRPC, and Pub/Sub calls. Learn how it extends Go's context, providing easy access to dependencies like databases, loggers, and HTTP clients. Explore features for reading HTTP requests, binding data, and accessing query and path parameters efficiently, all while reducing application complexity." + desc: "Discover the GoFr context, an injected object that simplifies request-specific data handling for HTTP, gRPC, and Pub/Sub calls. Learn how it extends Go's context, providing easy access to dependencies like databases, loggers, and HTTP clients. Explore features for reading HTTP requests, binding data, and accessing query and path parameters efficiently, all while reducing application complexity." }, { title: 'Configs', href: '/docs/references/configs', - desc: "Learn how to manage configuration settings in your GoFR applications, including default values for environment variables. This section provides a comprehensive list of all available configurations to streamline your setup." + desc: "Learn how to manage configuration settings in your GoFr applications, including default values for environment variables. This section provides a comprehensive list of all available configurations to streamline your setup." }, { title: 'Testing',