๐Ÿ””Welcome

HaloLight multi-framework admin dashboard docs is now live!

Supports 12+ framework versions. Welcome to try.

Skip to content

Fresh (Deno) Version โ€‹

HaloLight Fresh version is built on Fresh 2 + Deno, using Islands architecture + Preact to deliver a zero-config, ultra-fast admin dashboard.

Live Preview: https://halolight-fresh.h7ml.cn/

GitHub: https://github.com/halolight/halolight-fresh

Tech Stack โ€‹

TechnologyVersionDescription
Fresh2.xDeno full-stack framework
Deno2.xModern JavaScript runtime
Preact10.xLightweight UI library
@preact/signals2.xReactive state
TypeScriptBuilt-inType safety
Tailwind CSSBuilt-inAtomic CSS
Zod3.xData validation
Chart.js4.xChart visualization

Core Features โ€‹

  • Zero Config: Works out of the box, no complex configuration
  • Islands Architecture: Zero JS by default, hydrate on demand
  • Edge First: Native support for Deno Deploy edge deployment
  • Built-in TypeScript: No configuration needed, use directly
  • JIT Rendering: No build step, instant rendering
  • Secure by Default: Deno sandbox security model

Directory Structure โ€‹

halolight-fresh/
โ”œโ”€โ”€ routes/                        # File-based routing
โ”‚   โ”œโ”€โ”€ _app.tsx                  # Root layout
โ”‚   โ”œโ”€โ”€ _layout.tsx               # Default layout
โ”‚   โ”œโ”€โ”€ _middleware.ts            # Global middleware
โ”‚   โ”œโ”€โ”€ index.tsx                 # Homepage
โ”‚   โ”œโ”€โ”€ auth/                     # Auth pages
โ”‚   โ”‚   โ”œโ”€โ”€ login.tsx
โ”‚   โ”‚   โ”œโ”€โ”€ register.tsx
โ”‚   โ”‚   โ”œโ”€โ”€ forgot-password.tsx
โ”‚   โ”‚   โ””โ”€โ”€ reset-password.tsx
โ”‚   โ”œโ”€โ”€ dashboard/                # Dashboard pages
โ”‚   โ”‚   โ”œโ”€โ”€ _layout.tsx           # Dashboard layout
โ”‚   โ”‚   โ”œโ”€โ”€ _middleware.ts        # Auth middleware
โ”‚   โ”‚   โ”œโ”€โ”€ index.tsx
โ”‚   โ”‚   โ”œโ”€โ”€ users/
โ”‚   โ”‚   โ”‚   โ”œโ”€โ”€ index.tsx
โ”‚   โ”‚   โ”‚   โ”œโ”€โ”€ create.tsx
โ”‚   โ”‚   โ”‚   โ””โ”€โ”€ [id].tsx
โ”‚   โ”‚   โ”œโ”€โ”€ roles.tsx
โ”‚   โ”‚   โ”œโ”€โ”€ permissions.tsx
โ”‚   โ”‚   โ”œโ”€โ”€ settings.tsx
โ”‚   โ”‚   โ””โ”€โ”€ profile.tsx
โ”‚   โ””โ”€โ”€ api/                      # API routes
โ”‚       โ””โ”€โ”€ auth/
โ”‚           โ”œโ”€โ”€ login.ts
โ”‚           โ”œโ”€โ”€ register.ts
โ”‚           โ””โ”€โ”€ me.ts
โ”œโ”€โ”€ islands/                      # Interactive Islands
โ”‚   โ”œโ”€โ”€ LoginForm.tsx
โ”‚   โ”œโ”€โ”€ UserTable.tsx
โ”‚   โ”œโ”€โ”€ DashboardGrid.tsx
โ”‚   โ”œโ”€โ”€ ThemeToggle.tsx
โ”‚   โ””โ”€โ”€ Sidebar.tsx
โ”œโ”€โ”€ components/                   # Static components
โ”‚   โ”œโ”€โ”€ ui/                       # UI components
โ”‚   โ”‚   โ”œโ”€โ”€ Button.tsx
โ”‚   โ”‚   โ”œโ”€โ”€ Input.tsx
โ”‚   โ”‚   โ”œโ”€โ”€ Card.tsx
โ”‚   โ”‚   โ””โ”€โ”€ ...
โ”‚   โ”œโ”€โ”€ layout/                   # Layout components
โ”‚   โ”‚   โ”œโ”€โ”€ AdminLayout.tsx
โ”‚   โ”‚   โ”œโ”€โ”€ AuthLayout.tsx
โ”‚   โ”‚   โ””โ”€โ”€ Header.tsx
โ”‚   โ””โ”€โ”€ shared/                   # Shared components
โ”‚       โ””โ”€โ”€ PermissionGuard.tsx
โ”œโ”€โ”€ lib/                          # Utilities
โ”‚   โ”œโ”€โ”€ auth.ts
โ”‚   โ”œโ”€โ”€ permission.ts
โ”‚   โ”œโ”€โ”€ session.ts
โ”‚   โ””โ”€โ”€ cn.ts
โ”œโ”€โ”€ signals/                      # State management
โ”‚   โ”œโ”€โ”€ auth.ts
โ”‚   โ”œโ”€โ”€ ui-settings.ts
โ”‚   โ””โ”€โ”€ dashboard.ts
โ”œโ”€โ”€ static/                       # Static assets
โ”œโ”€โ”€ fresh.config.ts              # Fresh config
โ”œโ”€โ”€ deno.json                    # Deno config
โ””โ”€โ”€ tailwind.config.ts           # Tailwind config

Quick Start โ€‹

Install Deno โ€‹

bash
# macOS/Linux
curl -fsSL https://deno.land/install.sh | sh

# Windows
irm https://deno.land/install.ps1 | iex

Clone Project โ€‹

bash
git clone https://github.com/halolight/halolight-fresh.git
cd halolight-fresh

Environment Variables โ€‹

bash
cp .env.example .env
env
# .env example
API_URL=/api
USE_MOCK=true
DEMO_EMAIL=admin@halolight.h7ml.cn
DEMO_PASSWORD=123456
SHOW_DEMO_HINT=true
APP_TITLE=Admin Pro
BRAND_NAME=Halolight
SESSION_SECRET=your-secret-key

Start Development โ€‹

bash
deno task dev

Visit http://localhost:8000

Production Build โ€‹

bash
deno task build
deno task start

Core Features โ€‹

State Management (@preact/signals) โ€‹

ts
// signals/auth.ts
import { signal, computed, effect } from '@preact/signals'
import { IS_BROWSER } from '$fresh/runtime.ts'

interface User {
  id: number
  name: string
  email: string
  permissions: string[]
}

export const user = signal<User | null>(null)
export const token = signal<string | null>(null)
export const loading = signal(false)

export const isAuthenticated = computed(() => !!token.value && !!user.value)
export const permissions = computed(() => user.value?.permissions ?? [])

// Only persist in browser
if (IS_BROWSER) {
  const saved = localStorage.getItem('auth')
  if (saved) {
    const { user: savedUser, token: savedToken } = JSON.parse(saved)
    user.value = savedUser
    token.value = savedToken
  }

  effect(() => {
    if (user.value && token.value) {
      localStorage.setItem('auth', JSON.stringify({
        user: user.value,
        token: token.value,
      }))
    }
  })
}

export async function login(credentials: { email: string; password: string }) {
  loading.value = true
  try {
    const response = await fetch('/api/auth/login', {
      method: 'POST',
      body: JSON.stringify(credentials),
      headers: { 'Content-Type': 'application/json' },
    })
    const data = await response.json()

    user.value = data.user
    token.value = data.token
  } finally {
    loading.value = false
  }
}

export function logout() {
  user.value = null
  token.value = null
  if (IS_BROWSER) {
    localStorage.removeItem('auth')
  }
}

export function hasPermission(permission: string): boolean {
  const perms = permissions.value
  return perms.some((p) =>
    p === '*' || p === permission ||
    (p.endsWith(':*') && permission.startsWith(p.slice(0, -1)))
  )
}

Auth Middleware โ€‹

ts
// routes/dashboard/_middleware.ts
import { FreshContext } from '$fresh/server.ts'
import { getCookies } from '$std/http/cookie.ts'
import { verifyToken, getUser } from '../../lib/auth.ts'

export async function handler(req: Request, ctx: FreshContext) {
  const cookies = getCookies(req.headers)
  const token = cookies.token

  if (!token) {
    const url = new URL(req.url)
    return new Response(null, {
      status: 302,
      headers: { Location: `/auth/login?redirect=${url.pathname}` },
    })
  }

  try {
    const payload = await verifyToken(token)
    const user = await getUser(payload.userId)
    ctx.state.user = user
    ctx.state.token = token
  } catch {
    return new Response(null, {
      status: 302,
      headers: { Location: '/auth/login' },
    })
  }

  return ctx.next()
}

Islands (Interactive Components) โ€‹

tsx
// islands/LoginForm.tsx
import { useSignal } from '@preact/signals'
import { login, loading } from '../signals/auth.ts'
import { Button } from '../components/ui/Button.tsx'
import { Input } from '../components/ui/Input.tsx'

interface Props {
  redirectTo?: string
}

export default function LoginForm({ redirectTo = '/dashboard' }: Props) {
  const email = useSignal('')
  const password = useSignal('')
  const error = useSignal('')

  const handleSubmit = async (e: Event) => {
    e.preventDefault()
    error.value = ''

    try {
      await login({
        email: email.value,
        password: password.value,
      })
      globalThis.location.href = redirectTo
    } catch (e) {
      error.value = 'Invalid email or password'
    }
  }

  return (
    <form onSubmit={handleSubmit} class="space-y-4">
      {error.value && (
        <div class="text-destructive text-sm">{error.value}</div>
      )}

      <Input
        type="email"
        label="Email"
        value={email.value}
        onInput={(e) => email.value = e.currentTarget.value}
        required
      />

      <Input
        type="password"
        label="Password"
        value={password.value}
        onInput={(e) => password.value = e.currentTarget.value}
        required
      />

      <Button type="submit" class="w-full" disabled={loading.value}>
        {loading.value ? 'Logging in...' : 'Login'}
      </Button>
    </form>
  )
}

Page Routes โ€‹

tsx
// routes/auth/login.tsx
import { Handlers, PageProps } from '$fresh/server.ts'
import { AuthLayout } from '../../components/layout/AuthLayout.tsx'
import LoginForm from '../../islands/LoginForm.tsx'

export const handler: Handlers = {
  GET(req, ctx) {
    const url = new URL(req.url)
    const redirect = url.searchParams.get('redirect') || '/dashboard'
    return ctx.render({ redirect })
  },
}

export default function LoginPage({ data }: PageProps<{ redirect: string }>) {
  return (
    <AuthLayout>
      <div class="max-w-md mx-auto">
        <h1 class="text-2xl font-bold text-center mb-8">Login</h1>
        <LoginForm redirectTo={data.redirect} />
      </div>
    </AuthLayout>
  )
}

API Routes โ€‹

ts
// routes/api/auth/login.ts
import { Handlers } from '$fresh/server.ts'
import { z } from 'zod'
import { setCookie } from '$std/http/cookie.ts'
import { createToken } from '../../../lib/auth.ts'

const loginSchema = z.object({
  email: z.string().email(),
  password: z.string().min(6),
})

export const handler: Handlers = {
  async POST(req) {
    try {
      const body = await req.json()
      const { email, password } = loginSchema.parse(body)

      // Authenticate user (example)
      const user = await authenticateUser(email, password)
      if (!user) {
        return new Response(
          JSON.stringify({ error: 'Invalid email or password' }),
          { status: 401, headers: { 'Content-Type': 'application/json' } }
        )
      }

      const token = await createToken({ userId: user.id })

      const response = new Response(
        JSON.stringify({ user, token }),
        { headers: { 'Content-Type': 'application/json' } }
      )

      setCookie(response.headers, {
        name: 'token',
        value: token,
        path: '/',
        httpOnly: true,
        sameSite: 'Lax',
        maxAge: 60 * 60 * 24 * 7,
      })

      return response
    } catch (e) {
      if (e instanceof z.ZodError) {
        return new Response(
          JSON.stringify({ error: 'Validation failed', details: e.errors }),
          { status: 400, headers: { 'Content-Type': 'application/json' } }
        )
      }
      return new Response(
        JSON.stringify({ error: 'Server error' }),
        { status: 500, headers: { 'Content-Type': 'application/json' } }
      )
    }
  },
}

Permission Component โ€‹

tsx
// components/shared/PermissionGuard.tsx
import { ComponentChildren } from 'preact'

interface Props {
  permission: string
  userPermissions: string[]
  children: ComponentChildren
  fallback?: ComponentChildren
}

function checkPermission(
  userPermissions: string[],
  permission: string
): boolean {
  return userPermissions.some((p) =>
    p === '*' || p === permission ||
    (p.endsWith(':*') && permission.startsWith(p.slice(0, -1)))
  )
}

export function PermissionGuard({
  permission,
  userPermissions,
  children,
  fallback,
}: Props) {
  if (!checkPermission(userPermissions, permission)) {
    return fallback ?? null
  }

  return <>{children}</>
}
tsx
// Usage (in server-side rendering)
<PermissionGuard
  permission="users:delete"
  userPermissions={ctx.state.user.permissions}
  fallback={<span class="text-muted-foreground">No permission</span>}
>
  <Button variant="destructive">Delete</Button>
</PermissionGuard>

Dashboard Layout โ€‹

tsx
// routes/dashboard/_layout.tsx
import { PageProps } from '$fresh/server.ts'
import { AdminLayout } from '../../components/layout/AdminLayout.tsx'
import Sidebar from '../../islands/Sidebar.tsx'

export default function DashboardLayout({ Component, state }: PageProps) {
  return (
    <AdminLayout>
      <div class="flex min-h-screen">
        <Sidebar user={state.user} />
        <main class="flex-1 p-6">
          <Component />
        </main>
      </div>
    </AdminLayout>
  )
}

Page Routes โ€‹

PathPagePermission
/HomepagePublic
/auth/loginLoginPublic
/auth/registerRegisterPublic
/auth/forgot-passwordForgot PasswordPublic
/auth/reset-passwordReset PasswordPublic
/dashboardDashboarddashboard:view
/dashboard/usersUser Listusers:list
/dashboard/users/createCreate Userusers:create
/dashboard/users/[id]User Detailusers:view
/dashboard/rolesRole Managementroles:list
/dashboard/permissionsPermission Managementpermissions:list
/dashboard/settingsSystem Settingssettings:view
/dashboard/profileProfileLogged in

Configuration โ€‹

Fresh Configuration โ€‹

ts
// fresh.config.ts
import { defineConfig } from '$fresh/server.ts'
import tailwind from '$fresh/plugins/tailwind.ts'

export default defineConfig({
  plugins: [tailwind()],
})

Deno Configuration โ€‹

json
// deno.json
{
  "lock": false,
  "tasks": {
    "check": "deno fmt --check && deno lint && deno check **/*.ts && deno check **/*.tsx",
    "dev": "deno run -A --watch=static/,routes/ dev.ts",
    "build": "deno run -A dev.ts build",
    "start": "deno run -A main.ts",
    "update": "deno run -A -r https://fresh.deno.dev/update ."
  },
  "imports": {
    "$fresh/": "https://deno.land/x/fresh@2.0.0/",
    "$std/": "https://deno.land/std@0.224.0/",
    "preact": "https://esm.sh/preact@10.22.0",
    "preact/": "https://esm.sh/preact@10.22.0/",
    "@preact/signals": "https://esm.sh/@preact/signals@1.2.3",
    "zod": "https://deno.land/x/zod@v3.23.0/mod.ts"
  },
  "compilerOptions": {
    "jsx": "react-jsx",
    "jsxImportSource": "preact"
  }
}

Deployment โ€‹

bash
# Install deployctl
deno install -A --no-check -r -f https://deno.land/x/deploy/deployctl.ts

# Deploy
deployctl deploy --project=halolight-fresh main.ts

Docker โ€‹

dockerfile
FROM denoland/deno:2.0.0

WORKDIR /app
COPY . .

RUN deno cache main.ts

EXPOSE 8000
CMD ["run", "-A", "main.ts"]

Self-hosted โ€‹

bash
# Build
deno task build

# Run
deno task start

Testing โ€‹

The project uses Deno's built-in testing framework, test files are located in the tests/ directory.

Test Structure โ€‹

tests/
โ”œโ”€โ”€ setup.ts              # Test environment setup
โ”‚   โ”œโ”€โ”€ localStorage mock
โ”‚   โ”œโ”€โ”€ sessionStorage mock
โ”‚   โ”œโ”€โ”€ matchMedia mock
โ”‚   โ””โ”€โ”€ Helper functions (createMockUser, mockAuthenticatedState, etc.)
โ””โ”€โ”€ lib/
    โ”œโ”€โ”€ utils.test.ts     # Utility function tests
    โ”œโ”€โ”€ config.test.ts    # Config tests
    โ””โ”€โ”€ stores.test.ts    # State management tests

Run Tests โ€‹

bash
# Run all tests
deno task test

# Watch mode
deno task test:watch

# Test coverage
deno task test:coverage

# Coverage report output to coverage/lcov.info

Test Example โ€‹

ts
// tests/lib/config.test.ts
import { assertEquals, assertExists } from "$std/assert/mod.ts";
import "../setup.ts";

import { hasPermission } from "../../lib/config.ts";
import type { Permission } from "../../lib/types.ts";

Deno.test("hasPermission - permission check", async (t) => {
  const userPermissions: Permission[] = ["dashboard:view", "users:view"];

  await t.step("should return true when user has permission", () => {
    const result = hasPermission(userPermissions, "dashboard:view");
    assertEquals(result, true);
  });

  await t.step("should support wildcard permissions", () => {
    const adminPermissions: Permission[] = ["*"];
    const result = hasPermission(adminPermissions, "dashboard:view");
    assertEquals(result, true);
  });
});

CI/CD โ€‹

The project uses GitHub Actions for continuous integration, configuration file is located at .github/workflows/ci.yml.

Workflow Tasks โ€‹

TaskDescriptionTrigger
lintFormat check, code check, type checkpush/PR
testRun tests and upload coveragepush/PR
buildProduction build verificationAfter lint/test pass
securityDeno security auditpush/PR
dependency-reviewDependency security reviewPR only

Local CI Commands โ€‹

bash
# Run full CI check
deno task ci

# Equivalent to
deno task fmt:check && deno task lint && deno task check && deno task test

Code Quality Configuration โ€‹

json
// deno.json
{
  "lint": {
    "rules": {
      "tags": ["recommended"],
      "exclude": [
        "no-explicit-any",
        "explicit-function-return-type",
        "explicit-module-boundary-types",
        "jsx-button-has-type",
        "no-unused-vars"
      ]
    }
  },
  "fmt": {
    "lineWidth": 100,
    "indentWidth": 2,
    "singleQuote": false,
    "semiColons": true
  }
}

Comparison with Other Versions โ€‹

FeatureFresh VersionAstro VersionNext.js Version
RuntimeDenoNode.jsNode.js
State Management@preact/signals-Zustand
Data FetchingHandlersLoad functionsTanStack Query
Form ValidationZodZodReact Hook Form + Zod
Server-sideBuilt-in@astrojs/nodeAPI Routes
Component LibraryCustom-shadcn/ui
Islands Architectureโœ…โœ…โŒ
Zero Configโœ…โŒโŒ
Edge DeploymentDeno DeployCloudflareVercel Edge
Build StepOptionalRequiredRequired