Vercel Logo

Header and Navigation

Navigation tells users where they are and where they can go. An auth-aware header shows different options based on login stateβ€”sign in buttons for guests, account links for users. A sidebar in the protected area helps users navigate between account, billing, and premium features.

Outcome

Understand how the auth-aware header and protected area sidebar work in the starter.

Fast Track

  1. Review components/header.tsx - already in the starter with auth state check
  2. Review components/protected-sidebar.tsx - already in the starter with navigation links
  3. Understand how auth-aware navigation works

Hands-on Exercise 4.2

Review and understand the navigation system (pre-built in starter):

What's already implemented:

  1. Header shows different content based on auth state
  2. User email displays when signed in
  3. Sidebar in protected area links to Account, Membership, Subscription, Field Guide
  4. Active state indicated on current page link
  5. Field Guide link disabled until user has active subscription

Review hints:

  • Header is a Server Component that checks auth with createSupabaseClient()
  • Sidebar uses an InPageSidebar component that handles active states
  • The AuthPageSignOutButton component is used throughout for sign-out

Try It

  1. Test header (signed out):

  2. Test header (signed in):

    • Sign in to the app
    • Header should show your email and account link
  3. Test sidebar navigation:

  4. Test active states:

    • Click each sidebar link
    • Active indicator should update to current page

Done-When

  • Understand how the Header checks auth state server-side
  • Understand how the sidebar gates the Field Guide link
  • Header shows different content when signed in vs signed out
  • Sidebar navigation links work correctly
  • Active page is visually indicated in sidebar

Solution

The header and sidebar are pre-built in the starter. Here's how they work:

Header Component

The header in components/header.tsx checks auth state server-side:

components/header.tsx
import Link from "next/link";
import { Button } from "@/components/ui/button";
import { createSupabaseClient } from "@/utils/supabase/server";
 
export default async function Header() {
  // Try to get the user, gracefully handle if Supabase isn't implemented
  let user = null;
  try {
    const supabase = await createSupabaseClient();
    const { data } = await supabase.auth.getUser();
    user = data.user;
  } catch {
    // Supabase client not implemented yet - show logged out state
  }
 
  return (
    <nav className="border-b w-full h-16 shrink-0 flex items-center">
      <div className="px-6 w-full flex items-center justify-between mx-auto">
        <Link href="/" className="text-sm font-medium flex items-center gap-2">
          <span>🌿</span>
          Forager's Guild
        </Link>
        <div className="flex items-center gap-2">
          {user ? (
            <>
              <span className="text-sm text-muted-foreground hidden sm:inline">
                {user.email}
              </span>
              <Button variant="outline" size="sm" asChild>
                <Link href="/protected">My Guild</Link>
              </Button>
            </>
          ) : (
            <>
              <Button variant="outline" asChild>
                <Link href="/sign-in">Sign In</Link>
              </Button>
              <Button asChild>
                <Link href="/sign-up">Join Guild</Link>
              </Button>
            </>
          )}
        </div>
      </div>
    </nav>
  );
}

Key patterns:

  • Server Component uses createSupabaseClient() for auth check
  • Try/catch wrapper allows the header to work before Supabase is implemented
  • Conditional rendering based on user state

Protected Sidebar Component

The sidebar in components/protected-sidebar.tsx uses a reusable InPageSidebar component:

components/protected-sidebar.tsx
import InPageSidebar from "@/components/in-page-sidebar";
 
export default async function ProtectedSidebar() {
  // TODO: Uncomment when hasActiveSubscription is implemented in Section 2:
  // const supabase = await createSupabaseClient();
  // const hasAccess = await hasActiveSubscription(supabase);
  const hasAccess = false; // Hardcoded until Section 2
 
  return (
    <InPageSidebar
      basePath="/protected"
      items={[
        { label: "My Account", href: "/" },
        { label: "Membership", href: "/pricing" },
        { label: "Subscription", href: "/subscription" },
        { label: "Field Guide", href: "/paid-content", disabled: !hasAccess },
      ]}
    />
  );
}

Key patterns:

  • InPageSidebar handles active state detection automatically
  • disabled prop gates the Field Guide until subscription is active
  • Will integrate with hasActiveSubscription() after Section 2

Layout Structure

The layouts are already configured in the starter:

Root Layout (app/layout.tsx) includes the Header in the component tree.

Protected Layout (app/protected/layout.tsx) includes the sidebar:

app/protected/layout.tsx
import Content from "@/components/content";
import ProtectedSidebar from "@/components/protected-sidebar";
 
export default function ProtectedLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <Content>
      <div className="flex w-full h-full">
        <ProtectedSidebar />
        <div className="flex-1">{children}</div>
      </div>
    </Content>
  );
}
app/layout.tsx
    ↓
<Header /> (Server Component)
    ↓
Check auth with createSupabaseClient()
    ↓
Render: 🌿 Forager's Guild | [Sign In] [Join Guild]   (guest)
        🌿 Forager's Guild | email | [My Guild]       (signed in)

app/protected/layout.tsx
    ↓
<ProtectedSidebar /> (Server Component)
    ↓
<InPageSidebar /> handles active state
    ↓
Render navigation links with active indicators

File Structure

These components are pre-built in the starter:

components/
β”œβ”€β”€ header.tsx              ← Auth-aware header
β”œβ”€β”€ protected-sidebar.tsx   ← Protected area sidebar
β”œβ”€β”€ in-page-sidebar.tsx     ← Reusable sidebar component
β”œβ”€β”€ auth-sign-out-button.tsx
β”œβ”€β”€ field-guide-card.tsx
β”œβ”€β”€ pricing-card.tsx
└── ui/
    β”œβ”€β”€ button.tsx
    └── card.tsx

app/
β”œβ”€β”€ layout.tsx              ← Includes Header
└── protected/
    └── layout.tsx          ← Includes ProtectedSidebar

Troubleshooting

Header shows wrong auth state:

  • Server components cache aggressively in dev
  • Try hard refresh (Cmd+Shift+R / Ctrl+Shift+R)
  • Clear cookies and sign in again
  • Make sure you've implemented createSupabaseClient in Section 1

Header shows signed-out state even when signed in:

  • Verify createSupabaseClient is implemented (not throwing an error)
  • The header has a try/catch that falls back to logged-out state on error

Sidebar active state not updating:

  • The InPageSidebar component handles this automatically
  • Try navigating via links rather than direct URL entry
  • Hard refresh if state seems stuck

Field Guide link always disabled:

  • This is expected until you implement hasActiveSubscription() in Section 3
  • Once implemented, uncomment the subscription check in protected-sidebar.tsx