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 viawindow.RUNTIME_CONFIGfor client componentsUSERNAMEandPASSWORD– 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:
- Loads configuration via
await getConfig()fromsrc/lib/config.ts - Generates dynamic metadata using the loaded site configuration
- Injects a
<script>tag that serializeswindow.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 andlayout.tsxprovides global server-side wrapping. - Configuration flow:
src/lib/config.tsprovidesgetConfig()for server components, whilesrc/app/layout.tsxinjects values intowindow.RUNTIME_CONFIGfor client components. - Environment setup: Required variables include
NEXT_PUBLIC_STORAGE_TYPE, database URLs, and admin credentials in.env.local. - Development workflow: Run
pnpm devfor local development; useoutput: 'standalone'innext.config.jsfor 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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →