How to Set Up LunaTV with Next.js 14 App Router: Complete Installation Guide

Clone the LunaTV repository, install dependencies with pnpm, configure your .env.local file with NEXT_PUBLIC_* variables, and run pnpm dev to start the Next.js 14 App Router on localhost:3000.

LunaTV is a React-based media center built on Next.js 14 that leverages the App Router architecture for server-side rendering and dynamic routing. The application follows the modern src/app directory structure, utilizing React Server Components by default while injecting runtime configuration for client-side hydration. This guide walks you through the complete setup process using the official MoonTechLab/LunaTV source code.

Clone the Repository and Install Dependencies

Start by cloning the repository and installing the required packages. LunaTV uses pnpm as its package manager, though npm and yarn are supported alternatives.

git clone https://github.com/MoonTechLab/LunaTV.git
cd LunaTV
pnpm install

The installation process sets up Next.js 14 along with the custom webpack configuration needed for SVG transformation and PWA support defined in next.config.js.

Configure Environment Variables

Create a .env.local file in the project root to store sensitive configuration and public runtime variables. This file is automatically ignored by Git according to the repository's .gitignore, ensuring credentials remain secure.

USERNAME=admin
PASSWORD=your_secure_password
NEXT_PUBLIC_STORAGE_TYPE=kvrocks
KVROCKS_URL=redis://localhost:6666
NEXT_PUBLIC_SITE_NAME=LunaTV
ANNOUNCEMENT=Welcome to LunaTV!
NEXT_PUBLIC_DOUBAN_PROXY_TYPE=cmliussss-cdn-tencent

Critical variables for Next.js 14 App Router setup:

  • NEXT_PUBLIC_STORAGE_TYPE – Defines the storage backend (kvrocks, redis, or upstash)
  • NEXT_PUBLIC_SITE_NAME – Injected into the browser via window.RUNTIME_CONFIG for client components
  • USERNAME and PASSWORD – Admin credentials used by the auth middleware

The configuration loader in src/lib/config.ts reads these values at runtime, falling back to a JSON config file if environment variables are not present.

Start the Development Server

Launch the application using the development script:

pnpm dev

The server initializes on http://localhost:3000 with hot reloading enabled. Because next.config.js specifies output: 'standalone', the build process generates a self-contained Node.js server suitable for containerized deployments.

Verify the setup by navigating to the root URL. The home page renders via src/app/page.tsx while the global layout in src/app/layout.tsx wraps the content with theme providers and site-wide metadata.

Understanding the App Router Architecture

Root Layout and Server Components

The src/app/layout.tsx file serves as the Root Layout for the entire application. As a React Server Component, it executes exclusively on the server, enabling direct access to environment variables and external data sources during the request.

According to the source code, this layout performs three critical functions:

  1. Loads configuration via await getConfig() from src/lib/config.ts
  2. Generates dynamic metadata using the loaded site configuration
  3. Injects a <script> tag that serializes window.RUNTIME_CONFIG (lines 85-94) for client-side consumption
// src/app/layout.tsx (conceptual structure)
import { getConfig } from '@/lib/config';

export default async function RootLayout({ children }) {
  const cfg = await getConfig();
  return (
    <html lang="en">
      <body>
        <script
          dangerouslySetInnerHTML={{
            __html: `window.RUNTIME_CONFIG = ${JSON.stringify(cfg)}`
          }}
        />
        {children}
      </body>
    </html>
  );
}

File-System Routing Conventions

LunaTV uses the Next.js 14 App Router convention where directories in src/app map directly to URL paths. Dynamic segments use bracket notation [slug], and each route folder requires a page.tsx file to expose the UI.

To create a new route, add a folder structure:


src/app/
├── page.tsx           # /

├── layout.tsx         # Global layout

└── about/
    └── page.tsx       # /about

Example implementation for a custom about page:

// src/app/about/page.tsx
export default function AboutPage() {
  return (
    <main className="container mx-auto p-4">
      <h1 className="text-2xl font-bold">About LunaTV</h1>
      <p>Media management powered by Next.js 14 App Router.</p>
    </main>
  );
}

Runtime Configuration Injection

Client components cannot access server-side environment variables directly. LunaTV solves this by serializing configuration into the HTML document head within layout.tsx. Client components read from window.RUNTIME_CONFIG to access values like NEXT_PUBLIC_STORAGE_TYPE without bundling secrets into the JavaScript bundle.

Accessing Configuration in Components

Server Component Pattern

Use getConfig() in async server components to fetch settings during SSR:

// src/app/dashboard/page.tsx
import { getConfig } from '@/lib/config';

export default async function Dashboard() {
  const cfg = await getConfig();
  const storageBackend = cfg.SiteConfig.StorageType;
  
  return (
    <div>
      <h1>Dashboard</h1>
      <p>Connected to: {storageBackend}</p>
    </div>
  );
}

This pattern avoids exposing sensitive configuration to the client while allowing the component to render with fresh data on each request.

Client Component Pattern

For interactive components marked with the 'use client' directive, access the injected global:

// src/components/StorageIndicator.tsx
'use client';
import { useEffect, useState } from 'react';

export default function StorageIndicator() {
  const [type, setType] = useState<string>('unknown');

  useEffect(() => {
    // Access runtime config injected by layout.tsx
    const config = (window as any).RUNTIME_CONFIG;
    setType(config?.STORAGE_TYPE ?? 'localstorage');
  }, []);

  return <span>Storage: {type}</span>;
}

The window.RUNTIME_CONFIG object contains the serialized output of getConfig(), providing identical values to both server and client contexts.

Dynamic Metadata Generation

Next.js 14 App Router supports async generateMetadata exports that execute during the server render. LunaTV implements this pattern in src/app/layout.tsx to set page titles and OpenGraph tags based on the site configuration.

Example for a dynamic blog route:

// src/app/blog/[slug]/page.tsx
import type { Metadata } from 'next';
import { getPostMeta } from '@/lib/blog';

export async function generateMetadata({ 
  params 
}: { 
  params: { slug: string } 
}): Promise<Metadata> {
  const meta = await getPostMeta(params.slug);
  return {
    title: `${meta.title} | LunaTV`,
    description: meta.excerpt,
  };
}

This executes at request time, allowing SEO metadata to reflect current database state rather than build-time constants.

Deployment Configuration

Docker Standalone Build

The repository includes a production-ready Dockerfile that leverages the standalone output mode:


# Build the standalone output

pnpm build

# Run the container

docker run -p 3000:3000 -e NEXT_PUBLIC_SITE_NAME=LunaTV lunatv:latest

The docker-compose.dev.yml file provides a local development stack including Kvrocks and Redis for testing the data layer defined in src/lib/db.client.ts.

Platform-Specific Deployment

Vercel: Connect the GitHub repository; the build command next build executes automatically using the App Router. Ensure environment variables are configured in the Vercel dashboard, not just .env.local.

Zeabur: The containerized build works out-of-the-box. Set the NEXT_PUBLIC_* variables in the Zeabur console before deploying.

Self-hosted: Use the standalone output to run Node.js directly without containerization, though Docker is recommended for production consistency.

Summary

  • Repository structure: LunaTV uses Next.js 14 with the App Router in src/app/, where folders define routes and layout.tsx provides global server-side wrapping.
  • Configuration flow: src/lib/config.ts provides getConfig() for server components, while src/app/layout.tsx injects values into window.RUNTIME_CONFIG for client components.
  • Environment setup: Required variables include NEXT_PUBLIC_STORAGE_TYPE, database URLs, and admin credentials in .env.local.
  • Development workflow: Run pnpm dev for local development; use output: 'standalone' in next.config.js for production Docker builds.
  • Extensibility: Add new routes by creating folders under src/app, and access shared configuration consistently across server and client boundaries.

Frequently Asked Questions

Does LunaTV require the Pages Router or App Router?

LunaTV requires the App Router introduced in Next.js 13 and stabilized in Next.js 14. The application relies on React Server Components in src/app/layout.tsx and src/app/page.tsx for server-side data fetching and runtime configuration injection. The traditional Pages Router in src/pages is not used in this codebase.

How does LunaTV handle environment variables in client components?

Client components cannot access process.env directly. Instead, the Root Layout in src/app/layout.tsx serializes the configuration object into a global window.RUNTIME_CONFIG variable using a <script> tag. Client components read from this object at runtime, ensuring sensitive values never appear in the client-side JavaScript bundle while still allowing public configuration to be accessible.

What storage backends are supported by the data layer?

According to src/lib/db.client.ts, LunaTV abstracts storage behind a unified client that supports Redis, Kvrocks, and Upstash. Set NEXT_PUBLIC_STORAGE_TYPE to your preferred backend and provide the connection URL (e.g., KVROCKS_URL) in your environment file. The docker-compose.dev.yml includes a Kvrocks service for local development.

Can I customize the Next.js configuration for external image CDNs?

Yes. The next.config.js file disables Next.js image optimization (images: { unoptimized: true }) specifically to support external CDN hosting without the Image Optimization API. You can also modify the webpack configuration (lines 38-55) to adjust SVG handling via @svgr/webpack or enable additional PWA features through the withPWA wrapper.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →