Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 64 additions & 0 deletions .github/workflows/pages.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
name: Pages Demo

on:
push:
branches:
- main
workflow_dispatch:

permissions:
contents: read
pages: write
id-token: write

concurrency:
group: pages
cancel-in-progress: true

jobs:
build:
name: Build demo
runs-on: ubuntu-latest
timeout-minutes: 30
env:
VITE_YEOLLIN_BASE_PATH: /yeollin-cms
VITE_YEOLLIN_DEMO: 'true'

steps:
- uses: actions/checkout@v7

- uses: actions-rust-lang/setup-rust-toolchain@v1

- uses: oven-sh/setup-bun@v2
with:
bun-version: latest

- name: Install dependencies
run: bun install --frozen-lockfile

- name: Build interactive demo
working-directory: apps/example-app
run: cargo run -p yeollin-cli -- build --skip-backend

- name: Disable Jekyll processing
run: touch apps/example-app/.yeollin/app/out/.nojekyll

- uses: actions/configure-pages@v6

- name: Upload Pages artifact
uses: actions/upload-pages-artifact@v5
with:
path: apps/example-app/.yeollin/app/out

deploy:
name: Deploy demo
needs: build
runs-on: ubuntu-latest
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}

steps:
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v5
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@

Yeollin CMS is a Tauri-inspired, plugin-based CMS *framework* rather than a finished CMS product. A plugin is a single Rust crate that bundles its Axum/Vespera API routes, a vinext (Vite + RSC, Next-compatible) frontend under `app/`, and optionally sea-orm models with vespertide JSON migrations. The `yeollin-cli` binary assembles the registered plugins into one application: it extracts the frontend template, merges each plugin's pages, statically exports the result, and embeds it into the release binary via `include_dir!`, so a deployment is a single executable that serves both the API and the UI.

Try the serverless [interactive demo](https://dev-five-git.github.io/yeollin-cms/). It includes the example app's plugins and keeps all mock data inside the browser tab.

## Status

> **Status: v0.1, pre-release. APIs change without notice. Not production ready.**
>
> This repository is private and under active development. Treat every public
> This project is open source and under active development. Treat every public
> interface, file layout, and CLI flag as unstable.

## Prerequisites
Expand Down
15 changes: 6 additions & 9 deletions packages/app/src/app/(auth)/settings/page.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { Box, Flex, Grid, Text, VStack } from '@devup-ui/react'
import { readFile } from 'fs/promises'
import Link from 'next/link'
import path from 'path'

import { AppLink } from '@/components/common/AppLink'

interface PluginInfo {
description?: string
name: string
Expand Down Expand Up @@ -41,11 +42,7 @@ export default async function SettingsPage() {

<Grid columns={['1fr', null, '1fr 1fr']} gap={4} w="100%">
{configurablePlugins.map((plugin) => (
<Link
key={plugin.name}
href={plugin.settings!.pagePath}
style={{ textDecoration: 'none' }}
>
<AppLink key={plugin.name} href={plugin.settings!.pagePath}>
<Box
_hover={{
borderColor: '$primary',
Expand All @@ -72,10 +69,10 @@ export default async function SettingsPage() {
{plugin.description ?? 'Configure plugin settings'}
</Text>
</Box>
</Link>
</AppLink>
))}

<Link href="/settings/plugins" style={{ textDecoration: 'none' }}>
<AppLink href="/settings/plugins">
<Box
_hover={{
borderColor: '$primary',
Expand All @@ -95,7 +92,7 @@ export default async function SettingsPage() {
View versions, authors, and plugin metadata.
</Text>
</Box>
</Link>
</AppLink>
</Grid>
</VStack>
</Box>
Expand Down
7 changes: 4 additions & 3 deletions packages/app/src/app/(auth)/settings/plugins/page.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { Box, Flex, Grid, Text, VStack } from '@devup-ui/react'
import { readFile } from 'fs/promises'
import Link from 'next/link'
import path from 'path'

import { AppLink } from '@/components/common/AppLink'

interface PluginInfo {
name: string
version: string
Expand Down Expand Up @@ -86,7 +87,7 @@ export default async function PluginsPage() {
<Box p={6}>
<VStack alignItems="flex-start" gap={6}>
<Flex alignItems="center" gap={4}>
<Link href="/settings" style={{ textDecoration: 'none' }}>
<AppLink href="/settings">
<Text
_hover={{ color: '$primary' }}
color="$textSecondary"
Expand All @@ -95,7 +96,7 @@ export default async function PluginsPage() {
>
Settings
</Text>
</Link>
</AppLink>
<Text color="$textTertiary">/</Text>
<Text color="$text" typography="body">
Plugins
Expand Down
10 changes: 8 additions & 2 deletions packages/app/src/app/(guest)/signin/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import { Box, Flex, Text, VStack } from '@devup-ui/react'
import { useRouter } from 'next/navigation'
import { useState } from 'react'

import { DEMO_MODE, demoPageHref } from '@/lib/demo-path'

export default function SignInPage() {
const router = useRouter()
const [username, setUsername] = useState('')
Expand Down Expand Up @@ -37,8 +39,12 @@ export default function SignInPage() {
document.cookie = `refresh_token=${data.refresh_token}; path=/; max-age=${60 * 60 * 24 * 7}` // 7 days

// Redirect to dashboard
router.push('/')
router.refresh()
if (DEMO_MODE) {
window.location.assign(demoPageHref('/'))
} else {
router.push('/')
router.refresh()
}
} catch (err) {
setError(err instanceof Error ? err.message : 'An error occurred')
} finally {
Expand Down
29 changes: 29 additions & 0 deletions packages/app/src/components/common/AppLink.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
'use client'

import Link from 'next/link'
import type { ReactNode } from 'react'

import { DEMO_MODE, demoPageHref } from '@/lib/demo-path'

interface AppLinkProps {
children: ReactNode
href: string
}

export function AppLink({ children, href }: AppLinkProps) {
const style = { textDecoration: 'none' }

if (DEMO_MODE) {
return (
<a href={demoPageHref(href)} style={style}>
{children}
</a>
)
}

return (
<Link href={href} style={style}>
{children}
</Link>
)
}
11 changes: 4 additions & 7 deletions packages/app/src/components/content/ContentCollectionsHub.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Box, Grid, Text, VStack } from '@devup-ui/react'
import Link from 'next/link'

import { AppLink } from '@/components/common/AppLink'

interface ContentCollectionLink {
label: string
Expand Down Expand Up @@ -38,11 +39,7 @@ export function ContentCollectionsHub({
gridTemplateColumns={['1fr', 'repeat(2, minmax(0, 1fr))']}
>
{collections.map((collection) => (
<Link
key={collection.path}
href={collection.path}
style={{ textDecoration: 'none' }}
>
<AppLink key={collection.path} href={collection.path}>
<Box
_hover={{
borderColor: '$primary',
Expand All @@ -61,7 +58,7 @@ export function ContentCollectionsHub({
</Text>
</VStack>
</Box>
</Link>
</AppLink>
))}
</Grid>
</VStack>
Expand Down
33 changes: 30 additions & 3 deletions packages/app/src/components/layout/Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,22 @@
import { Box, Flex, Text } from '@devup-ui/react'
import { useRouter } from 'next/navigation'

import { resetMockApi } from '@/lib/mock-api'

const DEMO_MODE = import.meta.env.VITE_YEOLLIN_DEMO === 'true'

/**
* Header component with logout functionality.
* Requires 'use client' due to cookie manipulation and useRouter.
*/
export function Header() {
const router = useRouter()

const handleReset = () => {
resetMockApi()
window.location.reload()
}

const handleLogout = () => {
// Clear auth cookies
document.cookie = 'access_token=; path=/; max-age=0'
Expand All @@ -26,21 +35,39 @@ export function Header() {
borderBottom="1px solid $border"
display="flex"
h="56px"
justifyContent="flex-end"
justifyContent="space-between"
px={6}
w="100%"
>
{DEMO_MODE ? (
<Flex alignItems="center" gap={2}>
<Box bg="$primaryLight" borderRadius="999px" px={3} py={1}>
<Text color="$primary" typography="label">
Interactive demo
</Text>
</Box>
<Text
color="$textTertiary"
display={['none', null, 'block']}
typography="label"
>
Data stays in this browser tab
</Text>
</Flex>
) : (
<Box />
)}
<Flex
_hover={{ bg: '$backgroundSecondary', color: '$text' }}
alignItems="center"
borderRadius="6px"
color="$textSecondary"
cursor="pointer"
onClick={handleLogout}
onClick={DEMO_MODE ? handleReset : handleLogout}
px={3}
py={2}
>
<Text typography="label">Logout</Text>
<Text typography="label">{DEMO_MODE ? 'Reset demo' : 'Logout'}</Text>
</Flex>
</Box>
)
Expand Down
18 changes: 8 additions & 10 deletions packages/app/src/components/layout/NavItem.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
'use client'

import { Box, Flex, Text } from '@devup-ui/react'
import Link from 'next/link'
import { usePathname } from 'next/navigation'

import { AppLink } from '@/components/common/AppLink'
import { normalizeDemoPath } from '@/lib/demo-path'

interface MenuItem {
id: string
label: string
Expand All @@ -20,14 +22,14 @@ interface NavItemProps {
* Requires 'use client' due to usePathname hook.
*/
export function NavItem({ item }: NavItemProps) {
const pathname = usePathname()
const pathname = normalizeDemoPath(usePathname())
const isActive = pathname === item.path
const hasChildren = item.children && item.children.length > 0
const isChildActive = item.children?.some((child) => pathname === child.path)

return (
<Box flexShrink={0}>
<Link href={item.path} style={{ textDecoration: 'none' }}>
<AppLink href={item.path}>
<Flex
_hover={{ bg: isActive ? '$primary' : '$backgroundSecondary' }}
alignItems="center"
Expand All @@ -41,16 +43,12 @@ export function NavItem({ item }: NavItemProps) {
>
<Text fontSize="14px">{item.label}</Text>
</Flex>
</Link>
</AppLink>

{hasChildren && (isActive || isChildActive) && (
<Flex flexDirection="column" gap={1} ml={[0, null, 4]} mt={1}>
{item.children!.map((child) => (
<Link
key={child.id}
href={child.path}
style={{ textDecoration: 'none' }}
>
<AppLink key={child.id} href={child.path}>
<Flex
_hover={{ bg: '$backgroundSecondary' }}
alignItems="center"
Expand All @@ -67,7 +65,7 @@ export function NavItem({ item }: NavItemProps) {
>
<Text fontSize="13px">{child.label}</Text>
</Flex>
</Link>
</AppLink>
))}
</Flex>
)}
Expand Down
4 changes: 4 additions & 0 deletions packages/app/src/components/providers.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { useState } from 'react'

import { installMockApi } from '@/lib/mock-api'

if (import.meta.env.VITE_YEOLLIN_DEMO === 'true') installMockApi()

export function Providers({ children }: { children: React.ReactNode }) {
const [queryClient] = useState(
() =>
Expand Down
21 changes: 21 additions & 0 deletions packages/app/src/lib/demo-path.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
export const DEMO_MODE = import.meta.env.VITE_YEOLLIN_DEMO === 'true'

const DEMO_BASE_PATH = import.meta.env.VITE_YEOLLIN_BASE_PATH ?? ''

export function demoPageHref(path: string): string {
if (!DEMO_MODE) return path
if (path === '/') return `${DEMO_BASE_PATH}/`

return `${DEMO_BASE_PATH}${path}.html`
}

export function normalizeDemoPath(pathname: string): string {
if (!DEMO_MODE) return pathname

const withoutBase = pathname.startsWith(DEMO_BASE_PATH)
? pathname.slice(DEMO_BASE_PATH.length)
: pathname
const withoutHtml = withoutBase.replace(/\.html$/, '')

return withoutHtml || '/'
}
Loading
Loading