@spfn/cms 0.1.0-alpha.0
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/LICENSE +21 -0
- package/README.md +490 -0
- package/dist/actions.d.ts +9 -0
- package/dist/actions.d.ts.map +1 -0
- package/dist/actions.js +11 -0
- package/dist/actions.js.map +1 -0
- package/dist/client.d.ts +138 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +62 -0
- package/dist/client.js.map +1 -0
- package/dist/cms.config.d.ts +77 -0
- package/dist/cms.config.d.ts.map +1 -0
- package/dist/cms.config.js +111 -0
- package/dist/cms.config.js.map +1 -0
- package/dist/entities/cms-audit-logs.d.ts +213 -0
- package/dist/entities/cms-audit-logs.d.ts.map +1 -0
- package/dist/entities/cms-audit-logs.js +103 -0
- package/dist/entities/cms-audit-logs.js.map +1 -0
- package/dist/entities/cms-draft-cache.d.ts +188 -0
- package/dist/entities/cms-draft-cache.d.ts.map +1 -0
- package/dist/entities/cms-draft-cache.js +112 -0
- package/dist/entities/cms-draft-cache.js.map +1 -0
- package/dist/entities/cms-label-values.d.ts +192 -0
- package/dist/entities/cms-label-values.d.ts.map +1 -0
- package/dist/entities/cms-label-values.js +105 -0
- package/dist/entities/cms-label-values.js.map +1 -0
- package/dist/entities/cms-label-versions.d.ts +207 -0
- package/dist/entities/cms-label-versions.d.ts.map +1 -0
- package/dist/entities/cms-label-versions.js +80 -0
- package/dist/entities/cms-label-versions.js.map +1 -0
- package/dist/entities/cms-labels.d.ts +189 -0
- package/dist/entities/cms-labels.d.ts.map +1 -0
- package/dist/entities/cms-labels.js +48 -0
- package/dist/entities/cms-labels.js.map +1 -0
- package/dist/entities/cms-published-cache.d.ts +199 -0
- package/dist/entities/cms-published-cache.d.ts.map +1 -0
- package/dist/entities/cms-published-cache.js +103 -0
- package/dist/entities/cms-published-cache.js.map +1 -0
- package/dist/entities/index.d.ts +10 -0
- package/dist/entities/index.d.ts.map +1 -0
- package/dist/entities/index.js +10 -0
- package/dist/entities/index.js.map +1 -0
- package/dist/generators/index.d.ts +19 -0
- package/dist/generators/index.d.ts.map +1 -0
- package/dist/generators/index.js +19 -0
- package/dist/generators/index.js.map +1 -0
- package/dist/generators/label-sync-generator.d.ts +33 -0
- package/dist/generators/label-sync-generator.d.ts.map +1 -0
- package/dist/generators/label-sync-generator.js +86 -0
- package/dist/generators/label-sync-generator.js.map +1 -0
- package/dist/helpers/locale.actions.d.ts +132 -0
- package/dist/helpers/locale.actions.d.ts.map +1 -0
- package/dist/helpers/locale.actions.js +210 -0
- package/dist/helpers/locale.actions.js.map +1 -0
- package/dist/helpers/locale.constants.d.ts +10 -0
- package/dist/helpers/locale.constants.d.ts.map +1 -0
- package/dist/helpers/locale.constants.js +10 -0
- package/dist/helpers/locale.constants.js.map +1 -0
- package/dist/helpers/locale.d.ts +17 -0
- package/dist/helpers/locale.d.ts.map +1 -0
- package/dist/helpers/locale.js +20 -0
- package/dist/helpers/locale.js.map +1 -0
- package/dist/helpers/sync.d.ts +41 -0
- package/dist/helpers/sync.d.ts.map +1 -0
- package/dist/helpers/sync.js +309 -0
- package/dist/helpers/sync.js.map +1 -0
- package/dist/index.d.ts +20 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +24 -0
- package/dist/index.js.map +1 -0
- package/dist/init.d.ts +31 -0
- package/dist/init.d.ts.map +1 -0
- package/dist/init.js +36 -0
- package/dist/init.js.map +1 -0
- package/dist/labels/helpers.d.ts +31 -0
- package/dist/labels/helpers.d.ts.map +1 -0
- package/dist/labels/helpers.js +60 -0
- package/dist/labels/helpers.js.map +1 -0
- package/dist/labels/index.d.ts +7 -0
- package/dist/labels/index.d.ts.map +1 -0
- package/dist/labels/index.js +7 -0
- package/dist/labels/index.js.map +1 -0
- package/dist/repositories/cms-draft-cache.repository.d.ts +62 -0
- package/dist/repositories/cms-draft-cache.repository.d.ts.map +1 -0
- package/dist/repositories/cms-draft-cache.repository.js +56 -0
- package/dist/repositories/cms-draft-cache.repository.js.map +1 -0
- package/dist/repositories/cms-label-values.repository.d.ts +32 -0
- package/dist/repositories/cms-label-values.repository.d.ts.map +1 -0
- package/dist/repositories/cms-label-values.repository.js +72 -0
- package/dist/repositories/cms-label-values.repository.js.map +1 -0
- package/dist/repositories/cms-labels.repository.d.ts +53 -0
- package/dist/repositories/cms-labels.repository.d.ts.map +1 -0
- package/dist/repositories/cms-labels.repository.js +77 -0
- package/dist/repositories/cms-labels.repository.js.map +1 -0
- package/dist/repositories/cms-published-cache.repository.d.ts +53 -0
- package/dist/repositories/cms-published-cache.repository.d.ts.map +1 -0
- package/dist/repositories/cms-published-cache.repository.js +54 -0
- package/dist/repositories/cms-published-cache.repository.js.map +1 -0
- package/dist/repositories/index.d.ts +8 -0
- package/dist/repositories/index.d.ts.map +1 -0
- package/dist/repositories/index.js +9 -0
- package/dist/repositories/index.js.map +1 -0
- package/dist/routes/labels/[id]/contract.d.ts +68 -0
- package/dist/routes/labels/[id]/contract.d.ts.map +1 -0
- package/dist/routes/labels/[id]/contract.js +84 -0
- package/dist/routes/labels/[id]/contract.js.map +1 -0
- package/dist/routes/labels/[id]/index.d.ts +10 -0
- package/dist/routes/labels/[id]/index.d.ts.map +1 -0
- package/dist/routes/labels/[id]/index.js +96 -0
- package/dist/routes/labels/[id]/index.js.map +1 -0
- package/dist/routes/labels/by-key/[key]/contract.d.ts +24 -0
- package/dist/routes/labels/by-key/[key]/contract.d.ts.map +1 -0
- package/dist/routes/labels/by-key/[key]/contract.js +28 -0
- package/dist/routes/labels/by-key/[key]/contract.js.map +1 -0
- package/dist/routes/labels/by-key/[key]/index.d.ts +8 -0
- package/dist/routes/labels/by-key/[key]/index.d.ts.map +1 -0
- package/dist/routes/labels/by-key/[key]/index.js +32 -0
- package/dist/routes/labels/by-key/[key]/index.js.map +1 -0
- package/dist/routes/labels/contract.d.ts +59 -0
- package/dist/routes/labels/contract.d.ts.map +1 -0
- package/dist/routes/labels/contract.js +75 -0
- package/dist/routes/labels/contract.js.map +1 -0
- package/dist/routes/labels/index.d.ts +10 -0
- package/dist/routes/labels/index.d.ts.map +1 -0
- package/dist/routes/labels/index.js +73 -0
- package/dist/routes/labels/index.js.map +1 -0
- package/dist/routes/published-cache/contract.d.ts +25 -0
- package/dist/routes/published-cache/contract.d.ts.map +1 -0
- package/dist/routes/published-cache/contract.js +35 -0
- package/dist/routes/published-cache/contract.js.map +1 -0
- package/dist/routes/published-cache/index.d.ts +8 -0
- package/dist/routes/published-cache/index.d.ts.map +1 -0
- package/dist/routes/published-cache/index.js +33 -0
- package/dist/routes/published-cache/index.js.map +1 -0
- package/dist/routes/sync/contract.d.ts +33 -0
- package/dist/routes/sync/contract.d.ts.map +1 -0
- package/dist/routes/sync/contract.js +34 -0
- package/dist/routes/sync/contract.js.map +1 -0
- package/dist/routes/sync/index.d.ts +13 -0
- package/dist/routes/sync/index.d.ts.map +1 -0
- package/dist/routes/sync/index.js +241 -0
- package/dist/routes/sync/index.js.map +1 -0
- package/dist/routes/values/[labelId]/[version]/contract.d.ts +29 -0
- package/dist/routes/values/[labelId]/[version]/contract.d.ts.map +1 -0
- package/dist/routes/values/[labelId]/[version]/contract.js +33 -0
- package/dist/routes/values/[labelId]/[version]/contract.js.map +1 -0
- package/dist/routes/values/[labelId]/[version]/index.d.ts +8 -0
- package/dist/routes/values/[labelId]/[version]/index.d.ts.map +1 -0
- package/dist/routes/values/[labelId]/[version]/index.js +45 -0
- package/dist/routes/values/[labelId]/[version]/index.js.map +1 -0
- package/dist/routes/values/[labelId]/contract.d.ts +38 -0
- package/dist/routes/values/[labelId]/contract.d.ts.map +1 -0
- package/dist/routes/values/[labelId]/contract.js +59 -0
- package/dist/routes/values/[labelId]/contract.js.map +1 -0
- package/dist/routes/values/[labelId]/index.d.ts +8 -0
- package/dist/routes/values/[labelId]/index.d.ts.map +1 -0
- package/dist/routes/values/[labelId]/index.js +42 -0
- package/dist/routes/values/[labelId]/index.js.map +1 -0
- package/dist/server.d.ts +99 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +256 -0
- package/dist/server.js.map +1 -0
- package/dist/store.d.ts +87 -0
- package/dist/store.d.ts.map +1 -0
- package/dist/store.js +205 -0
- package/dist/store.js.map +1 -0
- package/dist/sync.d.ts +11 -0
- package/dist/sync.d.ts.map +1 -0
- package/dist/sync.js +179 -0
- package/dist/sync.js.map +1 -0
- package/dist/types.d.ts +74 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +7 -0
- package/dist/types.js.map +1 -0
- package/package.json +95 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 INFLIKE Inc.
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,490 @@
|
|
|
1
|
+
# @spfn/cms
|
|
2
|
+
|
|
3
|
+
Content Management System for Next.js with JSON-based labels and automatic database synchronization.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- 📁 **JSON file-based labels** - Simple file structure for label management
|
|
8
|
+
- 🔄 **Auto-sync to database** on server startup and during development
|
|
9
|
+
- 🌐 **Multi-language support** (i18n)
|
|
10
|
+
- 🍪 **Cookie-based locale management** - Automatic locale detection and persistence
|
|
11
|
+
- 📦 **Folder-based structure** for better organization
|
|
12
|
+
- 🔥 **Hot reload** during development
|
|
13
|
+
- 💾 **Published cache** for optimal performance
|
|
14
|
+
- ⚡ **Server Actions** for client-side locale management
|
|
15
|
+
- 🛠️ **Built on Drizzle ORM**
|
|
16
|
+
|
|
17
|
+
## Installation
|
|
18
|
+
|
|
19
|
+
### Recommended: Using SPFN CLI (Automatic Database Setup)
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
pnpm spfn add @spfn/cms
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
This command will:
|
|
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)
|
|
39
|
+
|
|
40
|
+
### Manual Installation
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
pnpm add @spfn/cms
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Then run database migrations:
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
pnpm spfn db generate # Generate migrations
|
|
50
|
+
pnpm spfn db migrate # Apply migrations
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
**Note:** Manual installation requires that you have `DATABASE_URL` configured in your `.env.local` file.
|
|
54
|
+
|
|
55
|
+
## Quick Start
|
|
56
|
+
|
|
57
|
+
### 1. Create Label Files
|
|
58
|
+
|
|
59
|
+
Create JSON files organized by sections and categories:
|
|
60
|
+
|
|
61
|
+
```
|
|
62
|
+
src/cms/labels/
|
|
63
|
+
layout/ ← Section name
|
|
64
|
+
nav.json ← Category
|
|
65
|
+
footer.json
|
|
66
|
+
home/
|
|
67
|
+
hero.json
|
|
68
|
+
features.json
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
**Example:** `src/cms/labels/layout/nav.json`
|
|
72
|
+
|
|
73
|
+
```json
|
|
74
|
+
{
|
|
75
|
+
"about": {
|
|
76
|
+
"key": "layout.nav.about",
|
|
77
|
+
"defaultValue": "About",
|
|
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
|
+
}
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
**Multi-language example:** `src/cms/labels/home/hero.json`
|
|
93
|
+
|
|
94
|
+
```json
|
|
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
|
+
```
|
|
112
|
+
|
|
113
|
+
**Variable substitution:** `src/cms/labels/layout/footer.json`
|
|
114
|
+
|
|
115
|
+
```json
|
|
116
|
+
{
|
|
117
|
+
"copyright": {
|
|
118
|
+
"key": "layout.footer.copyright",
|
|
119
|
+
"defaultValue": "© {year} Company. All rights reserved."
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
### 2. Enable Auto-Sync on Server Startup
|
|
125
|
+
|
|
126
|
+
Configure `src/server/server.config.ts`:
|
|
127
|
+
|
|
128
|
+
```typescript
|
|
129
|
+
import type { ServerConfig } from '@spfn/core/server';
|
|
130
|
+
import { initLabelSync } from '@spfn/cms';
|
|
131
|
+
|
|
132
|
+
export default {
|
|
133
|
+
beforeRoutes: async (app) => {
|
|
134
|
+
await initLabelSync({
|
|
135
|
+
verbose: true,
|
|
136
|
+
labelsDir: 'src/cms/labels' // Optional, this is the default
|
|
137
|
+
});
|
|
138
|
+
},
|
|
139
|
+
} satisfies ServerConfig;
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
### 3. Enable Auto-Sync During Development
|
|
143
|
+
|
|
144
|
+
Your `.spfnrc.json` should include:
|
|
145
|
+
|
|
146
|
+
```json
|
|
147
|
+
{
|
|
148
|
+
"codegen": {
|
|
149
|
+
"generators": [
|
|
150
|
+
{
|
|
151
|
+
"name": "@spfn/cms:label-sync",
|
|
152
|
+
"enabled": true
|
|
153
|
+
}
|
|
154
|
+
]
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
This is automatically configured when you run `pnpm spfn add @spfn/cms`.
|
|
160
|
+
|
|
161
|
+
### 4. Use Labels in Your App
|
|
162
|
+
|
|
163
|
+
**Server Component:**
|
|
164
|
+
|
|
165
|
+
```typescript
|
|
166
|
+
import { getSection } from '@spfn/cms/server';
|
|
167
|
+
|
|
168
|
+
export default async function HomePage() {
|
|
169
|
+
const { t } = await getSection('layout', 'ko');
|
|
170
|
+
|
|
171
|
+
return <h1>{t('nav.team')}</h1>;
|
|
172
|
+
}
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
**With variable substitution:**
|
|
176
|
+
|
|
177
|
+
```typescript
|
|
178
|
+
const { t } = await getSection('layout');
|
|
179
|
+
const copyright = t('footer.copyright', undefined, {
|
|
180
|
+
year: new Date().getFullYear()
|
|
181
|
+
});
|
|
182
|
+
// → "© 2025 Company. All rights reserved."
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
**Client Component:**
|
|
186
|
+
|
|
187
|
+
```typescript
|
|
188
|
+
'use client';
|
|
189
|
+
import { useSection } from '@spfn/cms/client';
|
|
190
|
+
|
|
191
|
+
export default function Nav() {
|
|
192
|
+
const { t, loading } = useSection('layout', { autoLoad: true });
|
|
193
|
+
|
|
194
|
+
if (loading) return <div>Loading...</div>;
|
|
195
|
+
|
|
196
|
+
return (
|
|
197
|
+
<nav>
|
|
198
|
+
<a>{t('nav.about')}</a>
|
|
199
|
+
<a>{t('nav.services')}</a>
|
|
200
|
+
</nav>
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
## File Structure
|
|
206
|
+
|
|
207
|
+
```
|
|
208
|
+
src/cms/labels/
|
|
209
|
+
layout/ # Section: layout
|
|
210
|
+
nav.json # Category: nav
|
|
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
|
|
221
|
+
|
|
222
|
+
Example:
|
|
223
|
+
```
|
|
224
|
+
src/cms/labels/layout/nav.json:
|
|
225
|
+
key: "layout.nav.team" → t('nav.team') in code
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
## JSON Label Format
|
|
229
|
+
|
|
230
|
+
```typescript
|
|
231
|
+
{
|
|
232
|
+
"labelName": {
|
|
233
|
+
"key": "section.category.name", // Required: Unique identifier
|
|
234
|
+
"defaultValue": "Text" | {...}, // Required: String or i18n object
|
|
235
|
+
"description": "Optional description" // Optional: For documentation
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
**Single language:**
|
|
241
|
+
```json
|
|
242
|
+
{
|
|
243
|
+
"welcome": {
|
|
244
|
+
"key": "home.welcome",
|
|
245
|
+
"defaultValue": "Welcome"
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
**Multi-language:**
|
|
251
|
+
```json
|
|
252
|
+
{
|
|
253
|
+
"welcome": {
|
|
254
|
+
"key": "home.welcome",
|
|
255
|
+
"defaultValue": {
|
|
256
|
+
"ko": "환영합니다",
|
|
257
|
+
"en": "Welcome",
|
|
258
|
+
"ja": "ようこそ"
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
**Variable placeholders:**
|
|
265
|
+
```json
|
|
266
|
+
{
|
|
267
|
+
"greeting": {
|
|
268
|
+
"key": "home.greeting",
|
|
269
|
+
"defaultValue": "Hello, {name}!"
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
Usage:
|
|
275
|
+
```typescript
|
|
276
|
+
t('greeting', undefined, { name: 'John' })
|
|
277
|
+
// → "Hello, John!"
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
## Configuration
|
|
281
|
+
|
|
282
|
+
### Environment Variables
|
|
283
|
+
|
|
284
|
+
Configure CMS behavior via environment variables in `.env.local`:
|
|
285
|
+
|
|
286
|
+
```bash
|
|
287
|
+
# Default locale (default: 'en')
|
|
288
|
+
SPFN_CMS_DEFAULT_LOCALE=ko
|
|
289
|
+
|
|
290
|
+
# Supported locales, comma-separated (default: 'en,ko')
|
|
291
|
+
SPFN_CMS_SUPPORTED_LOCALES=en,ko,ja
|
|
292
|
+
|
|
293
|
+
# Auto-detect browser language (default: true)
|
|
294
|
+
SPFN_CMS_DETECT_BROWSER_LANGUAGE=true
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
### Runtime Configuration
|
|
298
|
+
|
|
299
|
+
Override configuration at runtime (mainly for testing):
|
|
300
|
+
|
|
301
|
+
```typescript
|
|
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
|
|
318
|
+
|
|
319
|
+
### Automatic Locale Detection
|
|
320
|
+
|
|
321
|
+
The CMS automatically manages user locale with the following priority:
|
|
322
|
+
|
|
323
|
+
1. **Cookie** - User's explicitly selected locale (persisted)
|
|
324
|
+
2. **Browser Language** - Auto-detected from `Accept-Language` header (if enabled)
|
|
325
|
+
3. **Default Locale** - System default from environment variables
|
|
326
|
+
|
|
327
|
+
### Server Actions (`@spfn/cms/actions`)
|
|
328
|
+
|
|
329
|
+
Use Server Actions for locale management in both server and client components:
|
|
330
|
+
|
|
331
|
+
**Get current locale:**
|
|
332
|
+
|
|
333
|
+
```typescript
|
|
334
|
+
// Server Component
|
|
335
|
+
import { getLocale } from '@spfn/cms/actions';
|
|
336
|
+
|
|
337
|
+
export default async function RootLayout({ children }) {
|
|
338
|
+
const locale = await getLocale();
|
|
339
|
+
|
|
340
|
+
return <html lang={locale}>{children}</html>;
|
|
341
|
+
}
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
```typescript
|
|
345
|
+
// Client Component
|
|
346
|
+
'use client';
|
|
347
|
+
import { getLocale } from '@spfn/cms/actions';
|
|
348
|
+
import { useEffect, useState } from 'react';
|
|
349
|
+
|
|
350
|
+
export default function LanguageSwitcher() {
|
|
351
|
+
const [locale, setLocale] = useState('');
|
|
352
|
+
|
|
353
|
+
useEffect(() => {
|
|
354
|
+
getLocale().then(setLocale);
|
|
355
|
+
}, []);
|
|
356
|
+
|
|
357
|
+
return <div>Current: {locale}</div>;
|
|
358
|
+
}
|
|
359
|
+
```
|
|
360
|
+
|
|
361
|
+
**Change locale:**
|
|
362
|
+
|
|
363
|
+
```typescript
|
|
364
|
+
import { setLocale } from '@spfn/cms/actions';
|
|
365
|
+
|
|
366
|
+
async function changeLanguage(newLocale: string) {
|
|
367
|
+
await setLocale(newLocale);
|
|
368
|
+
window.location.reload(); // Reload to apply changes
|
|
369
|
+
}
|
|
370
|
+
```
|
|
371
|
+
|
|
372
|
+
**Get supported locales:**
|
|
373
|
+
|
|
374
|
+
```typescript
|
|
375
|
+
import { getLocales } from '@spfn/cms/actions';
|
|
376
|
+
|
|
377
|
+
const locales = await getLocales(); // ['ko', 'en', 'ja']
|
|
378
|
+
```
|
|
379
|
+
|
|
380
|
+
### Auto-detect Locale in Server Components
|
|
381
|
+
|
|
382
|
+
When `locale` is not specified, `getSection()` automatically uses the detected locale:
|
|
383
|
+
|
|
384
|
+
```typescript
|
|
385
|
+
import { getSection } from '@spfn/cms/server';
|
|
386
|
+
|
|
387
|
+
// Auto-detects locale from cookie → browser → default
|
|
388
|
+
const { t } = await getSection('home');
|
|
389
|
+
|
|
390
|
+
// Or explicitly specify locale
|
|
391
|
+
const { t: tEn } = await getSection('home', 'en');
|
|
392
|
+
```
|
|
393
|
+
|
|
394
|
+
## Documentation
|
|
395
|
+
|
|
396
|
+
- **[Label Auto-Sync Guide](./LABEL_SYNC_GUIDE.md)** - Detailed configuration guide
|
|
397
|
+
- **[Examples](./examples/)** - Usage examples
|
|
398
|
+
|
|
399
|
+
## Architecture
|
|
400
|
+
|
|
401
|
+
```
|
|
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
|
+
|
|
428
|
+
### Server-side API
|
|
429
|
+
|
|
430
|
+
- `getSection(section, locale?)` - Get section labels (auto-detects locale if not specified)
|
|
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`)
|
|
435
|
+
|
|
436
|
+
Available for both server and client components:
|
|
437
|
+
|
|
438
|
+
- `getLocale()` - Get current locale (cookie → browser → default)
|
|
439
|
+
- `setLocale(locale)` - Set locale (saves to cookie)
|
|
440
|
+
- `getLocales()` - Get supported locale list
|
|
441
|
+
- `LOCALE_COOKIE_KEY` - Locale cookie key constant
|
|
442
|
+
|
|
443
|
+
### Configuration API
|
|
444
|
+
|
|
445
|
+
- `getCmsConfig()` - Get current CMS configuration
|
|
446
|
+
- `configureCms(config)` - Update configuration (runtime)
|
|
447
|
+
- `resetCmsConfig()` - Reset configuration to defaults
|
|
448
|
+
|
|
449
|
+
### Client-side API (`@spfn/cms/client`)
|
|
450
|
+
|
|
451
|
+
- `useSection(section, options?)` - Section labels hook
|
|
452
|
+
- `useSections(sections)` - Multiple sections hook
|
|
453
|
+
- `useCmsStore()` - CMS store hook
|
|
454
|
+
- `cmsApi` - CMS API client
|
|
455
|
+
- `InitCms` - Client initialization component
|
|
456
|
+
|
|
457
|
+
### Sync API
|
|
458
|
+
|
|
459
|
+
- `loadLabelsFromJson(labelsDir)` - Load labels from JSON files
|
|
460
|
+
- `syncAll(sections, options?)` - Sync all sections
|
|
461
|
+
- `syncSection(definition, options?)` - Sync specific section
|
|
462
|
+
|
|
463
|
+
### Codegen Integration
|
|
464
|
+
|
|
465
|
+
- `createLabelSyncGenerator(config?)` - Generator factory
|
|
466
|
+
- `LabelSyncGenerator` - Generator class
|
|
467
|
+
|
|
468
|
+
## Development Workflow
|
|
469
|
+
|
|
470
|
+
1. **Create/Edit JSON files** in `src/cms/labels/`
|
|
471
|
+
2. **Auto-sync happens** (if dev server is running)
|
|
472
|
+
3. **Labels immediately available** via `getSection()` or `useSection()`
|
|
473
|
+
|
|
474
|
+
**Example:**
|
|
475
|
+
|
|
476
|
+
```bash
|
|
477
|
+
# Terminal 1: Start dev server
|
|
478
|
+
pnpm dev
|
|
479
|
+
|
|
480
|
+
# Terminal 2: Edit label file
|
|
481
|
+
echo '{"test": {"key": "layout.test", "defaultValue": "Test"}}' > src/cms/labels/layout/test.json
|
|
482
|
+
|
|
483
|
+
# Auto-sync triggers
|
|
484
|
+
# ✅ Label sync completed
|
|
485
|
+
# Created: 1
|
|
486
|
+
```
|
|
487
|
+
|
|
488
|
+
## License
|
|
489
|
+
|
|
490
|
+
MIT
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @spfn/cms/actions
|
|
3
|
+
*
|
|
4
|
+
* Server Actions
|
|
5
|
+
* 서버/클라이언트 컴포넌트 양쪽에서 사용 가능한 Server Actions
|
|
6
|
+
*/
|
|
7
|
+
export { getLocale, setLocale, getLocales, } from './helpers/locale.actions.js';
|
|
8
|
+
export { LOCALE_COOKIE_KEY } from './helpers/locale.constants.js';
|
|
9
|
+
//# sourceMappingURL=actions.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"actions.d.ts","sourceRoot":"","sources":["../src/actions.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAGH,OAAO,EACH,SAAS,EACT,SAAS,EACT,UAAU,GACb,MAAM,6BAA6B,CAAC;AAGrC,OAAO,EAAE,iBAAiB,EAAE,MAAM,+BAA+B,CAAC"}
|
package/dist/actions.js
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @spfn/cms/actions
|
|
3
|
+
*
|
|
4
|
+
* Server Actions
|
|
5
|
+
* 서버/클라이언트 컴포넌트 양쪽에서 사용 가능한 Server Actions
|
|
6
|
+
*/
|
|
7
|
+
// Locale Server Actions
|
|
8
|
+
export { getLocale, setLocale, getLocales, } from './helpers/locale.actions.js';
|
|
9
|
+
// Locale Constants
|
|
10
|
+
export { LOCALE_COOKIE_KEY } from './helpers/locale.constants.js';
|
|
11
|
+
//# sourceMappingURL=actions.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"actions.js","sourceRoot":"","sources":["../src/actions.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,wBAAwB;AACxB,OAAO,EACH,SAAS,EACT,SAAS,EACT,UAAU,GACb,MAAM,6BAA6B,CAAC;AAErC,mBAAmB;AACnB,OAAO,EAAE,iBAAiB,EAAE,MAAM,+BAA+B,CAAC"}
|
package/dist/client.d.ts
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @spfn/cms/client
|
|
3
|
+
*
|
|
4
|
+
* Client Components Only
|
|
5
|
+
* 클라이언트 컴포넌트 전용 (브라우저에서 실행)
|
|
6
|
+
*/
|
|
7
|
+
import type { InferContract } from '@spfn/core';
|
|
8
|
+
import { getLabelsContract, createLabelContract } from './routes/labels/contract';
|
|
9
|
+
import { getLabelContract, updateLabelContract, deleteLabelContract } from './routes/labels/[id]/contract';
|
|
10
|
+
import { getPublishedCacheContract } from './routes/published-cache/contract';
|
|
11
|
+
/**
|
|
12
|
+
* CMS API Client
|
|
13
|
+
*/
|
|
14
|
+
export declare const cmsApi: {
|
|
15
|
+
/**
|
|
16
|
+
* Labels API
|
|
17
|
+
*/
|
|
18
|
+
readonly labels: {
|
|
19
|
+
/**
|
|
20
|
+
* GET /cms/labels
|
|
21
|
+
* 라벨 목록 조회 (섹션 필터, 페이지네이션)
|
|
22
|
+
*/
|
|
23
|
+
readonly list: (options?: {
|
|
24
|
+
query?: InferContract<typeof getLabelsContract>["query"];
|
|
25
|
+
}) => Promise<{
|
|
26
|
+
limit: number;
|
|
27
|
+
offset: number;
|
|
28
|
+
labels: {
|
|
29
|
+
section: string;
|
|
30
|
+
id: number;
|
|
31
|
+
key: string;
|
|
32
|
+
type: string;
|
|
33
|
+
publishedVersion: number | null;
|
|
34
|
+
createdBy: string | null;
|
|
35
|
+
createdAt: string;
|
|
36
|
+
updatedAt: string;
|
|
37
|
+
}[];
|
|
38
|
+
total: number;
|
|
39
|
+
}>;
|
|
40
|
+
/**
|
|
41
|
+
* GET /cms/labels/:id
|
|
42
|
+
* 특정 라벨 조회
|
|
43
|
+
*/
|
|
44
|
+
readonly getById: (options: {
|
|
45
|
+
params: InferContract<typeof getLabelContract>["params"];
|
|
46
|
+
}) => Promise<{
|
|
47
|
+
section: string;
|
|
48
|
+
id: number;
|
|
49
|
+
key: string;
|
|
50
|
+
type: string;
|
|
51
|
+
publishedVersion: number | null;
|
|
52
|
+
createdBy: string | null;
|
|
53
|
+
createdAt: string;
|
|
54
|
+
updatedAt: string;
|
|
55
|
+
} | {
|
|
56
|
+
error: string;
|
|
57
|
+
}>;
|
|
58
|
+
/**
|
|
59
|
+
* POST /cms/labels
|
|
60
|
+
* 새 라벨 생성
|
|
61
|
+
*/
|
|
62
|
+
readonly create: (options: {
|
|
63
|
+
body: InferContract<typeof createLabelContract>["body"];
|
|
64
|
+
}) => Promise<{
|
|
65
|
+
section: string;
|
|
66
|
+
id: number;
|
|
67
|
+
key: string;
|
|
68
|
+
type: string;
|
|
69
|
+
publishedVersion: number | null;
|
|
70
|
+
createdBy: string | null;
|
|
71
|
+
createdAt: string;
|
|
72
|
+
updatedAt: string;
|
|
73
|
+
} | {
|
|
74
|
+
key?: string | undefined;
|
|
75
|
+
error: string;
|
|
76
|
+
}>;
|
|
77
|
+
/**
|
|
78
|
+
* PATCH /cms/labels/:id
|
|
79
|
+
* 라벨 업데이트
|
|
80
|
+
*/
|
|
81
|
+
readonly update: (options: {
|
|
82
|
+
params: InferContract<typeof updateLabelContract>["params"];
|
|
83
|
+
body: InferContract<typeof updateLabelContract>["body"];
|
|
84
|
+
}) => Promise<{
|
|
85
|
+
section: string;
|
|
86
|
+
id: number;
|
|
87
|
+
key: string;
|
|
88
|
+
type: string;
|
|
89
|
+
publishedVersion: number | null;
|
|
90
|
+
createdBy: string | null;
|
|
91
|
+
createdAt: string;
|
|
92
|
+
updatedAt: string;
|
|
93
|
+
} | {
|
|
94
|
+
error: string;
|
|
95
|
+
}>;
|
|
96
|
+
/**
|
|
97
|
+
* DELETE /cms/labels/:id
|
|
98
|
+
* 라벨 삭제
|
|
99
|
+
*/
|
|
100
|
+
readonly delete: (options: {
|
|
101
|
+
params: InferContract<typeof deleteLabelContract>["params"];
|
|
102
|
+
}) => Promise<{
|
|
103
|
+
id: number;
|
|
104
|
+
success: boolean;
|
|
105
|
+
} | {
|
|
106
|
+
error: string;
|
|
107
|
+
}>;
|
|
108
|
+
};
|
|
109
|
+
/**
|
|
110
|
+
* Published Cache API
|
|
111
|
+
*/
|
|
112
|
+
readonly publishedCache: {
|
|
113
|
+
/**
|
|
114
|
+
* GET /cms/published-cache
|
|
115
|
+
* 발행된 콘텐츠 캐시 조회
|
|
116
|
+
*/
|
|
117
|
+
readonly get: (options: {
|
|
118
|
+
query: InferContract<typeof getPublishedCacheContract>["query"];
|
|
119
|
+
}) => Promise<{
|
|
120
|
+
section: string;
|
|
121
|
+
locale: string;
|
|
122
|
+
content: {
|
|
123
|
+
[x: string]: any;
|
|
124
|
+
};
|
|
125
|
+
version: number;
|
|
126
|
+
publishedAt: string | null;
|
|
127
|
+
}[] | {
|
|
128
|
+
error: string;
|
|
129
|
+
}>;
|
|
130
|
+
};
|
|
131
|
+
};
|
|
132
|
+
/**
|
|
133
|
+
* Type exports
|
|
134
|
+
*/
|
|
135
|
+
export type CmsApi = typeof cmsApi;
|
|
136
|
+
export { useCmsStore, useSection, useSections } from './store';
|
|
137
|
+
export { InitCms } from './init';
|
|
138
|
+
//# sourceMappingURL=client.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAGH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAGhD,OAAO,EACH,iBAAiB,EACjB,mBAAmB,EACtB,MAAM,0BAA0B,CAAC;AAClC,OAAO,EACH,gBAAgB,EAChB,mBAAmB,EACnB,mBAAmB,EACtB,MAAM,+BAA+B,CAAC;AAGvC,OAAO,EAAE,yBAAyB,EAAE,MAAM,mCAAmC,CAAC;AAE9E;;GAEG;AACH,eAAO,MAAM,MAAM;IACf;;OAEG;;QAEC;;;WAGG;kCACc;YAAE,KAAK,CAAC,EAAE,aAAa,CAAC,OAAO,iBAAiB,CAAC,CAAC,OAAO,CAAC,CAAA;SAAE;;;;;;;;;;;;;;;QAG7E;;;WAGG;oCACgB;YAAE,MAAM,EAAE,aAAa,CAAC,OAAO,gBAAgB,CAAC,CAAC,QAAQ,CAAC,CAAA;SAAE;;;;;;;;;;;;QAG/E;;;WAGG;mCACe;YAAE,IAAI,EAAE,aAAa,CAAC,OAAO,mBAAmB,CAAC,CAAC,MAAM,CAAC,CAAA;SAAE;;;;;;;;;;;;;QAG7E;;;WAGG;mCACe;YACd,MAAM,EAAE,aAAa,CAAC,OAAO,mBAAmB,CAAC,CAAC,QAAQ,CAAC,CAAC;YAC5D,IAAI,EAAE,aAAa,CAAC,OAAO,mBAAmB,CAAC,CAAC,MAAM,CAAC,CAAC;SAC3D;;;;;;;;;;;;QAGD;;;WAGG;mCACe;YAAE,MAAM,EAAE,aAAa,CAAC,OAAO,mBAAmB,CAAC,CAAC,QAAQ,CAAC,CAAA;SAAE;;;;;;;IAIrF;;OAEG;;QAEC;;;WAGG;gCACY;YAAE,KAAK,EAAE,aAAa,CAAC,OAAO,yBAAyB,CAAC,CAAC,OAAO,CAAC,CAAA;SAAE;;;;;;;;;;;;CAGhF,CAAC;AAEX;;GAEG;AACH,MAAM,MAAM,MAAM,GAAG,OAAO,MAAM,CAAC;AAGnC,OAAO,EAAE,WAAW,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;AAG/D,OAAO,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAC"}
|