@spfn/cms 0.1.0-alpha.9 → 0.2.0-beta.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +320 -359
- package/dist/actions.d.ts +12 -6
- package/dist/actions.js +25 -10
- package/dist/actions.js.map +1 -1
- package/dist/config.d.ts +39 -0
- package/dist/config.js +39 -0
- package/dist/config.js.map +1 -0
- package/dist/errors.d.ts +149 -0
- package/dist/errors.js +164 -0
- package/dist/errors.js.map +1 -0
- package/dist/index.d.ts +138 -20
- package/dist/index.js +212 -23
- package/dist/index.js.map +1 -1
- package/dist/server.d.ts +44 -81
- package/dist/server.js +610 -256
- package/dist/server.js.map +1 -1
- package/migrations/0000_medical_ozymandias.sql +54 -0
- package/migrations/meta/0000_snapshot.json +336 -0
- package/migrations/meta/_journal.json +13 -0
- package/package.json +54 -44
- package/dist/actions.d.ts.map +0 -1
- package/dist/client.d.ts +0 -138
- package/dist/client.d.ts.map +0 -1
- package/dist/client.js +0 -62
- package/dist/client.js.map +0 -1
- package/dist/cms.config.d.ts +0 -77
- package/dist/cms.config.d.ts.map +0 -1
- package/dist/cms.config.js +0 -111
- package/dist/cms.config.js.map +0 -1
- package/dist/entities/cms-audit-logs.d.ts +0 -213
- package/dist/entities/cms-audit-logs.d.ts.map +0 -1
- package/dist/entities/cms-audit-logs.js +0 -103
- package/dist/entities/cms-audit-logs.js.map +0 -1
- package/dist/entities/cms-draft-cache.d.ts +0 -188
- package/dist/entities/cms-draft-cache.d.ts.map +0 -1
- package/dist/entities/cms-draft-cache.js +0 -112
- package/dist/entities/cms-draft-cache.js.map +0 -1
- package/dist/entities/cms-label-values.d.ts +0 -192
- package/dist/entities/cms-label-values.d.ts.map +0 -1
- package/dist/entities/cms-label-values.js +0 -105
- package/dist/entities/cms-label-values.js.map +0 -1
- package/dist/entities/cms-label-versions.d.ts +0 -207
- package/dist/entities/cms-label-versions.d.ts.map +0 -1
- package/dist/entities/cms-label-versions.js +0 -80
- package/dist/entities/cms-label-versions.js.map +0 -1
- package/dist/entities/cms-labels.d.ts +0 -189
- package/dist/entities/cms-labels.d.ts.map +0 -1
- package/dist/entities/cms-labels.js +0 -48
- package/dist/entities/cms-labels.js.map +0 -1
- package/dist/entities/cms-published-cache.d.ts +0 -199
- package/dist/entities/cms-published-cache.d.ts.map +0 -1
- package/dist/entities/cms-published-cache.js +0 -103
- package/dist/entities/cms-published-cache.js.map +0 -1
- package/dist/entities/index.d.ts +0 -10
- package/dist/entities/index.d.ts.map +0 -1
- package/dist/entities/index.js +0 -10
- package/dist/entities/index.js.map +0 -1
- package/dist/generators/index.d.ts +0 -19
- package/dist/generators/index.d.ts.map +0 -1
- package/dist/generators/index.js +0 -19
- package/dist/generators/index.js.map +0 -1
- package/dist/generators/label-sync-generator.d.ts +0 -33
- package/dist/generators/label-sync-generator.d.ts.map +0 -1
- package/dist/generators/label-sync-generator.js +0 -86
- package/dist/generators/label-sync-generator.js.map +0 -1
- package/dist/helpers/locale.actions.d.ts +0 -132
- package/dist/helpers/locale.actions.d.ts.map +0 -1
- package/dist/helpers/locale.actions.js +0 -210
- package/dist/helpers/locale.actions.js.map +0 -1
- package/dist/helpers/locale.constants.d.ts +0 -10
- package/dist/helpers/locale.constants.d.ts.map +0 -1
- package/dist/helpers/locale.constants.js +0 -10
- package/dist/helpers/locale.constants.js.map +0 -1
- package/dist/helpers/locale.d.ts +0 -17
- package/dist/helpers/locale.d.ts.map +0 -1
- package/dist/helpers/locale.js +0 -20
- package/dist/helpers/locale.js.map +0 -1
- package/dist/helpers/sync.d.ts +0 -41
- package/dist/helpers/sync.d.ts.map +0 -1
- package/dist/helpers/sync.js +0 -309
- package/dist/helpers/sync.js.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/init.d.ts +0 -31
- package/dist/init.d.ts.map +0 -1
- package/dist/init.js +0 -36
- package/dist/init.js.map +0 -1
- package/dist/labels/helpers.d.ts +0 -31
- package/dist/labels/helpers.d.ts.map +0 -1
- package/dist/labels/helpers.js +0 -60
- package/dist/labels/helpers.js.map +0 -1
- package/dist/labels/index.d.ts +0 -7
- package/dist/labels/index.d.ts.map +0 -1
- package/dist/labels/index.js +0 -7
- package/dist/labels/index.js.map +0 -1
- package/dist/repositories/cms-draft-cache.repository.d.ts +0 -62
- package/dist/repositories/cms-draft-cache.repository.d.ts.map +0 -1
- package/dist/repositories/cms-draft-cache.repository.js +0 -56
- package/dist/repositories/cms-draft-cache.repository.js.map +0 -1
- package/dist/repositories/cms-label-values.repository.d.ts +0 -32
- package/dist/repositories/cms-label-values.repository.d.ts.map +0 -1
- package/dist/repositories/cms-label-values.repository.js +0 -72
- package/dist/repositories/cms-label-values.repository.js.map +0 -1
- package/dist/repositories/cms-labels.repository.d.ts +0 -53
- package/dist/repositories/cms-labels.repository.d.ts.map +0 -1
- package/dist/repositories/cms-labels.repository.js +0 -77
- package/dist/repositories/cms-labels.repository.js.map +0 -1
- package/dist/repositories/cms-published-cache.repository.d.ts +0 -53
- package/dist/repositories/cms-published-cache.repository.d.ts.map +0 -1
- package/dist/repositories/cms-published-cache.repository.js +0 -54
- package/dist/repositories/cms-published-cache.repository.js.map +0 -1
- package/dist/repositories/index.d.ts +0 -8
- package/dist/repositories/index.d.ts.map +0 -1
- package/dist/repositories/index.js +0 -9
- package/dist/repositories/index.js.map +0 -1
- package/dist/routes/labels/[id]/contract.d.ts +0 -68
- package/dist/routes/labels/[id]/contract.d.ts.map +0 -1
- package/dist/routes/labels/[id]/contract.js +0 -84
- package/dist/routes/labels/[id]/contract.js.map +0 -1
- package/dist/routes/labels/[id]/index.d.ts +0 -10
- package/dist/routes/labels/[id]/index.d.ts.map +0 -1
- package/dist/routes/labels/[id]/index.js +0 -96
- package/dist/routes/labels/[id]/index.js.map +0 -1
- package/dist/routes/labels/by-key/[key]/contract.d.ts +0 -24
- package/dist/routes/labels/by-key/[key]/contract.d.ts.map +0 -1
- package/dist/routes/labels/by-key/[key]/contract.js +0 -28
- package/dist/routes/labels/by-key/[key]/contract.js.map +0 -1
- package/dist/routes/labels/by-key/[key]/index.d.ts +0 -8
- package/dist/routes/labels/by-key/[key]/index.d.ts.map +0 -1
- package/dist/routes/labels/by-key/[key]/index.js +0 -32
- package/dist/routes/labels/by-key/[key]/index.js.map +0 -1
- package/dist/routes/labels/contract.d.ts +0 -59
- package/dist/routes/labels/contract.d.ts.map +0 -1
- package/dist/routes/labels/contract.js +0 -75
- package/dist/routes/labels/contract.js.map +0 -1
- package/dist/routes/labels/index.d.ts +0 -10
- package/dist/routes/labels/index.d.ts.map +0 -1
- package/dist/routes/labels/index.js +0 -73
- package/dist/routes/labels/index.js.map +0 -1
- package/dist/routes/published-cache/contract.d.ts +0 -25
- package/dist/routes/published-cache/contract.d.ts.map +0 -1
- package/dist/routes/published-cache/contract.js +0 -35
- package/dist/routes/published-cache/contract.js.map +0 -1
- package/dist/routes/published-cache/index.d.ts +0 -8
- package/dist/routes/published-cache/index.d.ts.map +0 -1
- package/dist/routes/published-cache/index.js +0 -33
- package/dist/routes/published-cache/index.js.map +0 -1
- package/dist/routes/values/[labelId]/[version]/contract.d.ts +0 -29
- package/dist/routes/values/[labelId]/[version]/contract.d.ts.map +0 -1
- package/dist/routes/values/[labelId]/[version]/contract.js +0 -33
- package/dist/routes/values/[labelId]/[version]/contract.js.map +0 -1
- package/dist/routes/values/[labelId]/[version]/index.d.ts +0 -8
- package/dist/routes/values/[labelId]/[version]/index.d.ts.map +0 -1
- package/dist/routes/values/[labelId]/[version]/index.js +0 -45
- package/dist/routes/values/[labelId]/[version]/index.js.map +0 -1
- package/dist/routes/values/[labelId]/contract.d.ts +0 -38
- package/dist/routes/values/[labelId]/contract.d.ts.map +0 -1
- package/dist/routes/values/[labelId]/contract.js +0 -59
- package/dist/routes/values/[labelId]/contract.js.map +0 -1
- package/dist/routes/values/[labelId]/index.d.ts +0 -8
- package/dist/routes/values/[labelId]/index.d.ts.map +0 -1
- package/dist/routes/values/[labelId]/index.js +0 -42
- package/dist/routes/values/[labelId]/index.js.map +0 -1
- package/dist/server.d.ts.map +0 -1
- package/dist/store.d.ts +0 -87
- package/dist/store.d.ts.map +0 -1
- package/dist/store.js +0 -205
- package/dist/store.js.map +0 -1
- package/dist/types.d.ts +0 -74
- package/dist/types.d.ts.map +0 -1
- package/dist/types.js +0 -7
- package/dist/types.js.map +0 -1
package/README.md
CHANGED
|
@@ -1,490 +1,451 @@
|
|
|
1
1
|
# @spfn/cms
|
|
2
2
|
|
|
3
|
-
Content Management System for Next.js with
|
|
3
|
+
Type-safe Content Management System for Next.js with automatic database synchronization and published cache.
|
|
4
4
|
|
|
5
5
|
## Features
|
|
6
6
|
|
|
7
|
-
-
|
|
8
|
-
- 🔄 **Auto-sync
|
|
9
|
-
- 🌐 **Multi-language support
|
|
10
|
-
- 🍪 **
|
|
11
|
-
-
|
|
12
|
-
-
|
|
13
|
-
-
|
|
14
|
-
-
|
|
15
|
-
- 🛠️ **Built on Drizzle ORM**
|
|
7
|
+
- 🎯 **Type-safe labels** - Full TypeScript support with autocomplete
|
|
8
|
+
- 🔄 **Auto-sync** - Database synchronization on server startup
|
|
9
|
+
- 🌐 **Multi-language** - Type-checked locale support
|
|
10
|
+
- 🍪 **Smart locale detection** - Cookie-based with automatic fallback
|
|
11
|
+
- 💾 **Published cache** - 17x faster queries (5ms vs 87ms)
|
|
12
|
+
- 🎨 **Nested structure** - Organize labels hierarchically
|
|
13
|
+
- 🔧 **Template variables** - Dynamic content with `{placeholder}` syntax
|
|
14
|
+
- 📝 **Draft & versioning** - Version control and audit logs
|
|
16
15
|
|
|
17
16
|
## Installation
|
|
18
17
|
|
|
19
|
-
### Recommended: Using SPFN CLI (Automatic Database Setup)
|
|
20
|
-
|
|
21
18
|
```bash
|
|
22
19
|
pnpm spfn add @spfn/cms
|
|
23
20
|
```
|
|
24
21
|
|
|
25
|
-
|
|
26
|
-
1. ✅ Install the package
|
|
27
|
-
2. ✅ Discover CMS database schemas automatically
|
|
28
|
-
3. ✅ Generate migrations for 6 CMS tables
|
|
29
|
-
4. ✅ Apply migrations to your database
|
|
30
|
-
5. ✅ Show setup guide
|
|
31
|
-
|
|
32
|
-
**Tables created:**
|
|
33
|
-
- `cms_labels` - Label definitions (10 columns, 2 indexes)
|
|
34
|
-
- `cms_label_values` - Label values per locale (7 columns, 2 indexes, 1 FK)
|
|
35
|
-
- `cms_label_versions` - Version history (9 columns, 2 indexes, 1 FK)
|
|
36
|
-
- `cms_draft_cache` - Draft content cache (6 columns, 2 indexes)
|
|
37
|
-
- `cms_published_cache` - Published content cache (7 columns, 1 index)
|
|
38
|
-
- `cms_audit_logs` - Change audit trail (8 columns, 4 indexes, 1 FK)
|
|
22
|
+
## Quick Start
|
|
39
23
|
|
|
40
|
-
###
|
|
24
|
+
### 1. Define Labels & Configuration
|
|
41
25
|
|
|
42
|
-
```
|
|
43
|
-
|
|
44
|
-
|
|
26
|
+
```typescript
|
|
27
|
+
// labels.ts
|
|
28
|
+
import { defineLabelConfig, defineLabels, createCmsClient } from '@spfn/cms';
|
|
29
|
+
|
|
30
|
+
// Configure locales
|
|
31
|
+
export const labelConfig = defineLabelConfig({
|
|
32
|
+
locales: ['en', 'ko'] as const,
|
|
33
|
+
defaultLocale: 'ko',
|
|
34
|
+
fallbackLocale: 'en'
|
|
35
|
+
});
|
|
45
36
|
|
|
46
|
-
|
|
37
|
+
// Define labels with nested structure
|
|
38
|
+
export const labelsDefinition = defineLabels({
|
|
39
|
+
home: {
|
|
40
|
+
hero: {
|
|
41
|
+
title: { en: "Welcome", ko: "환영합니다" },
|
|
42
|
+
subtitle: { en: "Start your journey", ko: "여정을 시작하세요" },
|
|
43
|
+
greeting: { en: "Hello {name}!", ko: "{name}님 안녕하세요!" }
|
|
44
|
+
},
|
|
45
|
+
cta: { en: "Get Started", ko: "시작하기" }
|
|
46
|
+
},
|
|
47
|
+
about: {
|
|
48
|
+
title: { en: "About Us", ko: "회사 소개" }
|
|
49
|
+
}
|
|
50
|
+
});
|
|
47
51
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
52
|
+
// Create client with API, getLabel, getLabels, and format
|
|
53
|
+
export const { api, getLabel, getLabels, format } = createCmsClient(
|
|
54
|
+
labelsDefinition,
|
|
55
|
+
labelConfig
|
|
56
|
+
);
|
|
51
57
|
```
|
|
52
58
|
|
|
53
|
-
|
|
59
|
+
### 2. Enable Auto-Sync
|
|
54
60
|
|
|
55
|
-
|
|
61
|
+
```typescript
|
|
62
|
+
// server.config.ts
|
|
63
|
+
import { defineServerConfig } from '@spfn/core/server';
|
|
64
|
+
import { syncLabels } from '@spfn/cms/server';
|
|
65
|
+
import { labelsDefinition } from './labels';
|
|
56
66
|
|
|
57
|
-
### 1. Create Label Files
|
|
58
67
|
|
|
59
|
-
|
|
68
|
+
// Option 1: Single definition
|
|
69
|
+
export default defineServerConfig()
|
|
70
|
+
.lifecycle({
|
|
71
|
+
afterInfrastructure: async () => {
|
|
72
|
+
await syncLabels(labelsDefinition);
|
|
73
|
+
}
|
|
74
|
+
})
|
|
75
|
+
.build();
|
|
60
76
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
nav.json ← Category
|
|
65
|
-
footer.json
|
|
66
|
-
home/
|
|
67
|
-
hero.json
|
|
68
|
-
features.json
|
|
69
|
-
```
|
|
77
|
+
// Option 2: Multiple definitions (organized in separate files)
|
|
78
|
+
import { homeLabels } from './labels/home';
|
|
79
|
+
import { aboutLabels } from './labels/about';
|
|
70
80
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
"description": "Navigation link for About page"
|
|
79
|
-
},
|
|
80
|
-
"services": {
|
|
81
|
-
"key": "layout.nav.services",
|
|
82
|
-
"defaultValue": "Services",
|
|
83
|
-
"description": "Navigation link for Services page"
|
|
84
|
-
},
|
|
85
|
-
"team": {
|
|
86
|
-
"key": "layout.nav.team",
|
|
87
|
-
"defaultValue": "Team"
|
|
88
|
-
}
|
|
89
|
-
}
|
|
81
|
+
export default defineServerConfig()
|
|
82
|
+
.lifecycle({
|
|
83
|
+
afterInfrastructure: async () => {
|
|
84
|
+
await syncLabels([homeLabels, aboutLabels]);
|
|
85
|
+
}
|
|
86
|
+
})
|
|
87
|
+
.build();
|
|
90
88
|
```
|
|
91
89
|
|
|
92
|
-
|
|
90
|
+
### 3. Use in Your App
|
|
93
91
|
|
|
94
|
-
|
|
95
|
-
{
|
|
96
|
-
"title": {
|
|
97
|
-
"key": "home.hero.title",
|
|
98
|
-
"defaultValue": {
|
|
99
|
-
"ko": "혁신적인 솔루션",
|
|
100
|
-
"en": "Innovative Solutions"
|
|
101
|
-
}
|
|
102
|
-
},
|
|
103
|
-
"subtitle": {
|
|
104
|
-
"key": "home.hero.subtitle",
|
|
105
|
-
"defaultValue": {
|
|
106
|
-
"ko": "비즈니스 성장을 위한 최고의 파트너",
|
|
107
|
-
"en": "Your Best Partner for Business Growth"
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
```
|
|
92
|
+
**Server Component (Single Section):**
|
|
112
93
|
|
|
113
|
-
|
|
94
|
+
```typescript
|
|
95
|
+
import { getLabel, format } from '@/labels';
|
|
114
96
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
97
|
+
export default async function HomePage() {
|
|
98
|
+
// Single section - direct access without section name
|
|
99
|
+
const label = await getLabel('home');
|
|
100
|
+
|
|
101
|
+
return (
|
|
102
|
+
<div>
|
|
103
|
+
<h1>{label.hero.title}</h1>
|
|
104
|
+
<p>{label.hero.subtitle}</p>
|
|
105
|
+
<button>{label.cta}</button>
|
|
106
|
+
|
|
107
|
+
{/* Template variables */}
|
|
108
|
+
<p>{format(label.hero.greeting, { name: 'John' })}</p>
|
|
109
|
+
{/* Output: "John님 안녕하세요!" */}
|
|
110
|
+
</div>
|
|
111
|
+
);
|
|
121
112
|
}
|
|
122
113
|
```
|
|
123
114
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
Configure `src/server/server.config.ts`:
|
|
115
|
+
**Server Component (Multiple Sections):**
|
|
127
116
|
|
|
128
117
|
```typescript
|
|
129
|
-
import
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
118
|
+
import { getLabels } from '@/labels';
|
|
119
|
+
|
|
120
|
+
export default async function MultiSectionPage() {
|
|
121
|
+
// Multiple sections - with section names as keys
|
|
122
|
+
const labels = await getLabels(['home', 'about']);
|
|
123
|
+
|
|
124
|
+
return (
|
|
125
|
+
<div>
|
|
126
|
+
<h1>{labels.home.hero.title}</h1>
|
|
127
|
+
<p>{labels.about.title}</p>
|
|
128
|
+
</div>
|
|
129
|
+
);
|
|
130
|
+
}
|
|
140
131
|
```
|
|
141
132
|
|
|
142
|
-
|
|
133
|
+
**Locale Management:**
|
|
143
134
|
|
|
144
|
-
|
|
135
|
+
```typescript
|
|
136
|
+
'use client';
|
|
137
|
+
import { setLocale } from '@spfn/cms/actions';
|
|
145
138
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
}
|
|
154
|
-
]
|
|
155
|
-
}
|
|
139
|
+
export function LanguageSwitcher() {
|
|
140
|
+
return (
|
|
141
|
+
<div>
|
|
142
|
+
<button onClick={() => setLocale('ko')}>한국어</button>
|
|
143
|
+
<button onClick={() => setLocale('en')}>English</button>
|
|
144
|
+
</div>
|
|
145
|
+
);
|
|
156
146
|
}
|
|
157
147
|
```
|
|
158
148
|
|
|
159
|
-
|
|
149
|
+
## Key Features
|
|
160
150
|
|
|
161
|
-
###
|
|
151
|
+
### 🎯 Type Safety
|
|
162
152
|
|
|
163
|
-
**
|
|
153
|
+
**Section Name Validation:**
|
|
164
154
|
|
|
165
155
|
```typescript
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
const { t } = await getSection('layout', 'ko');
|
|
170
|
-
|
|
171
|
-
return <h1>{t('nav.team')}</h1>;
|
|
172
|
-
}
|
|
173
|
-
```
|
|
156
|
+
// ✅ Valid section names are enforced at compile time
|
|
157
|
+
const label = await getLabel('home'); // OK
|
|
158
|
+
await getLabel('homee'); // ❌ Compile error: 'homee' is not a valid section
|
|
174
159
|
|
|
175
|
-
|
|
160
|
+
// ✅ getLabel returns direct access (no section wrapper)
|
|
161
|
+
const homeLabel = await getLabel('home');
|
|
162
|
+
homeLabel.hero.title; // OK - direct access
|
|
163
|
+
homeLabel.cta; // OK
|
|
176
164
|
|
|
177
|
-
|
|
178
|
-
const
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
// → "© 2025 Company. All rights reserved."
|
|
165
|
+
// ✅ getLabels requires array and returns sections with names
|
|
166
|
+
const labels = await getLabels(['home', 'about']);
|
|
167
|
+
labels.home.hero.title; // OK
|
|
168
|
+
labels.about.title; // OK
|
|
169
|
+
labels.contact.email; // ❌ Compile error: 'contact' was not requested
|
|
183
170
|
```
|
|
184
171
|
|
|
185
|
-
**
|
|
172
|
+
**Property Access Validation:**
|
|
186
173
|
|
|
187
174
|
```typescript
|
|
188
|
-
|
|
189
|
-
|
|
175
|
+
// Single section - direct access
|
|
176
|
+
const label = await getLabel('signup');
|
|
190
177
|
|
|
191
|
-
|
|
192
|
-
|
|
178
|
+
// ✅ IDE autocomplete works perfectly
|
|
179
|
+
label.title; // OK - direct access
|
|
180
|
+
label.userName; // OK
|
|
181
|
+
label.email; // OK
|
|
193
182
|
|
|
194
|
-
|
|
183
|
+
// ❌ Typos are caught at compile time
|
|
184
|
+
label.titlee; // ❌ Compile error
|
|
185
|
+
label.userName; // ❌ Compile error (typo)
|
|
195
186
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
</nav>
|
|
201
|
-
);
|
|
202
|
-
}
|
|
187
|
+
// Multiple sections - with section names
|
|
188
|
+
const labels = await getLabels(['home', 'about']);
|
|
189
|
+
labels.home.hero.title; // OK
|
|
190
|
+
labels.about.title; // OK
|
|
203
191
|
```
|
|
204
192
|
|
|
205
|
-
|
|
193
|
+
**API Distinction:**
|
|
206
194
|
|
|
207
|
-
```
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
footer.json # Category: footer
|
|
212
|
-
home/ # Section: home
|
|
213
|
-
hero.json # Category: hero
|
|
214
|
-
features.json # Category: features
|
|
215
|
-
```
|
|
216
|
-
|
|
217
|
-
**How it maps:**
|
|
218
|
-
- Folder name = Section name
|
|
219
|
-
- JSON file name = Category name (for organization only)
|
|
220
|
-
- Inside JSON: `key` field defines the actual label key
|
|
195
|
+
```typescript
|
|
196
|
+
// getLabel: Single section, direct access
|
|
197
|
+
const signup = await getLabel('signup');
|
|
198
|
+
signup.title; // ✅ Direct access (cleaner!)
|
|
221
199
|
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
200
|
+
// getLabels: Multiple sections, section names as keys
|
|
201
|
+
const multi = await getLabels(['home', 'signup']);
|
|
202
|
+
multi.home.title; // ✅ With section name
|
|
203
|
+
multi.signup.userName; // ✅ With section name
|
|
226
204
|
```
|
|
227
205
|
|
|
228
|
-
|
|
206
|
+
### 🎨 Nested Structure
|
|
229
207
|
|
|
230
208
|
```typescript
|
|
231
|
-
{
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
209
|
+
defineLabels({
|
|
210
|
+
features: {
|
|
211
|
+
analytics: {
|
|
212
|
+
title: { en: "Analytics", ko: "분석" },
|
|
213
|
+
description: { en: "Track metrics", ko: "지표 추적" }
|
|
214
|
+
},
|
|
215
|
+
security: {
|
|
216
|
+
title: { en: "Security", ko: "보안" }
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
});
|
|
239
220
|
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
"defaultValue": "Welcome"
|
|
246
|
-
}
|
|
247
|
-
}
|
|
221
|
+
// Single section - direct access with nesting
|
|
222
|
+
const label = await getLabel('features');
|
|
223
|
+
label.analytics.title; // "분석" (auto locale, direct access!)
|
|
224
|
+
label.analytics.description; // "지표 추적"
|
|
225
|
+
label.security.title; // "보안"
|
|
248
226
|
```
|
|
249
227
|
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
228
|
+
### 🔧 Template Variables
|
|
229
|
+
|
|
230
|
+
```typescript
|
|
231
|
+
defineLabels({
|
|
232
|
+
home: {
|
|
233
|
+
welcome: {
|
|
234
|
+
en: "Welcome {name}, you have {count} messages",
|
|
235
|
+
ko: "{name}님, {count}개의 메시지가 있습니다"
|
|
236
|
+
}
|
|
259
237
|
}
|
|
260
|
-
|
|
261
|
-
}
|
|
262
|
-
```
|
|
238
|
+
});
|
|
263
239
|
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
{
|
|
267
|
-
"greeting": {
|
|
268
|
-
"key": "home.greeting",
|
|
269
|
-
"defaultValue": "Hello, {name}!"
|
|
270
|
-
}
|
|
271
|
-
}
|
|
272
|
-
```
|
|
240
|
+
const label = await getLabel('home');
|
|
241
|
+
const text = label.welcome; // Direct access!
|
|
273
242
|
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
t('greeting', undefined, { name: 'John' })
|
|
277
|
-
// → "Hello, John!"
|
|
243
|
+
format(text, { name: "John", count: 5 });
|
|
244
|
+
// Output: "John님, 5개의 메시지가 있습니다"
|
|
278
245
|
```
|
|
279
246
|
|
|
280
|
-
|
|
247
|
+
### 🍪 Smart Locale Detection
|
|
281
248
|
|
|
282
|
-
|
|
249
|
+
Automatic locale detection with priority:
|
|
250
|
+
1. User's cookie (`cms-locale`)
|
|
251
|
+
2. Config's `defaultLocale`
|
|
252
|
+
3. Final fallback: `'en'`
|
|
283
253
|
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
# Default locale (default: 'en')
|
|
288
|
-
SPFN_CMS_DEFAULT_LOCALE=ko
|
|
254
|
+
```typescript
|
|
255
|
+
// User sets locale (saved to cookie)
|
|
256
|
+
await setLocale('ko');
|
|
289
257
|
|
|
290
|
-
|
|
291
|
-
|
|
258
|
+
// Automatically uses 'ko' locale
|
|
259
|
+
const label = await getLabel('home');
|
|
260
|
+
label.hero.title; // "환영합니다" (Korean)
|
|
292
261
|
|
|
293
|
-
|
|
294
|
-
|
|
262
|
+
// Switch to English
|
|
263
|
+
await setLocale('en');
|
|
264
|
+
const label2 = await getLabel('home');
|
|
265
|
+
label2.hero.title; // "Welcome" (English)
|
|
295
266
|
```
|
|
296
267
|
|
|
297
|
-
###
|
|
268
|
+
### 🔄 Auto-Sync
|
|
298
269
|
|
|
299
|
-
|
|
270
|
+
Labels synchronize automatically on server startup:
|
|
271
|
+
- ✅ Creates new labels
|
|
272
|
+
- ✅ Updates changed labels (deep equality check)
|
|
273
|
+
- ✅ Skips unchanged labels (performance)
|
|
274
|
+
- ✅ Rebuilds published cache
|
|
275
|
+
- ⚠️ Optionally removes orphaned labels
|
|
300
276
|
|
|
301
|
-
|
|
302
|
-
import { configureCms, getCmsConfig } from '@spfn/cms';
|
|
303
|
-
|
|
304
|
-
// Get current configuration
|
|
305
|
-
const config = getCmsConfig();
|
|
306
|
-
console.log(config.defaultLocale); // 'ko'
|
|
307
|
-
console.log(config.supportedLocales); // ['ko', 'en']
|
|
308
|
-
|
|
309
|
-
// Update configuration
|
|
310
|
-
configureCms({
|
|
311
|
-
defaultLocale: 'en',
|
|
312
|
-
supportedLocales: ['en', 'ko', 'ja'],
|
|
313
|
-
detectBrowserLanguage: false
|
|
314
|
-
});
|
|
315
|
-
```
|
|
316
|
-
|
|
317
|
-
## Locale Management
|
|
277
|
+
## API Reference
|
|
318
278
|
|
|
319
|
-
###
|
|
279
|
+
### createCmsClient()
|
|
320
280
|
|
|
321
|
-
|
|
281
|
+
Factory function to create CMS client with API, getLabel, getLabels, and format utilities.
|
|
322
282
|
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
283
|
+
```typescript
|
|
284
|
+
const { api, getLabel, getLabels, format } = createCmsClient(labelsDefinition, labelConfig);
|
|
285
|
+
```
|
|
326
286
|
|
|
327
|
-
|
|
287
|
+
**Returns:**
|
|
288
|
+
- `api` - API client for CMS routes
|
|
289
|
+
- `getLabel(section)` - Fetch a single section (direct access)
|
|
290
|
+
- `getLabels(sections)` - Fetch multiple sections (with section names)
|
|
291
|
+
- `format(template, vars)` - Template variable substitution
|
|
328
292
|
|
|
329
|
-
|
|
293
|
+
### getLabel()
|
|
330
294
|
|
|
331
|
-
**
|
|
295
|
+
Fetch a **single section** from published cache with direct access (no section name wrapper).
|
|
332
296
|
|
|
333
297
|
```typescript
|
|
334
|
-
//
|
|
335
|
-
|
|
298
|
+
// Single section - direct access
|
|
299
|
+
const label = await getLabel('home');
|
|
300
|
+
label.hero.title; // Direct access without 'home.' prefix
|
|
301
|
+
label.cta;
|
|
302
|
+
```
|
|
336
303
|
|
|
337
|
-
|
|
338
|
-
|
|
304
|
+
**Use when:**
|
|
305
|
+
- You need labels from only ONE section
|
|
306
|
+
- You want cleaner, direct access to properties
|
|
307
|
+
- Most common use case for individual pages
|
|
339
308
|
|
|
340
|
-
|
|
341
|
-
}
|
|
342
|
-
```
|
|
309
|
+
**Returns:** Labels directly without section name wrapper
|
|
343
310
|
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
311
|
+
**Features:**
|
|
312
|
+
- Auto locale detection (cookie → defaultLocale → 'en')
|
|
313
|
+
- Direct property access (no section name)
|
|
314
|
+
- Type-safe with IDE autocomplete
|
|
315
|
+
- Merges published cache with defaults
|
|
349
316
|
|
|
350
|
-
|
|
351
|
-
const [locale, setLocale] = useState('');
|
|
317
|
+
### getLabels()
|
|
352
318
|
|
|
353
|
-
|
|
354
|
-
getLocale().then(setLocale);
|
|
355
|
-
}, []);
|
|
319
|
+
Fetch **multiple sections** from published cache with section names as keys.
|
|
356
320
|
|
|
357
|
-
|
|
358
|
-
|
|
321
|
+
```typescript
|
|
322
|
+
// Multiple sections - with section names
|
|
323
|
+
const labels = await getLabels(['home', 'about']);
|
|
324
|
+
labels.home.hero.title; // Access via section name
|
|
325
|
+
labels.about.title;
|
|
359
326
|
```
|
|
360
327
|
|
|
361
|
-
**
|
|
328
|
+
**Use when:**
|
|
329
|
+
- You need labels from MULTIPLE sections
|
|
330
|
+
- You're building a page that uses multiple label groups
|
|
362
331
|
|
|
363
|
-
|
|
364
|
-
import { setLocale } from '@spfn/cms/actions';
|
|
332
|
+
**Returns:** Object with section names as keys
|
|
365
333
|
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
334
|
+
**Features:**
|
|
335
|
+
- Auto locale detection (cookie → defaultLocale → 'en')
|
|
336
|
+
- Section filtering (only processes requested sections)
|
|
337
|
+
- Type-safe: only requested sections available
|
|
338
|
+
- Merges published cache with defaults
|
|
371
339
|
|
|
372
|
-
**
|
|
340
|
+
**Performance:**
|
|
341
|
+
- Only requested sections are processed (not entire labelsDefinition)
|
|
342
|
+
- 10x faster when requesting specific sections
|
|
343
|
+
- Reduces CPU and memory usage proportionally
|
|
373
344
|
|
|
374
|
-
|
|
375
|
-
|
|
345
|
+
### format()
|
|
346
|
+
|
|
347
|
+
Replace template variables in strings.
|
|
376
348
|
|
|
377
|
-
|
|
349
|
+
```typescript
|
|
350
|
+
format("Hello {name}!", { name: "John" }); // "Hello John!"
|
|
351
|
+
format("{count} items", { count: 5 }); // "5 items"
|
|
378
352
|
```
|
|
379
353
|
|
|
380
|
-
|
|
354
|
+
**Syntax:** `{variableName}` - Supports strings and numbers
|
|
381
355
|
|
|
382
|
-
|
|
356
|
+
### setLocale() / getLocale()
|
|
383
357
|
|
|
384
|
-
|
|
385
|
-
import { getSection } from '@spfn/cms/server';
|
|
358
|
+
Server actions for locale management (cookie-based).
|
|
386
359
|
|
|
387
|
-
|
|
388
|
-
|
|
360
|
+
```typescript
|
|
361
|
+
// Set user's preferred locale
|
|
362
|
+
await setLocale('ko'); // Saves to 'cms-locale' cookie
|
|
389
363
|
|
|
390
|
-
//
|
|
391
|
-
const
|
|
364
|
+
// Get current locale
|
|
365
|
+
const locale = await getLocale(defaultLocale); // Returns: cookie → defaultLocale → 'en'
|
|
392
366
|
```
|
|
393
367
|
|
|
394
|
-
|
|
368
|
+
**Cookie settings:**
|
|
369
|
+
- Name: `cms-locale`
|
|
370
|
+
- Max age: 1 year
|
|
371
|
+
- HttpOnly, Secure (production), SameSite: lax
|
|
395
372
|
|
|
396
|
-
|
|
397
|
-
- **[Examples](./examples/)** - Usage examples
|
|
373
|
+
### syncLabels()
|
|
398
374
|
|
|
399
|
-
|
|
375
|
+
Synchronize labels to database (server-side only).
|
|
400
376
|
|
|
377
|
+
```typescript
|
|
378
|
+
await syncLabels(labelsDefinition, {
|
|
379
|
+
removeOrphaned: false, // Delete labels not in code
|
|
380
|
+
dryRun: false // Preview changes without applying
|
|
381
|
+
});
|
|
401
382
|
```
|
|
402
|
-
JSON Files (src/cms/labels/**/*.json)
|
|
403
|
-
↓
|
|
404
|
-
loadLabelsFromJson()
|
|
405
|
-
↓
|
|
406
|
-
┌─────────────────────┐
|
|
407
|
-
│ LabelSyncGenerator │ ← File watcher (development)
|
|
408
|
-
│ initLabelSync() │ ← Server startup
|
|
409
|
-
└─────────────────────┘
|
|
410
|
-
↓
|
|
411
|
-
syncAll()
|
|
412
|
-
↓
|
|
413
|
-
┌─────────────────────┐
|
|
414
|
-
│ PostgreSQL DB │
|
|
415
|
-
│ - cms_labels │
|
|
416
|
-
│ - published_cache │ ⭐ Used by API
|
|
417
|
-
└─────────────────────┘
|
|
418
|
-
↓ HTTP API
|
|
419
|
-
┌─────────────────────┐
|
|
420
|
-
│ Application │
|
|
421
|
-
│ - getSection() │
|
|
422
|
-
│ - useSection() │
|
|
423
|
-
└─────────────────────┘
|
|
424
|
-
```
|
|
425
|
-
|
|
426
|
-
## API Reference
|
|
427
383
|
|
|
428
|
-
|
|
384
|
+
**Returns:** `{ added, updated, removed, unchanged }`
|
|
429
385
|
|
|
430
|
-
|
|
431
|
-
- `getSections(sections, locale?)` - Get multiple sections (auto-detects locale if not specified)
|
|
432
|
-
- `initLabelSync(options?)` - Sync labels on server startup
|
|
433
|
-
|
|
434
|
-
### Server Actions API (`@spfn/cms/actions`)
|
|
386
|
+
## Architecture
|
|
435
387
|
|
|
436
|
-
|
|
388
|
+
### Database Schema
|
|
437
389
|
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
390
|
+
```
|
|
391
|
+
cms_labels (metadata)
|
|
392
|
+
├─ id, key, section, type, defaultValue
|
|
393
|
+
└─ publishedVersion
|
|
442
394
|
|
|
443
|
-
|
|
395
|
+
cms_label_values (actual content)
|
|
396
|
+
├─ labelId, version, locale, breakpoint
|
|
397
|
+
└─ value (JSONB)
|
|
444
398
|
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
399
|
+
cms_published_cache (performance)
|
|
400
|
+
├─ section, locale, content (JSONB)
|
|
401
|
+
└─ version (for cache invalidation)
|
|
448
402
|
|
|
449
|
-
|
|
403
|
+
cms_audit_logs (tracking)
|
|
404
|
+
└─ action, userId, changes, metadata
|
|
405
|
+
```
|
|
450
406
|
|
|
451
|
-
|
|
452
|
-
- `useSections(sections)` - Multiple sections hook
|
|
453
|
-
- `useCmsStore()` - CMS store hook
|
|
454
|
-
- `cmsApi` - CMS API client
|
|
455
|
-
- `InitCms` - Client initialization component
|
|
407
|
+
### Query Flow
|
|
456
408
|
|
|
457
|
-
|
|
409
|
+
1. **getLabels()** → published_cache (single query, 5ms)
|
|
410
|
+
2. **Fallback** → bindLocale(defaults)
|
|
411
|
+
3. **Merge** → cache overrides defaults
|
|
412
|
+
4. **Return** → type-safe nested object
|
|
458
413
|
|
|
459
|
-
|
|
460
|
-
- `syncAll(sections, options?)` - Sync all sections
|
|
461
|
-
- `syncSection(definition, options?)` - Sync specific section
|
|
414
|
+
## Performance
|
|
462
415
|
|
|
463
|
-
|
|
416
|
+
- **Published cache:** 5ms (vs 87ms with JOINs) - 17x faster
|
|
417
|
+
- **N+1 prevention:** Bulk section queries with `inArray()`
|
|
418
|
+
- **Section filtering:** Only requested sections processed (10x faster for selective access)
|
|
419
|
+
- **Unchanged labels:** Skipped during sync (deep equality check)
|
|
420
|
+
- **Client caching:** Version-based invalidation
|
|
464
421
|
|
|
465
|
-
|
|
466
|
-
- `LabelSyncGenerator` - Generator class
|
|
422
|
+
### Example: Large Scale
|
|
467
423
|
|
|
468
|
-
|
|
424
|
+
```typescript
|
|
425
|
+
// 10 sections with 100 labels each = 1,000 total labels
|
|
426
|
+
const labelsDefinition = {
|
|
427
|
+
home: { /* 100 labels */ },
|
|
428
|
+
about: { /* 100 labels */ },
|
|
429
|
+
products: { /* 100 labels */ },
|
|
430
|
+
// ... 7 more sections
|
|
431
|
+
};
|
|
469
432
|
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
3. **Labels immediately available** via `getSection()` or `useSection()`
|
|
433
|
+
// Only request 'home' - direct access
|
|
434
|
+
const label = await getLabel('home');
|
|
473
435
|
|
|
474
|
-
|
|
436
|
+
// ✅ Performance: Processes only 100 labels (10% of total)
|
|
437
|
+
// ❌ Without filtering: Would process all 1,000 labels
|
|
438
|
+
```
|
|
475
439
|
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
440
|
+
**Benefits:**
|
|
441
|
+
- CPU usage: 10x reduction (100 vs 1,000 labels)
|
|
442
|
+
- Memory usage: 10x reduction
|
|
443
|
+
- Response time: Proportionally faster
|
|
479
444
|
|
|
480
|
-
|
|
481
|
-
echo '{"test": {"key": "layout.test", "defaultValue": "Test"}}' > src/cms/labels/layout/test.json
|
|
445
|
+
## Development Status
|
|
482
446
|
|
|
483
|
-
|
|
484
|
-
# ✅ Label sync completed
|
|
485
|
-
# Created: 1
|
|
486
|
-
```
|
|
447
|
+
This package is currently in alpha. APIs may change.
|
|
487
448
|
|
|
488
449
|
## License
|
|
489
450
|
|
|
490
|
-
MIT
|
|
451
|
+
MIT
|