memi-board 0.52.0 → 0.52.1
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 +297 -297
- package/dist/composables/useMemiBoardAutoTranslate.d.ts +1 -0
- package/dist/composables/useMemiBoardWritingAssistant.d.ts +1 -0
- package/dist/index.js +103 -68
- package/dist/index.js.map +1 -1
- package/dist/runtime/components/Editor.vue +0 -2
- package/docs/firebase-setup.md +85 -85
- package/docs/firestore.rules.example +58 -58
- package/docs/host-integration.md +80 -80
- package/docs/internationalization.md +76 -72
- package/docs/roles-migration.md +18 -18
- package/docs/storage.rules.example +18 -18
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,75 +1,75 @@
|
|
|
1
1
|
# memi-board
|
|
2
2
|
|
|
3
|
-
Nuxt 4 + Nuxt UI + Firebase
|
|
3
|
+
An npm package that adds a board to Nuxt 4 + Nuxt UI + Firebase projects.
|
|
4
4
|
|
|
5
|
-
-
|
|
6
|
-
-
|
|
7
|
-
-
|
|
8
|
-
-
|
|
5
|
+
- Post CRUD · comments · Storage attachments · categories · view counts
|
|
6
|
+
- Author/admin permissions (Firestore Rules)
|
|
7
|
+
- Local profanity filter + Firebase AI Logic (Gemini) moderation (optional)
|
|
8
|
+
- **Share previews (OG)** — list and detail meta without a host SEO API (`useMemiBoard*Seo`)
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
**No server or Firebase Admin required** (permissions live in Rules, moderation runs in the browser — it can be bypassed; see the limitations below).
|
|
11
|
+
Only the public list and post detail pages need SSR to fill OG tags; writing and settings pages can stay CSR.
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
Any host app on the same stack (Nuxt 4 · `@nuxt/ui` · `nuxt-vuefire` · Firebase) **can reuse it as is**. Each host can use its own Firebase project, and within one project board instances are separated by `boardId` — `memiBoardPosts`/`memiBoardComments`/`memiBoardLikes` are all top-level flat collections, and `boardId` is a field on each document (not a path segment).
|
|
14
14
|
|
|
15
|
-
##
|
|
15
|
+
## Guides
|
|
16
16
|
|
|
17
|
-
|
|
|
17
|
+
| Task | Guide |
|
|
18
18
|
|---|---|
|
|
19
|
-
|
|
|
20
|
-
|
|
|
21
|
-
| Firebase
|
|
22
|
-
|
|
|
23
|
-
|
|
|
19
|
+
| First install on another site | [Host integration guide](docs/host-integration.md) — minimal setup, routes, first board |
|
|
20
|
+
| Single language, or choosing multilingual UI and translation | [Internationalization and content translation](docs/internationalization.md) — per-mode setup and when translations are saved |
|
|
21
|
+
| Connecting Firebase and deploying rules and indexes | [Firebase setup](docs/firebase-setup.md) |
|
|
22
|
+
| Per-store/organization assigned permissions, migrating existing roles | [Roles and migration](docs/roles-migration.md) |
|
|
23
|
+
| Configuration, API, and troubleshooting details | This README's [host setup](#host-app-setup), [API](#provided-api), [troubleshooting](#troubleshooting) |
|
|
24
24
|
|
|
25
|
-
`i18n.enabled
|
|
25
|
+
`i18n.enabled` and `translation.enabled` are supported in **0.47.0 and later**. Hosts that use these options should install or update with `pnpm add memi-board@^0.47.0`.
|
|
26
26
|
|
|
27
|
-
##
|
|
27
|
+
## Platform requirements
|
|
28
28
|
|
|
29
|
-
|
|
|
29
|
+
| Item | Requirement |
|
|
30
30
|
|------|------|
|
|
31
|
-
|
|
|
32
|
-
| UI | `@nuxt/ui` **4+** (
|
|
33
|
-
|
|
|
31
|
+
| Runtime | Nuxt **4+** |
|
|
32
|
+
| UI | `@nuxt/ui` **4+** (components assume auto-imports such as `UButton`) |
|
|
33
|
+
| Data | Firebase **Firestore + Auth + Storage** |
|
|
34
34
|
| Vue | `vue` 3.5+, `vuefire` 3.2+, `firebase` 12+ |
|
|
35
|
-
|
|
|
35
|
+
| Module | `nuxt-vuefire` (installed and configured by the host) |
|
|
36
36
|
|
|
37
|
-
|
|
37
|
+
**Intentionally out of scope:** Firebase initialization, registering routes/middleware, App Check initialization, server-side moderation.
|
|
38
38
|
|
|
39
|
-
##
|
|
39
|
+
## Structure
|
|
40
40
|
|
|
41
|
-
|
|
|
41
|
+
| Entry | Role |
|
|
42
42
|
|--------|------|
|
|
43
|
-
| `memi-board` | **Nuxt
|
|
44
|
-
| `memi-board/runtime` | composables, types,
|
|
43
|
+
| `memi-board` | **Nuxt module** — configuration, SFC auto-import, peer dedupe |
|
|
44
|
+
| `memi-board/runtime` | composables, types, utilities (**no Vue component exports**) |
|
|
45
45
|
|
|
46
|
-
|
|
46
|
+
Most users only configure `memiBoard` in `nuxt.config.ts`. No separate Nuxt plugin is needed.
|
|
47
47
|
|
|
48
48
|
```
|
|
49
|
-
|
|
50
|
-
├── nuxt-vuefire + firebaseConfig ←
|
|
51
|
-
├── (
|
|
52
|
-
├── memi-board ← Nuxt
|
|
53
|
-
├── pages/board/* ←
|
|
54
|
-
└── firestore.rules + storage.rules ← docs
|
|
49
|
+
Host Nuxt app
|
|
50
|
+
├── nuxt-vuefire + firebaseConfig ← auth · Firestore
|
|
51
|
+
├── (optional) App Check plugin ← practically required with AI Logic Enforce
|
|
52
|
+
├── memi-board ← Nuxt module + runtime config
|
|
53
|
+
├── pages/board/* ← routes are owned by the host (+ one SEO line)
|
|
54
|
+
└── firestore.rules + storage.rules ← merge the docs examples, then deploy
|
|
55
55
|
```
|
|
56
56
|
|
|
57
|
-
##
|
|
57
|
+
## Installation
|
|
58
58
|
|
|
59
|
-
|
|
59
|
+
**Prerequisites:** Node + pnpm, a Firebase project (Firestore · Storage · Auth). Starting from scratch → [docs/firebase-setup.md](docs/firebase-setup.md).
|
|
60
60
|
|
|
61
61
|
```bash
|
|
62
62
|
pnpm add memi-board @nuxt/ui nuxt-vuefire vuefire firebase
|
|
63
63
|
```
|
|
64
64
|
|
|
65
|
-
`vuefire` / `firebase` / `nuxt-vuefire
|
|
65
|
+
Install `vuefire` / `firebase` / `nuxt-vuefire` **directly in the host**. TipTap is managed by `@nuxt/ui` and `memi-board`, so the host does not need to install it or align its version. Dates are formatted with `Intl`, so `dayjs` is not needed.
|
|
66
66
|
|
|
67
67
|
---
|
|
68
68
|
|
|
69
|
-
##
|
|
69
|
+
## Host app setup
|
|
70
70
|
|
|
71
|
-
|
|
72
|
-
|
|
71
|
+
This covers only what the host does. **No host-specific SEO API or server routes are needed.**
|
|
72
|
+
The package reads public posts (`listed === true`) from Firestore directly and fills OG tags with `useSeoMeta`.
|
|
73
73
|
|
|
74
74
|
### 1. `nuxt.config.ts` — modules + `memiBoard` + vuefire + routeRules
|
|
75
75
|
|
|
@@ -83,11 +83,11 @@ export default defineNuxtConfig({
|
|
|
83
83
|
],
|
|
84
84
|
|
|
85
85
|
memiBoard: {
|
|
86
|
-
//
|
|
86
|
+
// Single-language example. For multilingual services, see the internationalization guide below
|
|
87
87
|
locale: 'ko',
|
|
88
88
|
i18n: { enabled: false },
|
|
89
89
|
// Firestore: memiBoardPosts / memiBoardComments / memiBoardLikes / memiBoardSettings / memiBoardUsers
|
|
90
|
-
// (
|
|
90
|
+
// (all flat collections — boardId is a field on each document. Pass it as the boardId argument when calling composables)
|
|
91
91
|
auth: { providers: ['google', 'apple'] }, // + 'emailPassword'
|
|
92
92
|
moderation: {
|
|
93
93
|
enabled: true,
|
|
@@ -95,22 +95,22 @@ export default defineNuxtConfig({
|
|
|
95
95
|
onError: 'block', // 'allow' | 'block'
|
|
96
96
|
moderateImages: false,
|
|
97
97
|
useLimitedUseAppCheckTokens: false,
|
|
98
|
-
// localBlocklist: ['
|
|
98
|
+
// localBlocklist: ['banned-word'],
|
|
99
99
|
// blockBanThreshold: 10,
|
|
100
100
|
// blockBanDecayMs: 24 * 60 * 60 * 1000,
|
|
101
101
|
},
|
|
102
|
-
//
|
|
102
|
+
// Share previews (OG) — use the package composables, no host API
|
|
103
103
|
seo: {
|
|
104
104
|
enabled: true,
|
|
105
|
-
siteName: 'My App', // og:site_name · title
|
|
105
|
+
siteName: 'My App', // og:site_name · title suffix
|
|
106
106
|
siteUrl: process.env.NUXT_PUBLIC_SITE_URL
|
|
107
|
-
|| 'https://example.com', //
|
|
108
|
-
defaultOgImage: '/images/og-default.jpg', //
|
|
109
|
-
basePath: '/board', // canonical
|
|
107
|
+
|| 'https://example.com', // absolute URL origin (no trailing slash)
|
|
108
|
+
defaultOgImage: '/images/og-default.jpg', // when the post has no previewImage
|
|
109
|
+
basePath: '/board', // canonical path prefix
|
|
110
110
|
},
|
|
111
111
|
},
|
|
112
112
|
|
|
113
|
-
// (
|
|
113
|
+
// (optional) host-wide siteUrl — SEO may fall back to it after memiBoard.seo.siteUrl
|
|
114
114
|
runtimeConfig: {
|
|
115
115
|
public: {
|
|
116
116
|
siteUrl: process.env.NUXT_PUBLIC_SITE_URL || 'https://example.com',
|
|
@@ -118,8 +118,8 @@ export default defineNuxtConfig({
|
|
|
118
118
|
},
|
|
119
119
|
|
|
120
120
|
vuefire: {
|
|
121
|
-
//
|
|
122
|
-
// (
|
|
121
|
+
// Must match Console → Project settings → Your apps → web app SDK snippet
|
|
122
|
+
// (if it is a web app with a different appId, App Check / AI Logic fails)
|
|
123
123
|
config: {
|
|
124
124
|
apiKey: '...',
|
|
125
125
|
authDomain: '...',
|
|
@@ -132,8 +132,8 @@ export default defineNuxtConfig({
|
|
|
132
132
|
},
|
|
133
133
|
|
|
134
134
|
routeRules: {
|
|
135
|
-
//
|
|
136
|
-
//
|
|
135
|
+
// Public list and post detail: keep SSR (default) → link preview / OG crawlers read the HTML meta
|
|
136
|
+
// Only writing and settings pages are CSR
|
|
137
137
|
'/board/**/new': { ssr: false },
|
|
138
138
|
'/board/**/**/edit': { ssr: false },
|
|
139
139
|
'/board/version-history': { ssr: false },
|
|
@@ -143,54 +143,54 @@ export default defineNuxtConfig({
|
|
|
143
143
|
})
|
|
144
144
|
```
|
|
145
145
|
|
|
146
|
-
`memiBoard`
|
|
146
|
+
The Nuxt module passes the `memiBoard` options to the runtime plugin automatically. **Do not create `app/plugins/memi-board.ts`.**
|
|
147
147
|
|
|
148
|
-
#### `memiBoard`
|
|
148
|
+
#### `memiBoard` options summary
|
|
149
149
|
|
|
150
|
-
|
|
|
150
|
+
| Key | Default | Description |
|
|
151
151
|
|----|------|------|
|
|
152
|
-
| `postsCollection` | `'memiBoardPosts'` |
|
|
153
|
-
| `commentsCollection` | `'memiBoardComments'` |
|
|
154
|
-
| `likesCollection` | `'memiBoardLikes'` |
|
|
155
|
-
| `reportsCollection` | `'memiBoardReports'` |
|
|
156
|
-
| `settingsCollection` | `'memiBoardSettings'` |
|
|
157
|
-
| `usersCollection` | `'memiBoardUsers'` |
|
|
158
|
-
| `auth.providers` | `['google','apple']` |
|
|
159
|
-
| `moderation.*` | (
|
|
160
|
-
| `seo.enabled` | `true` | `false
|
|
161
|
-
| `seo.siteName` | `'Board'` | OG
|
|
162
|
-
| `seo.siteUrl` | `''` |
|
|
163
|
-
| `seo.defaultOgImage` | `''` |
|
|
152
|
+
| `postsCollection` | `'memiBoardPosts'` | Posts collection name |
|
|
153
|
+
| `commentsCollection` | `'memiBoardComments'` | Comments collection name |
|
|
154
|
+
| `likesCollection` | `'memiBoardLikes'` | Likes collection name |
|
|
155
|
+
| `reportsCollection` | `'memiBoardReports'` | Reports collection name |
|
|
156
|
+
| `settingsCollection` | `'memiBoardSettings'` | Board settings collection name |
|
|
157
|
+
| `usersCollection` | `'memiBoardUsers'` | Role documents collection name |
|
|
158
|
+
| `auth.providers` | `['google','apple']` | Sign-in providers |
|
|
159
|
+
| `moderation.*` | (AI section below) | Local · Gemini moderation |
|
|
160
|
+
| `seo.enabled` | `true` | When `false`, the SEO composables are no-ops |
|
|
161
|
+
| `seo.siteName` | `'Board'` | OG site name · title suffix |
|
|
162
|
+
| `seo.siteUrl` | `''` | Absolute URL origin. If empty, falls back to `runtimeConfig.public.siteUrl` → request origin |
|
|
163
|
+
| `seo.defaultOgImage` | `''` | Used when there is no preview image (relative path or `https://…`) |
|
|
164
164
|
| `seo.basePath` | `'/board'` | canonical: `{base}`, `{base}/{category}`, `{base}/{category}/{slug}` |
|
|
165
|
-
| `i18n.enabled` | `true` | `false
|
|
166
|
-
| `translation.enabled` | `true` | `false
|
|
167
|
-
| `locale` | `'ko'` |
|
|
168
|
-
| `roleLabels` |
|
|
165
|
+
| `i18n.enabled` | `true` | When `false`, fixes the UI to `locale` and disables language selection and content translation |
|
|
166
|
+
| `translation.enabled` | `true` | When `false`, keeps UI language switching but disables generating and showing content translations |
|
|
167
|
+
| `locale` | `'ko'` | With internationalization on, the fallback after the board and host languages. With it off, the fixed UI language |
|
|
168
|
+
| `roleLabels` | Default names per language | Per-service role display names. Does not change permissions or assignment scope |
|
|
169
169
|
|
|
170
|
-
#### routeRules
|
|
170
|
+
#### routeRules principles
|
|
171
171
|
|
|
172
|
-
|
|
|
172
|
+
| Path | SSR | Reason |
|
|
173
173
|
|------|-----|------|
|
|
174
|
-
| `/board`, `/board/{category}`, `/board/{category}/{slug}` | **on** (
|
|
175
|
-
| `/board/**/new`, `…/edit`,
|
|
174
|
+
| `/board`, `/board/{category}`, `/board/{category}/{slug}` | **on** (default) | OG / link previews (e.g. KakaoTalk, Slack) |
|
|
175
|
+
| `/board/**/new`, `…/edit`, settings · user management | **off** | Sign-in and permission UI, no SEO needed |
|
|
176
176
|
|
|
177
|
-
>
|
|
177
|
+
> The older guide's all-CSR `/board/**: { ssr: false }` **leaves share previews empty.** Keep SSR on for public read paths.
|
|
178
178
|
|
|
179
|
-
### 2.
|
|
179
|
+
### 2. One SEO line per page (auto-import)
|
|
180
180
|
|
|
181
|
-
|
|
182
|
-
|
|
181
|
+
The module auto-imports `useMemiBoardListSeo` / `useMemiBoardPostSeo`.
|
|
182
|
+
**Do not create host `/server/api/board/**` SEO endpoints.**
|
|
183
183
|
|
|
184
184
|
```vue
|
|
185
|
-
<!-- app/pages/board/index.vue —
|
|
185
|
+
<!-- app/pages/board/index.vue — full list -->
|
|
186
186
|
<script setup lang="ts">
|
|
187
187
|
await useMemiBoardListSeo({ path: '/board' })
|
|
188
|
-
// … MemiBoardList
|
|
188
|
+
// … MemiBoardList, etc.
|
|
189
189
|
</script>
|
|
190
190
|
```
|
|
191
191
|
|
|
192
192
|
```vue
|
|
193
|
-
<!-- app/pages/board/[category]/index.vue —
|
|
193
|
+
<!-- app/pages/board/[category]/index.vue — category list -->
|
|
194
194
|
<script setup lang="ts">
|
|
195
195
|
const route = useRoute()
|
|
196
196
|
const categoryId = computed(() => String(route.params.category || ''))
|
|
@@ -203,7 +203,7 @@ await useMemiBoardListSeo({
|
|
|
203
203
|
```
|
|
204
204
|
|
|
205
205
|
```vue
|
|
206
|
-
<!-- app/pages/board/[category]/[id]/index.vue —
|
|
206
|
+
<!-- app/pages/board/[category]/[id]/index.vue — post detail (the id slot = slug) -->
|
|
207
207
|
<script setup lang="ts">
|
|
208
208
|
const route = useRoute()
|
|
209
209
|
const categoryId = computed(() => String(route.params.category || ''))
|
|
@@ -216,8 +216,8 @@ const { post: postSeo } = await useMemiBoardPostSeo({
|
|
|
216
216
|
`/board/${encodeURIComponent(categoryId.value)}/${encodeURIComponent(id.value)}`,
|
|
217
217
|
),
|
|
218
218
|
})
|
|
219
|
-
// postSeo: id · title · previewImage
|
|
220
|
-
//
|
|
219
|
+
// postSeo: id · title · previewImage, etc. — filled immediately on the first SSR load.
|
|
220
|
+
// On client navigation it is not awaited so page transitions are not blocked, so it may be null at first.
|
|
221
221
|
|
|
222
222
|
const { resolvePostIdBySlug } = useMemiBoardPosts(categoryId)
|
|
223
223
|
const internalId = ref('')
|
|
@@ -227,43 +227,43 @@ watch([categoryId, id], async ([category, slug]) => {
|
|
|
227
227
|
return
|
|
228
228
|
}
|
|
229
229
|
if (import.meta.server) return
|
|
230
|
-
// id
|
|
230
|
+
// Look up the id only (MemiBoardDetail subscribes to the body). A just-written post resolves without a lookup.
|
|
231
231
|
const result = await resolvePostIdBySlug(slug)
|
|
232
232
|
internalId.value = result.status === 'ok' ? result.id : ''
|
|
233
233
|
}, { immediate: true })
|
|
234
234
|
</script>
|
|
235
235
|
```
|
|
236
236
|
|
|
237
|
-
>
|
|
237
|
+
> On detail and edit pages, resolve slug → id with `resolvePostIdBySlug` instead of `getPostBySlug` (which also reads the body). Opening the preview right after saving becomes faster by two Firestore round trips.
|
|
238
238
|
|
|
239
|
-
| composable |
|
|
239
|
+
| composable | Target | OG summary |
|
|
240
240
|
|------------|------|---------|
|
|
241
|
-
| `useMemiBoardListSeo` |
|
|
242
|
-
| `useMemiBoardPostSeo` |
|
|
241
|
+
| `useMemiBoardListSeo` | List | Category label · description, image from the latest public post |
|
|
242
|
+
| `useMemiBoardPostSeo` | Detail | Title · summary · author, **`previewImage` → og:image** |
|
|
243
243
|
|
|
244
|
-
-
|
|
245
|
-
-
|
|
246
|
-
-
|
|
244
|
+
- Only public (`listed`) posts are queried. **Hidden boards** leave the meta empty and behave closer to `noindex`.
|
|
245
|
+
- Post detail `og:type` = `article`, list = `website`.
|
|
246
|
+
- Local check:
|
|
247
247
|
`curl -sL 'http://localhost:3000/board/…' | grep og:title`
|
|
248
|
-
|
|
249
|
-
-
|
|
248
|
+
or the browser's **View Page Source** (`view-source:`). Looking only at DevTools Elements can be confused with the post-hydration DOM.
|
|
249
|
+
- Check link previews (e.g. KakaoTalk, Slack) with the **deployed URL** (localhost cannot be crawled).
|
|
250
250
|
|
|
251
|
-
### 3.
|
|
251
|
+
### 3. Host setup checklist
|
|
252
252
|
|
|
253
|
-
- [ ]
|
|
253
|
+
- [ ] `@nuxt/ui`, `nuxt-vuefire`, `memi-board` in `modules`
|
|
254
254
|
- [ ] `memiBoard.auth` · `moderation` · **`seo`**
|
|
255
255
|
- [ ] `seo.siteUrl` / `seo.siteName` / `seo.defaultOgImage` / `seo.basePath`
|
|
256
|
-
- [ ]
|
|
257
|
-
- [ ]
|
|
258
|
-
- [ ]
|
|
259
|
-
- [ ] CSS `@source` (
|
|
260
|
-
- [ ] (
|
|
261
|
-
- [ ] Rules
|
|
262
|
-
- [ ]
|
|
256
|
+
- [ ] **SSR on** for public board paths, `ssr: false` only for writing and settings
|
|
257
|
+
- [ ] One `await useMemiBoard*Seo(…)` line on list and detail pages
|
|
258
|
+
- [ ] **No** host SEO API
|
|
259
|
+
- [ ] CSS `@source` (below)
|
|
260
|
+
- [ ] (optional) Read-post styling CSS (below)
|
|
261
|
+
- [ ] **Deploy** Rules and indexes
|
|
262
|
+
- [ ] Initial owner + first board
|
|
263
263
|
|
|
264
|
-
### 4. Tailwind `@source` (
|
|
264
|
+
### 4. Tailwind `@source` (required)
|
|
265
265
|
|
|
266
|
-
|
|
266
|
+
So classes inside the package SFCs are not purged:
|
|
267
267
|
|
|
268
268
|
```css
|
|
269
269
|
/* app/assets/css/main.css */
|
|
@@ -272,25 +272,25 @@ watch([categoryId, id], async ([category, slug]) => {
|
|
|
272
272
|
@source "../../../node_modules/memi-board/dist/runtime/components";
|
|
273
273
|
```
|
|
274
274
|
|
|
275
|
-
### 5.
|
|
275
|
+
### 5. Internationalization and content translation (optional)
|
|
276
276
|
|
|
277
|
-
[
|
|
277
|
+
The [internationalization and translation guide](docs/internationalization.md) covers copy-paste examples per mode, connecting the host language, when translations are saved, error handling, and the policy for keeping existing data.
|
|
278
278
|
|
|
279
|
-
|
|
|
279
|
+
| Goal | `memiBoard` config |
|
|
280
280
|
|---|---|
|
|
281
|
-
|
|
|
282
|
-
| UI
|
|
283
|
-
| UI
|
|
281
|
+
| Korean only | `locale: 'ko', i18n: { enabled: false }` |
|
|
282
|
+
| Multilingual UI only | `i18n: { enabled: true }, translation: { enabled: false }` |
|
|
283
|
+
| Multilingual UI and content | `i18n: { enabled: true }, translation: { enabled: true }` |
|
|
284
284
|
|
|
285
|
-
|
|
285
|
+
If omitted, both options are `true`; with `i18n.enabled: false`, content translation is also off. The UI is fixed to `locale` and content is shown in the original. Existing translation data is kept. Add `@nuxtjs/i18n` only when connecting the host's language switcher.
|
|
286
286
|
|
|
287
|
-
|
|
287
|
+
The board setting **Auto-translate posts on write** (`autoTranslatePosts`) is off by default. When on, the title, summary, and body of new posts are translated and saved together.
|
|
288
288
|
|
|
289
|
-
|
|
289
|
+
The translate button translates labels and descriptions as a temporary change; press **Save** to apply. The auto-translate toggle is off by default; when on, it translates **when settings are saved** and then saves. Retranslation is skipped when the text is unchanged and all translations exist. If internationalization or translation is turned off service-wide, auto-translation does not run either.
|
|
290
290
|
|
|
291
|
-
### 6.
|
|
291
|
+
### 6. Read-post styling (optional)
|
|
292
292
|
|
|
293
|
-
|
|
293
|
+
Post links in the list and mini components carry the `.memi-board-post-link` class, and the title text uses `text-inherit` to follow the link color. The package does not force a color, so to dim visited posts, just add a `:visited` style to the host CSS.
|
|
294
294
|
|
|
295
295
|
```css
|
|
296
296
|
/* app/assets/css/main.css */
|
|
@@ -303,57 +303,57 @@ a.memi-board-post-link:visited {
|
|
|
303
303
|
}
|
|
304
304
|
```
|
|
305
305
|
|
|
306
|
-
**`:visited
|
|
306
|
+
**`:visited` only allows `color`, and browsers even ignore `var()` and alpha values there** (to prevent history sniffing). So Nuxt UI tokens (`var(--ui-text-muted)`) cannot be used directly; specify literal colors for light and dark separately (the values above are Tailwind `neutral-500`/`neutral-400`).
|
|
307
307
|
|
|
308
|
-
|
|
308
|
+
Because it is the browser's native `:visited`, no server or DB storage is needed. However, navigation only via SPA client routing (`NuxtLink`) may not be recorded by the browser as a "real visit", so it is not fully reliable — if you need accurate read tracking, implement a separate mechanism that stores visited post IDs.
|
|
309
309
|
|
|
310
|
-
### 7. Security Rules +
|
|
310
|
+
### 7. Deploying Security Rules + indexes
|
|
311
311
|
|
|
312
|
-
1. [docs/firestore.rules.example](docs/firestore.rules.example) →
|
|
313
|
-
(`memiBoardPosts`/`memiBoardComments`/`memiBoardLikes`/`memiBoardReports`/`memiBoardSettings`/`memiBoardUsers` —
|
|
314
|
-
2. [docs/storage.rules.example](docs/storage.rules.example) → `storage.rules`
|
|
315
|
-
3. `firebase.json
|
|
316
|
-
(`*.appspot.com` vs `*.firebasestorage.app` — [firebase-setup.md](docs/firebase-setup.md)
|
|
317
|
-
4.
|
|
318
|
-
5.
|
|
312
|
+
1. Merge [docs/firestore.rules.example](docs/firestore.rules.example) → host `firestore.rules`
|
|
313
|
+
(`memiBoardPosts`/`memiBoardComments`/`memiBoardLikes`/`memiBoardReports`/`memiBoardSettings`/`memiBoardUsers` — all top-level flat collections)
|
|
314
|
+
2. Merge [docs/storage.rules.example](docs/storage.rules.example) → `storage.rules`
|
|
315
|
+
3. Check that the Storage `bucket` in `firebase.json` matches `vuefire.config.storageBucket`
|
|
316
|
+
(`*.appspot.com` vs `*.firebasestorage.app` — see [firebase-setup.md](docs/firebase-setup.md))
|
|
317
|
+
4. Deploy the composite indexes needed for list and per-author queries (below)
|
|
318
|
+
5. Deploy:
|
|
319
319
|
```bash
|
|
320
320
|
firebase deploy --only firestore:rules,storage,firestore:indexes
|
|
321
321
|
```
|
|
322
|
-
6. Storage
|
|
322
|
+
6. Grant Storage permission to read Firestore: approve the prompt to add the cross-service IAM role during deploy, and check that the Storage service account of the host Firebase project has `roles/firebaserules.firestoreServiceAgent`. See the [host setup procedure](docs/firebase-setup.md#firestore-read-access-for-storage-rules-required-host-app-setup). A successful CI deploy alone does not guarantee this permission.
|
|
323
323
|
|
|
324
|
-
0.45.1
|
|
324
|
+
Hosts updating to 0.45.1 should also check the [procedure for applying the new rules and migrating](docs/firebase-setup.md#0451-rule-changes-and-migrating-existing-hosts). Installing the package or pushing the site does not deploy Firebase rules.
|
|
325
325
|
|
|
326
|
-
**Rules
|
|
327
|
-
**Storage
|
|
326
|
+
**If Rules are only in the file and not deployed**, writing posts fails with `permission-denied` — the most common installation mistake.
|
|
327
|
+
**If Storage rules are deployed to a bucket other than the one the app uses**, body images fail with `storage/unauthorized`.
|
|
328
328
|
|
|
329
|
-
####
|
|
329
|
+
#### Composite indexes
|
|
330
330
|
|
|
331
|
-
[docs/firestore.indexes.json.example](docs/firestore.indexes.json.example)
|
|
331
|
+
Merge [docs/firestore.indexes.json.example](docs/firestore.indexes.json.example) into the host `firestore.indexes.json`. If you customized collection names (`memiBoard.postsCollection`, etc.), change the indexes' `collectionGroup` values to match.
|
|
332
332
|
|
|
333
|
-
> ⚠️ npm
|
|
333
|
+
> ⚠️ The npm package tarball also includes `docs/` (0.4.7+). For older versions, see `docs/` on GitHub.
|
|
334
334
|
|
|
335
|
-
### 8.
|
|
335
|
+
### 8. Creating the initial admin and categories
|
|
336
336
|
|
|
337
|
-
Rules
|
|
337
|
+
Rules prevent regular users from making themselves admins. So the first time, follow these steps:
|
|
338
338
|
|
|
339
|
-
1.
|
|
340
|
-
2. Firebase Console → Firestore
|
|
341
|
-
3.
|
|
342
|
-
- UID
|
|
343
|
-
4. `/board-settings
|
|
344
|
-
5.
|
|
339
|
+
1. After creating the board pages below, sign in with Google or Apple.
|
|
340
|
+
2. In Firebase Console → Firestore, find the `memiBoardUsers/{uid}` document.
|
|
341
|
+
3. Change that document's `role` to the string `owner`.
|
|
342
|
+
- You can also find the UID in Firebase Console → Authentication → Users.
|
|
343
|
+
4. Create the first category at `/board-settings`. Posts cannot be written without a category.
|
|
344
|
+
5. After that, change roles in `MemiBoardUsers` at `/board-users`.
|
|
345
345
|
|
|
346
|
-
>
|
|
346
|
+
> Only a trusted project operator should assign the initial admin, and only in Firebase Console. Do not put admin UIDs or bypass rules in client code.
|
|
347
347
|
|
|
348
|
-
##
|
|
348
|
+
## Usage
|
|
349
349
|
|
|
350
|
-
|
|
351
|
-
`import { MemiBoardEditor } from 'memi-board/runtime'
|
|
350
|
+
Routing and middleware are **owned by the host**. Components are auto-imported by the module (`MemiBoardList`, etc.).
|
|
351
|
+
`import { MemiBoardEditor } from 'memi-board/runtime'` **does not work**. Components are auto-imported by the module; only composables and types can be imported explicitly from `memi-board/runtime`.
|
|
352
352
|
|
|
353
|
-
|
|
353
|
+
Complete per-route examples are in the [host integration guide](docs/host-integration.md#7-list-new-post-and-detail-pages). Pass `boardId` to the list, editor, and detail.
|
|
354
354
|
|
|
355
355
|
```vue
|
|
356
|
-
<!-- free
|
|
356
|
+
<!-- free board list: create "free" in the board settings first -->
|
|
357
357
|
<template>
|
|
358
358
|
<MemiBoardList
|
|
359
359
|
board-id="free"
|
|
@@ -363,7 +363,7 @@ Rules는 일반 사용자가 스스로 관리자가 되는 것을 막는다. 따
|
|
|
363
363
|
```
|
|
364
364
|
|
|
365
365
|
```vue
|
|
366
|
-
<!--
|
|
366
|
+
<!-- New post: saved emits the new post's slug -->
|
|
367
367
|
<template>
|
|
368
368
|
<MemiBoardEditor
|
|
369
369
|
board-id="free"
|
|
@@ -372,11 +372,11 @@ Rules는 일반 사용자가 스스로 관리자가 되는 것을 막는다. 따
|
|
|
372
372
|
</template>
|
|
373
373
|
```
|
|
374
374
|
|
|
375
|
-
|
|
375
|
+
For the detail page, use `<MemiBoardDetail :board-id="boardId" :post-id="postId" />`. Convert the URL slug to the Firestore document ID with `resolvePostIdBySlug()`. When editing an existing post, the `saved` event emits the document ID, so keep the slug of the current detail URL. Follow the installation guide for the full wiring order, including SEO for public pages and guards for sign-in and admin pages.
|
|
376
376
|
|
|
377
|
-
###
|
|
377
|
+
### Admin pages
|
|
378
378
|
|
|
379
|
-
|
|
379
|
+
Global settings and user management use the `owner` or `admin` role. In a default install, do not pass `authorized`. Only the owner can assign the admin role, and changing the owner itself is done from a trusted server or the console. For per-board settings by an assigned manager, use `MemiBoardCategorySettings` and `canConfigureBoard(boardId)`.
|
|
380
380
|
|
|
381
381
|
```vue
|
|
382
382
|
<!-- app/pages/board-settings.vue -->
|
|
@@ -385,9 +385,9 @@ Rules는 일반 사용자가 스스로 관리자가 되는 것을 막는다. 따
|
|
|
385
385
|
</template>
|
|
386
386
|
```
|
|
387
387
|
|
|
388
|
-
|
|
388
|
+
The global board settings show an admin-only (`owner`/`admin`) **orphaned file and document cleanup**. Pressing **Find orphaned items** reads all posts, comments, likes, and Storage, and lists comments and files whose parent post is missing, images of deleted comments, and likes whose target post or comment is missing. After reviewing the path, type, and file size, you can delete items individually or select all. Files updated within the last 24 hours and files referenced by existing bodies, translations, attachments, or comments are preserved. Report records and `body` subdocuments without a post are not cleaned up by this feature.
|
|
389
389
|
|
|
390
|
-
|
|
390
|
+
To use it, apply the admin read and delete permissions from the [Firestore rules example](docs/firestore.rules.example) and the [Storage rules example](docs/storage.rules.example) to the host project. If you use custom collections, update the rule paths as well. The scan reads all data including bodies, so it incurs read costs and time depending on data size. Deletion targets are pinned to an internal snapshot, and deletion stops if the account, settings, or file generation changes after the scan. Items are re-fetched from the server right before deletion, and documents check for the parent's existence in a transaction. File deletion and Firestore writes are not atomic, so run it when board usage is low. Failed items stay in the list and can be retried.
|
|
391
391
|
|
|
392
392
|
```vue
|
|
393
393
|
<!-- app/pages/board-users.vue -->
|
|
@@ -396,116 +396,116 @@ Rules는 일반 사용자가 스스로 관리자가 되는 것을 막는다. 따
|
|
|
396
396
|
</template>
|
|
397
397
|
```
|
|
398
398
|
|
|
399
|
-
|
|
399
|
+
Use `authorized` only when integrating the host's own site-admin system. This prop only allows the UI to be shown and does not bypass Firestore Rules. Even then, you must register those users as board admin/owner or change the host Rules to match the same admin system. Adding host route guards is recommended so admin URLs are not exposed to regular users.
|
|
400
400
|
|
|
401
|
-
###
|
|
401
|
+
### Provided API
|
|
402
402
|
|
|
403
|
-
|
|
403
|
+
**Components (auto-import):**
|
|
404
404
|
`MemiBoardList`, `MemiBoardDetail`, `MemiBoardEditor`, `MemiBoardCommentList`, `MemiBoardCommentForm`, `MemiBoardAttachments`, `MemiBoardSignIn`, `MemiBoardSettings`, `MemiBoardUsers`, `MemiBoardVersionHistory`
|
|
405
405
|
|
|
406
|
-
**List props:** `boardId`(
|
|
406
|
+
**List props:** `boardId` (required), `pageSize`, `category` (former name), `postLinkBase`, `getPostLink(post)`, `linkBase` (deprecated)
|
|
407
407
|
|
|
408
|
-
**Editor props:** `boardId`, `postId`(
|
|
409
|
-
|
|
408
|
+
**Editor props:** `boardId`, `postId` (edit), `fixedCategory` (hides the selector when the path fixes it)
|
|
409
|
+
The body uses Nuxt UI **`UEditor`** (TipTap, `content-type="html"`). The host's `@nuxt/ui` 4.x must include Editor.
|
|
410
410
|
|
|
411
|
-
|
|
411
|
+
**Editor images:** From the toolbar you can add a file from the device or an external HTTPS image URL; pasted and dropped files are uploaded to Firebase Storage. External URLs are not copied to Storage and stop displaying if the original is deleted or hotlinking is blocked. The first body image, whether uploaded or external, is used as the list and share preview image.
|
|
412
412
|
|
|
413
|
-
`memiBoardPosts/{postId}/images/*` + `.../images/thumbnails/*` (
|
|
414
|
-
|
|
413
|
+
`memiBoardPosts/{postId}/images/*` + `.../images/thumbnails/*` (optimized image + thumbnail).
|
|
414
|
+
The post document may not exist yet, and pastes arrive as `image/png` and similar. The example Storage rules limit writes to **signed-in + under 5MiB + `image/*` + ownership check**. Existing posts can only be modified by the author or a global admin/owner. Before the post is saved, the file's `customMetadata.ownerUid` must equal the signed-in UID, and when overwriting an existing file, the existing metadata must match too. Image uploads also check the post's existence and ownership in Firestore. The rules must be deployed to **the same bucket as the app's `storageBucket`**.
|
|
415
415
|
|
|
416
|
-
|
|
417
|
-
|
|
416
|
+
Body images can be selected up to 25MB, based on phone originals. Over 5MB, the browser optimizes them to at most 2560px · JPEG 85%; if the result is still over 5MB, it shrinks once more to 2048px · 75% before uploading. Images of 5MB or less are kept as is.
|
|
417
|
+
The body HTML holds the original URL. Clean up files abandoned during editing with a host scheduler (compare body references with `extractEditorImageUrls`).
|
|
418
418
|
|
|
419
419
|
**composables:**
|
|
420
420
|
`useMemiBoardAuth`, `useMemiBoardPosts`, `useMemiBoardComments`, `useMemiBoardStorage`, `useMemiBoardModeration`, `useMemiBoardSettings`, `useMemiBoardUsers`, `useMemiBoardViews`
|
|
421
|
-
**SEO (auto-import,
|
|
421
|
+
**SEO (auto-import, host pipeline):**
|
|
422
422
|
`useMemiBoardListSeo`, `useMemiBoardPostSeo`
|
|
423
|
-
**SEO fetch (
|
|
423
|
+
**SEO fetch (when needed):**
|
|
424
424
|
`fetchPublicListForSeo`, `fetchPublicPostForSeo`, `resolvePublicSeoDb`
|
|
425
425
|
|
|
426
|
-
|
|
426
|
+
Comments are fetched chronologically, 10 at a time with a cursor; when the actual `More comments` button enters the viewport, the next page is loaded automatically after 1 second. Only after all existing comments are loaded does it subscribe in real time to the single latest comment. Replies are fetched 5 at a time, and after all are loaded it subscribes in real time to the single latest reply; nesting is displayed up to 2 levels deep.
|
|
427
427
|
|
|
428
|
-
###
|
|
428
|
+
### Boards (formerly categories)
|
|
429
429
|
|
|
430
|
-
-
|
|
431
|
-
-
|
|
432
|
-
- `MemiBoardSettings
|
|
433
|
-
- `boardId
|
|
430
|
+
- Document: `memiBoardSettings/{boardId}` → `{ label, listView, editorType, writeRole, commentWriteRole, visibility, allowedStaffUids, allowedManagerUids, locale, autoTranslate, autoTranslatePosts, i18n, i18nSource, order, updatedAt }`
|
|
431
|
+
- With no settings, the list is empty; no default board is created automatically
|
|
432
|
+
- In `MemiBoardSettings`, admins save, delete, reorder boards and manage the list view and minimum write role per board
|
|
433
|
+
- `boardId` is both the document ID and the `boardId` reference on each post (`memiBoardPosts`), so changing it after creation is not recommended
|
|
434
434
|
|
|
435
|
-
###
|
|
435
|
+
### Sign-in providers
|
|
436
436
|
|
|
437
|
-
`auth.providers`
|
|
437
|
+
`auth.providers` defaults to `['google', 'apple']`; `'emailPassword'` can be added.
|
|
438
438
|
|
|
439
|
-
- **Google:**
|
|
440
|
-
- **Apple:** Services ID
|
|
439
|
+
- **Google:** enable it under Sign-in method in the console
|
|
440
|
+
- **Apple:** needs extra setup such as a Services ID and key → [docs/firebase-setup.md#apple](docs/firebase-setup.md#apple)
|
|
441
441
|
|
|
442
|
-
###
|
|
442
|
+
### Version history
|
|
443
443
|
|
|
444
|
-
`<MemiBoardVersionHistory />`
|
|
444
|
+
Place `<MemiBoardVersionHistory />` on a page to show package feature changes to visitors.
|
|
445
445
|
|
|
446
446
|
## Security Model
|
|
447
447
|
|
|
448
|
-
|
|
448
|
+
With no server, **Rules are everything**.
|
|
449
449
|
|
|
450
|
-
|
|
450
|
+
Permission summary:
|
|
451
451
|
|
|
452
|
-
|
|
|
452
|
+
| Action | Condition |
|
|
453
453
|
|------|------|
|
|
454
|
-
|
|
|
455
|
-
|
|
|
456
|
-
|
|
|
457
|
-
|
|
|
458
|
-
|
|
|
459
|
-
|
|
|
460
|
-
|
|
|
461
|
-
|
|
|
462
|
-
|
|
|
454
|
+
| Read post | Public (`listed` · published), or author · admin · assigned staff. Hidden boards: author · admin · assigned staff only |
|
|
455
|
+
| Read comment | Public (`listed`), or author · admin · assigned staff · anyone who can read the parent post |
|
|
456
|
+
| Create post | Signed in + meets the board's `writeRole` + `moderationStatus == 'approved'` |
|
|
457
|
+
| Create comment | Signed in + meets the board's `commentWriteRole`. **Hidden boards: assigned staff only** (`commentWriteRole` ignored) |
|
|
458
|
+
| Edit/delete post | Author, or that board's admin · assigned staff |
|
|
459
|
+
| Delete comment | Comment author · post author · that board's admin · assigned staff |
|
|
460
|
+
| Clean up related likes | Like author, or parent post author · that board's admin · assigned staff. Comment likes can also be cleaned up by the comment author |
|
|
461
|
+
| Upload/delete file | For existing posts, the post author or a global admin/owner. If there is no post document, verified by the file's `ownerUid` |
|
|
462
|
+
| Role management | The owner can assign other admins. Admins manage manager · staff · user. Changing your own role or the owner is forbidden from the client |
|
|
463
463
|
|
|
464
|
-
|
|
464
|
+
When editing a post, `boardId` and `authorUid` cannot change, and `listed` follows the visibility transition rules. A visibility change records `visibilityTransition`, which locks new post/comment creation and regular post edits, and then completes. On failure, save again with the same target visibility. So that the post and its body can be created in one batch, body writes verify the parent post with `getAfter()`. Report records are kept even after a post is deleted.
|
|
465
465
|
|
|
466
|
-
|
|
466
|
+
File reads and listing in the Storage example are public. The Firestore read restrictions for hidden boards and private drafts do not extend to attachments. Storage admin/owner permissions are global, regardless of board assignment. Managing other users' files as assigned staff/manager requires a separate server API. For the detailed reasoning and how to migrate, see the [rule change notes](docs/firebase-setup.md#0451-rule-changes-and-migrating-existing-hosts).
|
|
467
467
|
|
|
468
|
-
|
|
468
|
+
**Known limitation:** AI moderation is client-only. Writing directly via the SDK bypasses it. For critical services, the host should add Cloud Functions validation (outside the package's scope).
|
|
469
469
|
|
|
470
|
-
## Firebase AI Logic (
|
|
470
|
+
## Firebase AI Logic (moderation)
|
|
471
471
|
|
|
472
|
-
|
|
472
|
+
Flow: **local blocklist → Gemini structured JSON** → if flagged, nothing is saved.
|
|
473
473
|
|
|
474
|
-
|
|
|
474
|
+
| Option | Default | Description |
|
|
475
475
|
|------|------|------|
|
|
476
|
-
| `enabled` | `true` |
|
|
477
|
-
| `model` | `gemini-3.5-flash-lite` | Firebase AI Logic
|
|
478
|
-
| `onError` | `'allow'` | AI
|
|
479
|
-
| `localBlocklist` | `[]` |
|
|
480
|
-
| `useLimitedUseAppCheckTokens` | `true` | `false
|
|
481
|
-
| `blockBanThreshold` | `10` |
|
|
482
|
-
| `blockBanDecayMs` | `86400000` |
|
|
476
|
+
| `enabled` | `true` | When off, local only / or close to letting everything through |
|
|
477
|
+
| `model` | `gemini-3.5-flash-lite` | A model name supported by Firebase AI Logic |
|
|
478
|
+
| `onError` | `'allow'` | On AI failure, `'block'` rejects the save |
|
|
479
|
+
| `localBlocklist` | `[]` | Strings added to the default list |
|
|
480
|
+
| `useLimitedUseAppCheckTokens` | `true` | When `false`, uses regular App Check session tokens |
|
|
481
|
+
| `blockBanThreshold` | `10` | Restricts posts and comments once accumulated content blocks (local/ai) reach this. `0` = off |
|
|
482
|
+
| `blockBanDecayMs` | `86400000` | Interval for removing one warning (default 24h) |
|
|
483
483
|
|
|
484
|
-
|
|
485
|
-
10
|
|
484
|
+
As content blocks accumulate, `moderationBlockCount` / `moderationBlockAt` are recorded in `memiBoardUsers/{uid}`.
|
|
485
|
+
At 10, the user cannot write for about a day (one is removed every 24h). In the example Rules, users can only **+1** their own count; decreasing or resetting is for admins.
|
|
486
486
|
|
|
487
|
-
###
|
|
487
|
+
### Admin moderation bypass
|
|
488
488
|
|
|
489
|
-
|
|
489
|
+
At the bottom of the post form (`Editor.vue`), above the submit button, a "Ignore profanity filter" switch is shown only to `isAdmin` users. Submitting with it on skips both the local blocklist and AI moderation and saves (`via: 'admin-override'`; the warning count does not increase either). The comment form (`CommentForm.vue`) does not have it.
|
|
490
490
|
|
|
491
|
-
|
|
491
|
+
When calling directly, bypass with `useMemiBoardModeration().checkText(text, { skipFilter: true })`. `skipFilter` trusts whatever value the host passes, so deciding whether the user is an admin is the caller's (host's) responsibility.
|
|
492
492
|
|
|
493
|
-
### AI
|
|
493
|
+
### AI writing assistant usage limit
|
|
494
494
|
|
|
495
|
-
|
|
495
|
+
The editor's AI writing assistant can be used up to **10 times** per signed-in user in each 24-hour window. Before the call, only an `aiWritingPending` reservation is made; when the model call succeeds, it is committed to `aiWritingCount`. If the call fails, only the reservation is rolled back. Committed counts never go down. Reservation and commit update `memiBoardUsers/{uid}` in a Firestore transaction, so the total cannot exceed 10 even when several tabs run at once.
|
|
496
496
|
|
|
497
|
-
|
|
497
|
+
The host must merge and deploy the `memiBoardUsers/{uid}` rules from the latest `docs/firestore.rules.example` into its actual Firestore Rules. Reservation fields were added in 0.47.1. Without deploying the rules, the new package's AI writing count updates are rejected, or, while only the old rules are in place, clients can modify the counter arbitrarily and bypass the limit.
|
|
498
498
|
|
|
499
|
-
|
|
499
|
+
Enable **AI Logic (Gemini Developer API)** in the console. New projects may get 404 for the old `gemini-2.5-flash` → use 3.x Flash / Flash-Lite.
|
|
500
500
|
|
|
501
|
-
### App Check (
|
|
501
|
+
### App Check (effectively required with AI Enforce)
|
|
502
502
|
|
|
503
|
-
`firebaseml` / AI Logic
|
|
503
|
+
If `firebaseml` / AI Logic is **Enforced**, `generateContent` fails without an App Check token. memi-board does not initialize App Check. The default `moderation.useLimitedUseAppCheckTokens: true` requires **limited-use tokens**. reCAPTCHA v3's `ReCaptchaV3Provider` does not produce these tokens.
|
|
504
504
|
|
|
505
|
-
1. reCAPTCHA Enterprise
|
|
506
|
-
2. Firebase
|
|
507
|
-
3.
|
|
508
|
-
4.
|
|
505
|
+
1. Create a **score-based** web key in reCAPTCHA Enterprise. Do not use checkbox keys or the v3 provider. Add only the real site addresses as domains. The site key is a public value, so keep it in client config. Never put the secret key in the client. Firebase exchanges the token.
|
|
506
|
+
2. Firebase console → App Check → register that site key on the web app. The registered web app's `appId` must equal `vuefire.config.appId`.
|
|
507
|
+
3. Include **Firebase App Check API** in the browser API key's API restrictions. Do not remove previously allowed APIs.
|
|
508
|
+
4. Exchange the Enterprise token in a host plugin after mount. In an SPA, `document.body` is empty before the first paint, so the hidden widget of `ReCaptchaEnterpriseProvider` fails with `appCheck/recaptcha-error`.
|
|
509
509
|
|
|
510
510
|
```ts
|
|
511
511
|
// app/plugins/app-check.client.ts
|
|
@@ -523,7 +523,7 @@ export default defineNuxtPlugin((nuxtApp) => {
|
|
|
523
523
|
provider: new CustomProvider({
|
|
524
524
|
getToken: () => exchangeEnterpriseToken(app, siteKey),
|
|
525
525
|
}),
|
|
526
|
-
//
|
|
526
|
+
// Limited-use tokens are not refreshed as a session. The AI SDK calls getToken per request.
|
|
527
527
|
isTokenAutoRefreshEnabled: false,
|
|
528
528
|
})
|
|
529
529
|
})
|
|
@@ -547,7 +547,7 @@ function loadEnterprise(siteKey: string) {
|
|
|
547
547
|
const ready = () => {
|
|
548
548
|
const enterprise = window.grecaptcha?.enterprise
|
|
549
549
|
if (!enterprise) {
|
|
550
|
-
reject(new Error('reCAPTCHA Enterprise
|
|
550
|
+
reject(new Error('reCAPTCHA Enterprise failed to load.'))
|
|
551
551
|
return
|
|
552
552
|
}
|
|
553
553
|
enterprise.ready(() => resolve())
|
|
@@ -560,7 +560,7 @@ function loadEnterprise(siteKey: string) {
|
|
|
560
560
|
script.src = `https://www.google.com/recaptcha/enterprise.js?render=${encodeURIComponent(siteKey)}`
|
|
561
561
|
script.async = true
|
|
562
562
|
script.onload = ready
|
|
563
|
-
script.onerror = () => reject(new Error('reCAPTCHA Enterprise
|
|
563
|
+
script.onerror = () => reject(new Error('Failed to load the reCAPTCHA Enterprise script.'))
|
|
564
564
|
document.head.appendChild(script)
|
|
565
565
|
})
|
|
566
566
|
return enterpriseReady
|
|
@@ -569,7 +569,7 @@ function loadEnterprise(siteKey: string) {
|
|
|
569
569
|
async function exchangeEnterpriseToken(app: FirebaseApp, siteKey: string) {
|
|
570
570
|
await loadEnterprise(siteKey)
|
|
571
571
|
const enterprise = window.grecaptcha?.enterprise
|
|
572
|
-
if (!enterprise) throw new Error('reCAPTCHA Enterprise
|
|
572
|
+
if (!enterprise) throw new Error('reCAPTCHA Enterprise failed to load.')
|
|
573
573
|
const recaptchaToken = await enterprise.execute(siteKey, { action: 'fire_app_check' })
|
|
574
574
|
const { projectId, appId, apiKey } = app.options
|
|
575
575
|
const response = await fetch(
|
|
@@ -590,68 +590,68 @@ async function exchangeEnterpriseToken(app: FirebaseApp, siteKey: string) {
|
|
|
590
590
|
}
|
|
591
591
|
```
|
|
592
592
|
|
|
593
|
-
|
|
593
|
+
To use regular session tokens, change to `useLimitedUseAppCheckTokens: false` and remove `limited_use` from the exchange body.
|
|
594
594
|
|
|
595
|
-
5. App Check
|
|
596
|
-
6.
|
|
595
|
+
5. Enable App Check enforcement **only for Firebase AI Logic**. Leave Cloud Storage and Cloud Firestore on monitoring. Only AI Logic accepts limited-use tokens. If you enforce it on Storage or Firestore, image uploads and post saves are rejected with `storage/unauthorized` or permission errors.
|
|
596
|
+
6. Locally, register the UUID printed by `self.FIREBASE_APPCHECK_DEBUG_TOKEN = true` as a debug token in the console. Do not enable the debug token in production builds.
|
|
597
597
|
|
|
598
|
-
##
|
|
598
|
+
## Checklist for adding it to another project
|
|
599
599
|
|
|
600
|
-
- [ ]
|
|
600
|
+
- [ ] Same stack: Nuxt 4 + `@nuxt/ui` 4 + `nuxt-vuefire` + Firebase
|
|
601
601
|
- [ ] `pnpm add memi-board @nuxt/ui nuxt-vuefire vuefire firebase`
|
|
602
|
-
- [ ]
|
|
602
|
+
- [ ] `@nuxt/ui`, `nuxt-vuefire`, `memi-board` in `modules`
|
|
603
603
|
- [ ] `memiBoard` (`boardId` · `auth` · `moderation` · **`seo`**)
|
|
604
|
-
- [ ]
|
|
605
|
-
- [ ]
|
|
606
|
-
- [ ]
|
|
604
|
+
- [ ] **SSR on** for public list and detail, `ssr: false` only for writing and settings (no all-CSR routes)
|
|
605
|
+
- [ ] `await useMemiBoardListSeo` / `useMemiBoardPostSeo` on list and detail pages
|
|
606
|
+
- [ ] No host SEO API
|
|
607
607
|
- [ ] CSS `@source` …/memi-board/dist/runtime/components
|
|
608
|
-
- [ ] (
|
|
609
|
-
- [ ] Rules
|
|
610
|
-
- [ ]
|
|
611
|
-
- [ ]
|
|
612
|
-
- [ ]
|
|
613
|
-
- [ ]
|
|
614
|
-
- [ ]
|
|
615
|
-
- [ ] (
|
|
616
|
-
- [ ] `pnpm why vuefire`
|
|
608
|
+
- [ ] (optional) `.memi-board-post-link:visited` read-post styling CSS
|
|
609
|
+
- [ ] Merge the Rules examples + **deploy**
|
|
610
|
+
- [ ] Check the Firestore read IAM role on the Storage service account ([host setup](docs/firebase-setup.md#firestore-read-access-for-storage-rules-required-host-app-setup))
|
|
611
|
+
- [ ] Deploy indexes when using category / listed queries
|
|
612
|
+
- [ ] Write pages and middleware yourself (or copy from the playground)
|
|
613
|
+
- [ ] After the first sign-in, set `memiBoardUsers/{uid}.role = 'owner'` in Firebase Console
|
|
614
|
+
- [ ] Create the first category on the settings page
|
|
615
|
+
- [ ] (moderation) Enable AI Logic + recommended model + App Check
|
|
616
|
+
- [ ] Check for duplicate instances with `pnpm why vuefire`
|
|
617
617
|
|
|
618
|
-
|
|
618
|
+
It is fine if the same Firebase project has a host app user collection — board roles are **exclusive to `memiBoardUsers`**.
|
|
619
619
|
|
|
620
|
-
##
|
|
620
|
+
## Troubleshooting
|
|
621
621
|
|
|
622
622
|
### `useCurrentUser() called before the VueFireAuth module...`
|
|
623
623
|
|
|
624
|
-
`vuefire.auth.enabled`
|
|
624
|
+
`vuefire.auth.enabled` is not set, or `vuefire`/`firebase` is installed twice. `pnpm why vuefire` → reduce to one. The module adds vue/vuefire/firebase to `vite.resolve.dedupe`, but **the host's peer installation** takes precedence.
|
|
625
625
|
|
|
626
|
-
###
|
|
626
|
+
### Category filter query error / index link
|
|
627
627
|
|
|
628
|
-
|
|
628
|
+
Create the composite index the console suggests, or deploy the `firestore.indexes.json` above.
|
|
629
629
|
|
|
630
|
-
### AI
|
|
630
|
+
### AI moderation fails / post not published (`onError: 'block'`)
|
|
631
631
|
|
|
632
|
-
App Check
|
|
632
|
+
Check the App Check token, matching `appId`, model name (3.x), AI Logic enabled, and network. Look for the `moderation AI failed` warning in the browser console.
|
|
633
633
|
|
|
634
|
-
###
|
|
634
|
+
### Editor images `storage/unauthorized`
|
|
635
635
|
|
|
636
|
-
|
|
636
|
+
If body pastes or uploads fail with `User does not have permission to access 'memiBoardPosts/…/images/…'`:
|
|
637
637
|
|
|
638
|
-
1.
|
|
639
|
-
2. `vuefire.config.storageBucket
|
|
640
|
-
3.
|
|
641
|
-
4.
|
|
642
|
-
5.
|
|
638
|
+
1. Check that you are signed in
|
|
639
|
+
2. Check that `vuefire.config.storageBucket` and `firebase.json` `storage.bucket` have **the same name**. Older projects may split between `*.appspot.com` (app) and `*.firebasestorage.app` (CLI default) — deploy the rules to the one the app uses.
|
|
640
|
+
3. Check that the host `storage.rules` has the `memiBoardPosts/{postId}/images` rules from `docs/storage.rules.example`. Writes must be `image/*` (pasted PNG).
|
|
641
|
+
4. Check that the file has `customMetadata.ownerUid` before the post is saved. Packages 0.45.1 and later add it automatically. For existing posts, check that the user is the author or a global admin/owner.
|
|
642
|
+
5. Image rules also query Firestore, so check the [cross-service IAM role](docs/firebase-setup.md#firestore-read-access-for-storage-rules-required-host-app-setup), run `firebase deploy --only storage`, and try uploading again.
|
|
643
643
|
|
|
644
|
-
###
|
|
644
|
+
### `storage/unauthorized` on attachments such as ZIP
|
|
645
645
|
|
|
646
|
-
`memiBoardPosts/{postId}/attachments
|
|
646
|
+
If `memiBoardPosts/{postId}/attachments/...` fails, first check the sign-in state, that the attachment rules are deployed to the bucket the app uses, and the **under 20MiB per file** condition. The example rules do not block ZIP MIME types or `.exe.zip` names.
|
|
647
647
|
|
|
648
|
-
|
|
648
|
+
If it still fails with those conditions met, check that **the Storage service account of the host Firebase project has the Firestore read role**. Image and attachment rules query Firestore in `canWritePostFile` via `postExists` · `isPostOwner` · `isBoardStaff`. Without read permission, even the existence check for a new post's document can fail. Set `roles/firebaserules.firestoreServiceAgent` following the [host setup procedure](docs/firebase-setup.md#firestore-read-access-for-storage-rules-required-host-app-setup).
|
|
649
649
|
|
|
650
|
-
###
|
|
650
|
+
### Post deletion `storage/unauthorized`
|
|
651
651
|
|
|
652
|
-
|
|
652
|
+
If an author deleting their own post fails with `User does not have permission to access 'memiBoardPosts/{postId}'`, it is not the file deletion but **folder listing (`listAll`)** that is blocked.
|
|
653
653
|
|
|
654
|
-
|
|
654
|
+
The request is `prefix=memiBoardPosts/{postId}/` (trailing slash). `match /memiBoardPosts/{postId} { allow list }` alone does not match this prefix. The host `storage.rules` needs a block like the example:
|
|
655
655
|
|
|
656
656
|
```
|
|
657
657
|
match /memiBoardPosts/{postId}/{allPaths=**} {
|
|
@@ -660,69 +660,69 @@ match /memiBoardPosts/{postId}/{allPaths=**} {
|
|
|
660
660
|
}
|
|
661
661
|
```
|
|
662
662
|
|
|
663
|
-
`canDeletePostFile
|
|
663
|
+
Also merge `canDeletePostFile` and related functions from the latest `docs/storage.rules.example`. Do not use the old condition (`!postExists`) that allowed deletion with just a sign-in when the post document is missing.
|
|
664
664
|
|
|
665
|
-
`write`
|
|
665
|
+
Do not put `write` in this block (size and `image/*` limits are handled by the `images`/`attachments`/`comment-images` matches). Run `firebase deploy --only storage` and delete again. The package empties only `images/` · `attachments/` · `comment-images/` by listing. Comment images are deleted while the post document still exists. If the post document disappears first, the post author can no longer delete comment files uploaded by other commenters.
|
|
666
666
|
|
|
667
|
-
|
|
667
|
+
If listing succeeds but **individual file deletion** is denied, also check the same Firestore read IAM role as in the [attachment error](#storageunauthorized-on-attachments-such-as-zip) above. Deletion rules also read post ownership and user roles from Firestore.
|
|
668
668
|
|
|
669
|
-
### monorepo `link:` / `file:`
|
|
669
|
+
### Developing with monorepo `link:` / `file:`
|
|
670
670
|
|
|
671
|
-
|
|
671
|
+
Prepare the package dependencies and `dist` before linking.
|
|
672
672
|
|
|
673
673
|
```bash
|
|
674
|
-
# memi-board
|
|
674
|
+
# memi-board repository
|
|
675
675
|
pnpm install
|
|
676
676
|
pnpm build
|
|
677
677
|
|
|
678
|
-
#
|
|
678
|
+
# host repository
|
|
679
679
|
pnpm add link:../memi-board
|
|
680
680
|
```
|
|
681
681
|
|
|
682
|
-
|
|
|
682
|
+
| What you change | When it takes effect |
|
|
683
683
|
|-----------|------|
|
|
684
|
-
| `src/components/*` |
|
|
685
|
-
| `useMemiBoard*Seo` (`src/composables/useMemiBoardSeo.ts`) |
|
|
686
|
-
| `src/module.ts` ·
|
|
684
|
+
| `src/components/*` | The module reads the linked source directly → **immediately** (dev HMR) |
|
|
685
|
+
| `useMemiBoard*Seo` (`src/composables/useMemiBoardSeo.ts`) | The host Nuxt compiles the source → **immediately** (may need a dev restart) |
|
|
686
|
+
| `src/module.ts` · other core composables (`dist`) | After **`pnpm build`**, restart the host dev server |
|
|
687
687
|
|
|
688
|
-
SEO
|
|
688
|
+
The SEO composables use the host Nuxt instance via `#imports`. `nuxt/app` is not bundled into `dist/index.js`, so linking does not cause duplicate Nuxt loading (E1001).
|
|
689
689
|
|
|
690
|
-
|
|
690
|
+
The module automatically dedupes Vue · Vue Router · VueFire · Firebase · TipTap. Before deploying, switch dependencies back to **npm versions**.
|
|
691
691
|
|
|
692
|
-
|
|
692
|
+
To try new Tailwind classes right away while linked, add one more source path to the host CSS.
|
|
693
693
|
|
|
694
694
|
```css
|
|
695
695
|
@source "../../../node_modules/memi-board/src/components";
|
|
696
696
|
@source "../../../node_modules/memi-board/dist/runtime/components";
|
|
697
697
|
```
|
|
698
698
|
|
|
699
|
-
### OG
|
|
699
|
+
### OG meta is empty or link previews do not work
|
|
700
700
|
|
|
701
|
-
1.
|
|
702
|
-
2.
|
|
701
|
+
1. Check that public paths are not turned off with `ssr: false` (`routeRules`)
|
|
702
|
+
2. Check that the page calls `await useMemiBoardListSeo` / `useMemiBoardPostSeo`
|
|
703
703
|
3. `memiBoard.seo.siteUrl` · `siteName` · `defaultOgImage`
|
|
704
|
-
4. `curl` / view-source
|
|
705
|
-
5.
|
|
706
|
-
6.
|
|
704
|
+
4. Check that `og:title` is in `curl` / view-source (don't look only at Elements)
|
|
705
|
+
5. Check that the post is `listed` / not on a hidden board (hidden boards are noindex)
|
|
706
|
+
6. Re-scrape with the deployed URL (KakaoTalk / Facebook caches)
|
|
707
707
|
|
|
708
|
-
##
|
|
708
|
+
## Local development (this repo)
|
|
709
709
|
|
|
710
710
|
```bash
|
|
711
711
|
pnpm install
|
|
712
712
|
pnpm build
|
|
713
713
|
pnpm dev:prepare
|
|
714
714
|
pnpm dev # playground
|
|
715
|
-
pnpm build:watch #
|
|
715
|
+
pnpm build:watch # rebuild dist when src changes
|
|
716
716
|
```
|
|
717
717
|
|
|
718
|
-
|
|
718
|
+
Emulators:
|
|
719
719
|
|
|
720
720
|
```bash
|
|
721
721
|
firebase emulators:start --project demo-memi-board
|
|
722
722
|
NUXT_PUBLIC_USE_EMULATORS=1 pnpm dev
|
|
723
723
|
```
|
|
724
724
|
|
|
725
|
-
|
|
725
|
+
Permission regression tests for the cleanup feature (requires Java 21+ and the Firebase CLI):
|
|
726
726
|
|
|
727
727
|
```bash
|
|
728
728
|
pnpm build
|
|
@@ -730,28 +730,28 @@ pnpm test:cleanup
|
|
|
730
730
|
firebase emulators:exec --project demo-memi-cleanup-security --only auth,firestore,storage 'node --test tests/cleanup-rules.test.mjs'
|
|
731
731
|
```
|
|
732
732
|
|
|
733
|
-
|
|
733
|
+
Notice (pinned) permission regression tests:
|
|
734
734
|
|
|
735
735
|
```bash
|
|
736
736
|
firebase emulators:exec --project demo-memi-notice-rules --only auth,firestore 'node --test tests/notice-rules.test.mjs'
|
|
737
737
|
```
|
|
738
738
|
|
|
739
|
-
## npm
|
|
739
|
+
## Publishing to npm
|
|
740
740
|
|
|
741
|
-
npm Trusted Publishing(OIDC)
|
|
741
|
+
npm Trusted Publishing (OIDC) is used. No long-lived `NPM_TOKEN` or local OTP is stored.
|
|
742
742
|
|
|
743
|
-
1. `package.json`, `CHANGELOG.md`,
|
|
744
|
-
2. `main`
|
|
745
|
-
3.
|
|
743
|
+
1. Update the version in `package.json`, `CHANGELOG.md`, and the in-app `versionHistory`, then push to `main`.
|
|
744
|
+
2. Pushing to `main` runs `.github/workflows/publish.yml`.
|
|
745
|
+
3. After checking whether the version is already published, it builds, type-checks, runs regression tests, Firebase security rules tests, and a package dry-run, then publishes to npm.
|
|
746
746
|
|
|
747
|
-
npm
|
|
747
|
+
In the npm package settings, register the Trusted Publisher as GitHub repository `fkkmemi/memi-board`, workflow `publish.yml`. If you assign approvers to the GitHub Environment `npm-publish`, you can approve once more after the workflow runs and before the actual npm publish.
|
|
748
748
|
|
|
749
|
-
##
|
|
749
|
+
## Version · License
|
|
750
750
|
|
|
751
751
|
- UI: `<MemiBoardVersionHistory />`
|
|
752
|
-
-
|
|
752
|
+
- Details: [CHANGELOG.md](CHANGELOG.md)
|
|
753
753
|
- **MIT**
|
|
754
754
|
|
|
755
|
-
### 0.46
|
|
755
|
+
### 0.46 roles and per-service names
|
|
756
756
|
|
|
757
|
-
5
|
|
757
|
+
For the 5 roles, per-board manager scope, `roleLabels`, and the host migration procedure, see [role migration](docs/roles-migration.md). Use `canConfigureBoard()` for settings access.
|