Next.js vs. React Router(Remix): New Pages & Layout – Part 4

With the homepage complete, it’s time to give the site more structure. In this post, I’ll add two new pages: Policy and Help.

These pages share a common layout:

  • A banner at the top
  • The menu
  • The main content
  • And the footer at the bottom

This layout is similar to the homepage, but with some small differences. For example, the homepage doesn’t include the banner.

This is the perfect moment to introduce layouts into the project. Instead of repeating the same structure across pages, I’ll create a layout that wraps the content for both the Policy and Help pages.

Next.js

A way to make the Help and Policy pages share the same layout is by using something Next.js calls Route Groups. Route groups are special folders with parentheses in their names—like (folderName)—and they prevent the folder from being included in the route’s URL path. This means I can place both the Help and Policy pages inside a (pages) folder, along with a shared layout file, and the final URLs will still be /help and /policy, not /pages/help or /pages/policy.

This approach keeps related pages neatly grouped together in the code, while also keeping the URLs clean and user-friendly. Here’s what the folder structure looks like:

/app
  ├── layout.tsx
  ├── page.tsx 
  
  ├── components/
      ├── About.tsx         
      ├── AskOracleButton.tsx
      ├── Examples.tsx
      ├── FAQ.tsx
      ├── Footer.tsx
      ├── Hero.tsx
      ├── HowItWorks.tsx
      └── Menu.tsx
  
  └── (pages)/
       ├── layout.tsx         # Shared layout for Help and Policy
       ├── help/
           └── page.tsx
       └── policy/
            └── page.tsx
Bash

Creating the Banner Component

Before setting up the layout for the Help and Policy pages, I converted the original banner into a component: <Banner />. This component appears at the top of both the Help and Policy pages and displays the website’s name as a link to the homepage.

// nextjs > app > components > Banner.tsx
import Link from "next/link";

export default function Banner() {
    return(
        <section className=" bg-cover bg-center bg-no-repeat
            [background-image:url('/images/tartarus-insight-banner-small.webp')]
            xl:[background-image:url('/images/tartarus-insight-banner-medium.webp')]
            2xl:[background-image:url('/images/tartarus-insight-banner-large.webp')]">
            <h1 className="banner-shadow !m-0 p-[30px_44px] text-white text-center">
                <Link href="/">Tartarus Insight</Link>
            </h1>
        </section>
    );
}
JSX

I added the following CSS rule to the globals.css to get a text shadow effect.

/* nextjs > app > globals.css */

.banner-shadow {
    text-shadow: 2px 2px 15px black;
}
CSS

Creating the Shared Layout and Pages

With the <Banner /> component ready, I moved on to setting up a shared layout for the Help and Policy pages. Since both pages use the same structure—banner at the top, followed by the menu, main content, and footer.

This is what the layout file looks like:

// nextjs > app > (pages) > layout.tsx
import Banner from "@/app/components/Banner";
import Menu from "@/app/components/Menu";
import Footer from "@/app/components/Footer";

export default function PagesLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <div className="flex min-h-screen flex-col justify-between bg-[url('/images/pattern-marble.webp')]">
      <Banner />
      <Menu />
      {children}
      <Footer />
    </div>
  );
}
JSX

After setting up the layout, I created the two pages: help/page.tsx and policy/page.tsx. Each page contains its own content and is automatically wrapped with the shared layout thanks to the folder structure.

// nextjs > app > (pages) > help > page.tsx
import AskOracleButton from "@/app/components/AskOracleButton";

export default function HelpPage() {
    return (
        <section id="about-section" className="px-[5px] py-[35px] text-center">
            <h1>Help</h1>
            <div className="text-container mx-auto mb-5">
                <p>The Oracle is ready to guide you! Follow these steps to begin your journey:</p>
                
                <h2>1. Connect With the Oracle</h2>
                <p>Click on the “Connect With the Oracle” button.</p>
                <p>This will reveal a list of AI models currently available on your computer.</p>
                    
                <h2>2. Choose Your Model</h2>
                <p>Select a model from the list. We recommend using any Llama model for the best experience.</p>
                <p>Once you’ve chosen your model, type your question into the message box and click the “Send” button. The Oracle will respond with its wisdom!</p>
                    
                <h2>3. Adjust the Connection (If Needed)</h2>
                <p>By default, Ollama operates locally at http://localhost:11434.</p>
                <p>If you’ve manually changed this location, simply click the “Settings” button to update the address. Enter the new location in place of the default, and you’re good to go.</p>
                <br/>
                <hr/>
                <br/>
                <p><i>If you encounter any issues, please visit our <a className="text-white underline" href="https://github.com/paaggeli/tartarus-insight/issues">GitHub repository</a> and open an issue. The Oracle values your feedback (and promises not to hold grudges).</i></p>
            </div>
            <AskOracleButton />
        </section>
    );
}
JSX
// nextjs > app > (pages) > policy > page.tsx
export default function PolicyPage() {
    return (
        <section className="mx-auto p-2 text-container">
            <h2 className="text-center">Privacy Policy</h2>
            <div>
                <p>At Tartarus Insight, we are committed to protecting your personal data and respecting your privacy. 
                    This Privacy Policy outlines how we collect, use, and protect your personal information in accordance 
                    with applicable laws and regulations.
                </p>
                <h3 className="mt-4 mb-2">Collection of Personal Data</h3>
                <p>We collect personal data only when necessary to provide our services or offer our content. 
                    The types of personal data we may collect include:
                </p>
                <ul className="list-disc p-6">
                    <li>Email address (used by MailChimp for marketing purposes)</li>
                </ul>
                <h3 className="mt-4 mb-2">How We Use Your Data:</h3>
                <p>Tartarus Insight complies with the <strong>General Data Protection Regulation (GDPR)</strong> and respects user privacy. 
                    We do not store any personal data on our servers.<br/>The following services are used to manage our website and communication:</p>
                <ol className="list-decimal p-6 space-y-2">
                    <li>
                        <strong>MailChimp: </strong>We use MailChimp to manage our email list and send newsletters. 
                        By providing your email address, you consent to be added to our email list.
                    </li>
                    <li>
                        <strong>Google Analytics: </strong>We use Google Analytics to track website traffic and analyze user behavior. 
                        The data collected is anonymous and used solely for statistical purposes.
                    </li>
                </ol>
                <h3 className="mt-4 mb-2">Contact Information::</h3>
                <p>If you have questions about our Privacy Policy or wish to opt-out of our email list, <a href="mailto:paggelidis@hotmail.co.uk">please contact us</a></p>
            </div>
        </section>
    );
}
JSX

Now that the Policy page exists, I have to update the <Footer /> component so the link points to the correct page.

// nextjs > app > components > Footer.tsx
import Link from 'next/link';

export default function Footer() {
    return(
        <footer className="flex justify-center items-center p-2.5 bg-[#333] text-white">
            <p className="!text-sm mb-2">
                © 2024 <a href="https://x.com/PanosAngel1" className="text-white underline">Panos</a> | 
                Built with ❤️ | 
                <Link href="policy" className="text-white underline"> Privacy policy</Link> | 
                <a href="https://github.com/paaggeli/tartarus-insight" target="_blank" className="text-white underline"> Contribute on GitHub</a>
            </p>
        </footer>
    );
}
JSX

I also went back and updated the links in the <Menu /> component, since I had originally forgotten to add the right ones.

// nextjs > app > components > Menu.tsx
'use client';
import { useState } from 'react';
import Link from 'next/link';
import { FaBars, FaTimes } from "react-icons/fa";

export default function Menu() {
    const [menuOpen, setMenuOpen] = useState(false);

    const handleToggleMenu = () => {
        setMenuOpen(true);
    };

    const handleCloseMenu = () => {
        setMenuOpen(false);
    };

    const handleLinkClick = () => {
        setMenuOpen(false);
    };

    return (
        <header className="w-full 
                text-inherit 
                z-20 
                sm:relative
                sm:p-0
                sm:border-y-[15px]
                sm:bg-[url('/images/pattern-marble.webp')] 
                sm:bg-repeat">
            <nav id="navMenu" className={`fixed 
                flex 
                justify-center 
                items-center 
                top-0 
                left-0 
                w-full 
                h-full 
                bg-neutral-900/95 
                text-white 
                text-center
                duration-300
                ${menuOpen ? 'translate-y-0' : '-translate-y-full'}
                sm:p-3
                sm:bg-[unset]
                sm:relative
                sm:translate-none
            `}>
                <ul className="space-y-10 sm:space-y-0 sm:flex sm:space-x-5">
                    <li><Link href="/" onClick={handleLinkClick} className="hover:text-neutral-300">Home</Link></li>
                    <li><Link href="/#about-section" onClick={handleLinkClick} className="hover:text-neutral-300">About</Link></li>
                    <li><Link href="/#how-it-works-section" onClick={handleLinkClick} className="hover:text-neutral-300">How it Works</Link></li>
                    <li><Link href="/#examples-section" onClick={handleLinkClick} className="hover:text-neutral-300">Examples</Link></li>
                    <li><Link href="/#faq-section" onClick={handleLinkClick} className="hover:text-neutral-300">FAQ</Link></li>
                </ul>
            </nav>
            {!menuOpen && (
                <a className="absolute top-4 right-6 text-[#bca68a] text-2xl sm:hidden" onClick={handleToggleMenu}>
                    <FaBars />
                </a>
            )}
            {menuOpen && (
                <a className="fixed top-4 right-6 text-white text-2xl sm:hidden" onClick={handleCloseMenu}>
                    <FaTimes />
                </a>
            )}
        </header>
    );
}
JSX

React Router

Now that the Next.js version is complete, it’s time to rebuild the Help and Policy pages using React Router. But before doing that, let’s fix a small mistake I made in the previous post.

You see, I’ve been following a structure where I create a folder for each page and place all the components specific to that page inside it. I also have a separate components folder for shared components used across multiple pages. However, I accidentally placed the Menu component inside the home folder when it should have been in the components folder.

So, I moved menu.tsx from home to components and updated the import path in routes/home.tsx from import Menu from "../home/menu"; to import Menu from "../components/menu";

// react-router > app > routes > home.tsx
import type { Route } from "./+types/home";
import Menu from "../components/menu";
import Hero from "../home/hero";
import About from "../home/about";
import HowItWorks from "../home/how-it-works";
import Examples from "../home/examples";
import FAQ from "../home/faq";
import Footer from "../components/footer";

export function meta({}: Route.MetaArgs) {
  return [
    { title: "Tartarus Insight" },
    { 
      name: "description", 
      content: "Trapped in the abyss of business struggles? Harness the Oracle's wisdom to ascend from struggle to success." 
    },
  ];
}

export default function Home() {
  return (
    <>
    <Menu />
    <Hero />
    <About />
    <HowItWorks />
    <Examples />
    <FAQ />
    <Footer />
    </>
  );
}
JSX

Creating the Banner Component

Now it’s time to create the Banner component inside the components folder, before moving on to building the Help and Policy pages, where it will be used.

// react-router > app > components > banner.tsx
import { Link } from 'react-router';

export default function Banner() {
    return(
        <section className=" bg-cover bg-center bg-no-repeat
        [background-image:url('/images/tartarus-insight-banner-small.webp')]
        xl:[background-image:url('/images/tartarus-insight-banner-medium.webp')]
        2xl:[background-image:url('/images/tartarus-insight-banner-large.webp')]">
        <h1 className="banner-shadow !m-0 p-[30px_44px] text-white text-center">
            <Link to="/">Tartarus Insight</Link>
        </h1>
    </section>
    );
}
JSX

The component is almost identical to the one from the Next.js version, with two small differences: it imports the Link component from 'react-router' instead of 'next/link', and it uses 'to' instead of 'href' as the attribute for the Link.

Creating the Shared Layout

This is where the differences between Next.js and React Router start to become more noticeable. While both frameworks support reusable layouts, the way you define and apply them is quite different.

To set up a shared layout for the Help and Policy pages, I created a new folder called layouts inside the app directory, and added a file named pages.tsx. The name isn’t special—we can name the file whatever we want. This file will serve as the layout for both pages.

// react-router > app > layouts > banner.tsx
import { Outlet } from "react-router";
import Menu from "~/components/menu";
import Banner from "~/components/banner";
import Footer from "~/components/footer";

export default function Pages() {
    return (
        <div className="flex min-h-screen flex-col justify-between bg-[url('/images/pattern-marble.webp')]">
            <Banner />
            <Menu />
            <Outlet />
            <Footer />
        </div>
    )
}
JSX

It includes the shared elements <Banner />, <Menu />, and <Footer />, and uses React Router’s <Outlet /> component to render the content of the nested routes (Help and Policy pages).

Creating the Help and Policy Pages

With the layout component in place, the next step is to create the actual pages. I placed both help.tsx and policy.tsx files directly inside the routes directory. The policy.tsx file is a direct copy of the one from the Next.js project, while the help.tsx file is mostly the same too—except for an updated import path for the AskOracleButton component to reflect the new project structure.

// react-router > app > routes > help.tsx
import AskOracleButton from "../components/ask-oracle-button";

export default function HelpPage() {
    return (
        <section id="about-section" className="px-[5px] py-[35px] text-center">
            <h1>Help</h1>
            <div className="text-container mx-auto mb-5">
                <p>The Oracle is ready to guide you! Follow these steps to begin your journey:</p>
                
                <h2>1. Connect With the Oracle</h2>
                <p>Click on the “Connect With the Oracle” button.</p>
                <p>This will reveal a list of AI models currently available on your computer.</p>
                    
                <h2>2. Choose Your Model</h2>
                <p>Select a model from the list. We recommend using any Llama model for the best experience.</p>
                <p>Once you’ve chosen your model, type your question into the message box and click the “Send” button. The Oracle will respond with its wisdom!</p>
                    
                <h2>3. Adjust the Connection (If Needed)</h2>
                <p>By default, Ollama operates locally at http://localhost:11434.</p>
                <p>If you’ve manually changed this location, simply click the “Settings” button to update the address. Enter the new location in place of the default, and you’re good to go.</p>
                <br/>
                <hr/>
                <br/>
                <p><i>If you encounter any issues, please visit our <a className="text-white underline" href="https://github.com/paaggeli/tartarus-insight/issues">GitHub repository</a> and open an issue. The Oracle values your feedback (and promises not to hold grudges).</i></p>
            </div>
            <AskOracleButton />
        </section>
    );
}
JSX
// react-router > app > routes > policy.tsx
export default function PolicyPage() {
    return (
        <section className="mx-auto p-2 text-container">
            <h2 className="text-center">Privacy Policy</h2>
            <div>
                <p>At Tartarus Insight, we are committed to protecting your personal data and respecting your privacy. 
                    This Privacy Policy outlines how we collect, use, and protect your personal information in accordance 
                    with applicable laws and regulations.
                </p>
                <h3 className="mt-4 mb-2">Collection of Personal Data</h3>
                <p>We collect personal data only when necessary to provide our services or offer our content. 
                    The types of personal data we may collect include:
                </p>
                <ul className="list-disc p-6">
                    <li>Email address (used by MailChimp for marketing purposes)</li>
                </ul>
                <h3 className="mt-4 mb-2">How We Use Your Data:</h3>
                <p>Tartarus Insight complies with the <strong>General Data Protection Regulation (GDPR)</strong> and respects user privacy. 
                    We do not store any personal data on our servers.<br/>The following services are used to manage our website and communication:</p>
                <ol className="list-decimal p-6 space-y-2">
                    <li>
                        <strong>MailChimp: </strong>We use MailChimp to manage our email list and send newsletters. 
                        By providing your email address, you consent to be added to our email list.
                    </li>
                    <li>
                        <strong>Google Analytics: </strong>We use Google Analytics to track website traffic and analyze user behavior. 
                        The data collected is anonymous and used solely for statistical purposes.
                    </li>
                </ol>
                <h3 className="mt-4 mb-2">Contact Information::</h3>
                <p>If you have questions about our Privacy Policy or wish to opt-out of our email list, <a href="mailto:paggelidis@hotmail.co.uk">please contact us</a></p>
            </div>
        </section>
    );
}
JSX

At this point, the pages themselves are created, but they’re not yet using the shared layout.

Updating the Routes to Use the Shared Layout

Now that both the Help and Policy pages are in place, the final step is to make them share the same layout. In React Router, this is done by wrapping the routes in a layout using the layout() in the routes.ts file.

I imported layout and route from @react-router/dev/routes, and updated the route configuration to nest the Help and Policy pages under the shared layout.

import { type RouteConfig, index, layout, route } from "@react-router/dev/routes";

export default [
    index("routes/home.tsx"),
    
    layout("./layouts/pages.tsx", [
        route("help", "./routes/help.tsx"),
        route("policy", "./routes/policy.tsx")
    ])
] satisfies RouteConfig;
JSX

The code tells React Router to use the pages.tsx layout file—located inside the /app/layouts folder—for both the /help and /policy routes. Inside that layout, the <Outlet /> component will render the content of each individual page.

As with the Next.js version, I corrected the Policy link in the <Footer /> and updated the <Menu /> links as well.

// react-router > app > components > footer.tsx
import { Link } from 'react-router';

export default function Footer() {
    return(
        <footer className="flex justify-center items-center p-2.5 bg-[#333] text-white">
            <p className="!text-sm mb-2">
                © 2024 <a href="https://x.com/PanosAngel1" className="text-white underline">Panos</a> | 
                Built with ❤️ | 
                <Link to="policy" className="text-white underline"> Privacy policy</Link> | 
                <a href="https://github.com/paaggeli/tartarus-insight" target="_blank" className="text-white underline"> Contribute on GitHub</a>
            </p>
        </footer>
    );
}
JSX
// react-router > app > components > menu.tsx
import { useState } from 'react';
import { Link } from "react-router";
import { FaBars, FaTimes } from "react-icons/fa";

export default function Menu() {
  const [menuOpen, setMenuOpen] = useState(false);

  const handleToggleMenu = () => {
    setMenuOpen(true);
  };

  const handleCloseMenu = () => {
    setMenuOpen(false);
  };

  const handleLinkClick = () => {
    setMenuOpen(false);
  };

  return (
    <header className="w-full 
                text-inherit 
                z-20 
                sm:relative
                sm:p-0
                sm:border-y-[15px]
                sm:bg-[url('images/pattern-marble.webp')] 
                sm:bg-repeat">
            <nav id="navMenu" className={`fixed 
                flex 
                justify-center 
                items-center 
                top-0 
                left-0 
                w-full 
                h-full 
                bg-neutral-900/95 
                text-white 
                text-center
                duration-300
                ${menuOpen ? 'translate-y-0' : '-translate-y-full'}
                sm:p-3
                sm:bg-[unset]
                sm:relative
                sm:translate-none
            `}>
                <ul className="space-y-10 sm:space-y-0 sm:flex sm:space-x-5">
                    <li><Link to="/" onClick={handleLinkClick} className="hover:text-neutral-300">Home</Link></li>
                    <li><Link to="/#about-section" onClick={handleLinkClick} className="hover:text-neutral-300">About</Link></li>
                    <li><Link to="/#how-it-works-section" onClick={handleLinkClick} className="hover:text-neutral-300">How it Works</Link></li>
                    <li><Link to="/#examples-section" onClick={handleLinkClick} className="hover:text-neutral-300">Examples</Link></li>
                    <li><Link to="/#faq-section" onClick={handleLinkClick} className="hover:text-neutral-300">FAQ</Link></li>
                </ul>
            </nav>
            {!menuOpen && (
                <a className="absolute top-4 right-6 text-[#bca68a] text-2xl sm:hidden" onClick={handleToggleMenu}>
                    <FaBars />
                </a>
            )}
            {menuOpen && (
                <a className="fixed top-4 right-6 text-white text-2xl sm:hidden" onClick={handleCloseMenu}>
                    <FaTimes />
                </a>
            )}
        </header>
  );
}
JSX

Here’s how the /app directory looks after setting up the shared layout and adding the Help and Policy pages:

/app
  ├── layouts/
       └── pages.tsx
  ├── routes/
       ├── home.tsx
       ├── help.tsx
       └── policy.tsx
  ├── components/
       ├── ask-oracle-button.tsx
       ├── banner.tsx
       ├── footer.tsx
       └── menu.tsx
  ├── home/
       ├── about.tsx
       ├── examples.tsx
       ├── faq.tsx
       ├── hero.tsx
       └── how-it-works.tsx
  └── routes.ts
Bash

Conclusion

For this part of the project, I focused mainly on setting up layouts and routing — and that’s where the biggest differences between Next.js and React Router showed up.

Next.js follows a “convention over configuration” style: your folder and file structure inside the app directory matters, and the framework handles the routing for you. I actually like this a lot because it reminds me of Ruby on Rails — it’s organized, predictable, and you don’t have to think much about it.

On the other hand, React Router makes you configure your routes manually. At first, I thought this might feel tedious, but it turned out to be really easy. And even though I like conventions, I can’t say that Next.js is better. React Router offers more flexibility, which can be a big plus if you know how to structure your app properly.

That said, for someone new (like me), too much freedom could also be a little overwhelming when it comes to keeping the project maintainable. React Router does offer a way to use a convention-based setup similar to Next.js through the @react-router/fs-routes package, which automatically generates routes based on your filesystem.

Overall, I think I appreciate React Router’s way a bit more because it gives you a choice: you can use full configuration, or you can opt for a convention-based structure if you want — and configuring routes manually was super easy.

What’s Next?

With the Help and Policy pages set up, it’s time to move on to the core of the project: the page where we actually talk to the Oracle (the AI)!

In the next post, I’ll build the main interaction page, set up the user interface, and connect it to the AI models. This is where the real functionality of Tartarus Insight starts to come together

You can check out the Git repo here.

Leave a Reply

Your email address will not be published. Required fields are marked *