Back to 0fee
0fee

Merging the Marketing Website Into the Frontend

How we merged 0fee.dev's separate marketing website into the frontend app, going from 3 services to 2 with 3-layout routing.

Thales & Claude | March 25, 2026 8 min 0fee
architecturesolidjsroutingmonoreposimplification

By Session 007, 0fee.dev had three separate services: a FastAPI backend, a SolidJS dashboard, and a separate SolidJS marketing website. Three services meant three build processes, three Docker containers, three sets of dependencies to maintain, and an nginx reverse proxy routing between them based on URL path. Session 008 simplified this to two services by merging the marketing website into the dashboard's SolidJS application.

This article covers why we merged, how the 3-layout routing system works, the mechanics of the merge, and why reducing from three services to two made everything simpler.

Before the Merge: Three Services

0fee.dev Architecture (Sessions 001-007)
├── backend/ (FastAPI, port 8000)
│   ├── REST API
│   ├── Hosted checkout (Jinja2)
│   └── Webhooks
├── frontend/ (SolidJS, port 3000)
│   ├── Dashboard (authenticated)
│   └── Auth pages
└── website/ (SolidJS, port 3001)
    ├── Home page
    ├── Products, Pricing, Docs
    ├── About, Contact
    └── Legal pages

This architecture had problems:

IssueImpact
Shared styles driftMarketing and dashboard diverged visually
Duplicate componentsButton, Card, Input reimplemented in both
Two build pipelinesTwo npm run build, two Docker images
Complex nginx routingPath-based routing between frontend and website
SEO complicationsTwo SPA origins for one domain
State sharing impossibleLogin state not accessible from marketing pages

The marketing website and the dashboard were both SolidJS applications using TailwindCSS. They shared the same design language but maintained separate component libraries. When we updated a button style in the dashboard, we had to remember to update it in the marketing site too.

The Merge: Session 008

Session 008 executed the merge in a single session:

Step 1: Copy Marketing Components

bash# Copy components
cp -r website/src/components/* frontend/src/components/marketing/

# Copy pages
cp -r website/src/pages/* frontend/src/pages/marketing/

Step 2: Fix Import Paths

Every marketing component had import paths relative to the website/src/ directory. These needed updating for the new frontend/src/ structure:

typescript// Before (in website/)
import { Hero } from "../components/Hero";
import { Features } from "../components/Features";

// After (in frontend/)
import { Hero } from "../components/marketing/Hero";
import { Features } from "../components/marketing/Features";

Step 3: Merge Tailwind Configuration

The two Tailwind configs had diverged. The marketing site had animations and colors that the dashboard did not:

javascript// frontend/tailwind.config.js (merged)
module.exports = {
  darkMode: "class",
  content: ["./src/**/*.{js,jsx,ts,tsx}"],
  theme: {
    extend: {
      // From dashboard config
      colors: {
        emerald: { /* dashboard brand colors */ },
      },
      // From marketing config (newly merged)
      animation: {
        "fade-in": "fadeIn 0.5s ease-out",
        "slide-up": "slideUp 0.5s ease-out",
        "float": "float 6s ease-in-out infinite",
        "pulse-slow": "pulse 4s ease-in-out infinite",
      },
      keyframes: {
        fadeIn: { from: { opacity: "0" }, to: { opacity: "1" } },
        slideUp: {
          from: { opacity: "0", transform: "translateY(20px)" },
          to: { opacity: "1", transform: "translateY(0)" },
        },
        float: {
          "0%, 100%": { transform: "translateY(0)" },
          "50%": { transform: "translateY(-10px)" },
        },
      },
    },
  },
};

Step 4: Merge CSS Styles

Marketing-specific CSS (glassmorphism, gradient backgrounds, decorative elements) was appended to the dashboard's index.css:

css/* frontend/src/styles/index.css */

/* Dashboard styles (existing) */
@tailwind base;
@tailwind components;
@tailwind utilities;

/* Marketing styles (merged from website) */
.glass {
  background: rgba(255, 255, 255, 0.05);
  backdrop-filter: blur(12px);
  border: 1px solid rgba(255, 255, 255, 0.1);
}

.gradient-text {
  background: linear-gradient(135deg, #10b981, #06b6d4);
  -webkit-background-clip: text;
  -webkit-text-fill-color: transparent;
}

.hero-gradient {
  background: linear-gradient(135deg, #0f172a 0%, #1e293b 50%, #0f172a 100%);
}

Step 5: Update App.tsx with 3-Layout Routing

The most significant change was restructuring the router to support three distinct layouts:

tsxfunction App() {
  return (
    <LanguageProvider>
      <ThemeProvider>
        <Router>
          {/* Marketing pages: Navbar + Footer */}
          <Route path="/" component={MarketingLayout}>
            <Route path="/" component={Home} />
            <Route path="/products" component={Products} />
            <Route path="/pricing" component={Pricing} />
            <Route path="/docs" component={Docs} />
            <Route path="/about" component={About} />
            <Route path="/contact" component={Contact} />
            <Route path="/legal" component={Legal} />
            <Route path="/legal/terms" component={Terms} />
            <Route path="/legal/privacy" component={Privacy} />
          </Route>

          {/* Auth pages: Minimal centered layout */}
          <Route path="/" component={AuthLayout}>
            <Route path="/login" component={Login} />
            <Route path="/register" component={Register} />
            <Route path="/oauth/callback" component={OAuthCallback} />
          </Route>

          {/* Dashboard pages: Sidebar + Header */}
          <Route path="/" component={DashboardLayout}>
            <Route path="/dashboard" component={Dashboard} />
            <Route path="/apps" component={Apps} />
            <Route path="/transactions" component={Transactions} />
            {/* ... more dashboard routes */}
          </Route>

          {/* Standalone: No layout chrome */}
          <Route path="/payment/success" component={PaymentResult} />
          <Route path="/payment/cancel" component={PaymentResult} />
          <Route path="/payment/pending" component={PaymentResult} />
          <Route path="/payment/failed" component={PaymentResult} />
        </Router>
      </ThemeProvider>
    </LanguageProvider>
  );
}

Step 6: Update Route Paths

The dashboard's root route changed from / to /dashboard:

tsx// Before: Dashboard at root
<Route path="/" component={Dashboard} />

// After: Dashboard at /dashboard, marketing at root
<Route path="/dashboard" component={Dashboard} />

This is a natural pattern -- visitors see the marketing site at the root URL, while authenticated developers work in the /dashboard path.

Step 7: Create Payment Result Pages

During the merge, we also created payment result pages that live outside any layout:

tsx// PaymentResult.tsx
function PaymentResult() {
  const [searchParams] = useSearchParams();
  const status = searchParams.status || "unknown";
  const transactionId = searchParams.transaction_id;

  const [transaction] = createResource(
    () => transactionId,
    async (id) => {
      const response = await apiClient.get(`/v1/payments/${id}`);
      return response.data;
    }
  );

  return (
    <div class="min-h-screen flex items-center justify-center">
      <Switch>
        <Match when={status === "completed"}>
          <SuccessCard transaction={transaction()} />
        </Match>
        <Match when={status === "pending"}>
          <PendingCard transaction={transaction()} />
        </Match>
        <Match when={status === "cancelled"}>
          <CancelledCard />
        </Match>
        <Match when={status === "failed"}>
          <FailedCard />
        </Match>
      </Switch>
    </div>
  );
}

Payment result pages have no navbar, sidebar, or footer. They show a simple card with the transaction status and, for pending payments, auto-poll for status updates.

Step 8: Delete the Website Folder

bashrm -rf website/

With the merge complete, the website/ directory was deleted entirely.

After the Merge: Two Services

0fee.dev Architecture (Session 008+)
├── backend/ (FastAPI, port 8000)
│   ├── REST API
│   ├── Hosted checkout (Jinja2)
│   └── Webhooks
└── frontend/ (SolidJS, port 3000)
    ├── Marketing pages (public)
    ├── Auth pages
    ├── Dashboard (protected)
    └── Payment result pages

The Layout Components

Each layout is a simple wrapper that provides the appropriate chrome:

Marketing Layout

tsxfunction MarketingLayout(props) {
  return (
    <div class="min-h-screen">
      <Navbar />
      <main>{props.children}</main>
      <Footer />
    </div>
  );
}

Auth Layout

tsxfunction AuthLayout(props) {
  return (
    <div class="min-h-screen flex items-center justify-center bg-gray-50">
      <div class="w-full max-w-md">
        {props.children}
      </div>
    </div>
  );
}

Dashboard Layout

tsxfunction DashboardLayout(props) {
  return (
    <ProtectedRoute>
      <div class="flex h-screen bg-gray-100 dark:bg-gray-950">
        <Sidebar />
        <div class="flex-1 flex flex-col overflow-hidden">
          <Header />
          <main class="flex-1 overflow-auto p-6">
            {props.children}
          </main>
        </div>
      </div>
    </ProtectedRoute>
  );
}

The ProtectedRoute wrapper checks authentication and redirects to /login if the user is not authenticated. Marketing and auth pages have no protection.

What Changed in Docker

The Docker configuration simplified from three frontend services to one:

Before

yaml# docker-compose.yml (3 services)
services:
  api:
    build: ./backend
    ports: ["8000:8000"]

  dashboard:
    build: ./frontend
    ports: ["3000:3000"]

  website:
    build: ./website
    ports: ["3001:3001"]

  nginx:
    image: nginx
    volumes:
      - ./nginx/nginx.conf:/etc/nginx/nginx.conf
    ports: ["80:80"]

After

yaml# docker-compose.yml (2 services)
services:
  api:
    build: ./backend
    ports: ["8000:8000"]

  frontend:
    build: ./frontend
    ports: ["3000:3000"]

  nginx:
    image: nginx
    volumes:
      - ./nginx/nginx.conf:/etc/nginx/nginx.conf
    ports: ["80:80"]

The nginx configuration also simplified -- no more path-based routing between two SolidJS apps.

PawaPay Page Provider

During Session 008, we also created the PawaPay hosted payment page provider (PawapayPageProvider). This was separate from the merge but happened in the same session. The new provider redirects customers to PawaPay's hosted checkout page, which handles country selection, operator detection, and payment collection.

The original PawapayProvider (direct API, USSD push) remained available alongside the new hosted page variant.

Benefits of the Merge

MetricBefore (3 services)After (2 services)
Docker containers4 (API + dashboard + website + nginx)3 (API + frontend + nginx)
Build commands2 npm builds1 npm build
Bundle output2 separate JS/CSS bundles1 combined bundle
Shared componentsDuplicatedShared
State sharingImpossibleLogin state available on marketing pages
Deploy complexity3 services to coordinate2 services to coordinate

What We Learned

The merge taught us three things:

  1. Separate frontends for marketing and dashboard is premature optimization. Unless you have different teams or radically different tech stacks, keeping everything in one SolidJS app with layout-based routing is simpler and more maintainable.
  1. 3-layout routing handles every use case. Marketing (public, with navbar/footer), Auth (minimal, centered), and Dashboard (protected, with sidebar/header) cover every page type we needed across the entire platform.
  1. Merge early, not late. The merge in Session 008 was straightforward because both apps used the same tech stack and were only a few sessions old. Waiting longer would have meant more divergence, more duplicate code, and a harder merge.

Session 008 was a cleanup session -- no new features, just architectural simplification. By the end of the session, 0fee.dev went from three frontend services to two, with a cleaner routing system and shared component library. Every session after this benefited from the simplification.


This article is part of the "How We Built 0fee.dev" series. 0fee.dev is a payment orchestrator covering 53+ providers across 200+ countries, built by Juste A. GNIMAVO and Claude from Abidjan with zero human engineers. Follow the series for the complete build story.

Share this article:

Responses

Write a response
0/2000
Loading responses...

Related Articles