@commonpub/layer 0.4.6 → 0.4.7

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/pages/index.vue CHANGED
@@ -157,30 +157,38 @@ async function handleHubJoin(hubSlug: string): Promise<void> {
157
157
  <div v-if="activeContest?.endDate" class="cpub-hero-aside">
158
158
  <CountdownTimer :target-date="activeContest.endDate" />
159
159
  </div>
160
- <!-- Large Town Square logo — shown for Agora theme when no contest -->
160
+ <!-- Large textured Town Square logo — shown for Agora theme when no contest -->
161
161
  <div v-if="!activeContest" class="cpub-hero-aside cpub-hero-logo-aside">
162
162
  <svg class="cpub-hero-logo" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 200" aria-hidden="true">
163
- <rect x="18" y="18" width="72" height="72" fill="none" stroke="currentColor" stroke-width="5.5" stroke-linejoin="round"/>
164
- <rect x="110" y="18" width="72" height="72" fill="none" stroke="currentColor" stroke-width="5.5" stroke-linejoin="round"/>
165
- <rect x="18" y="110" width="72" height="72" fill="none" stroke="currentColor" stroke-width="5.5" stroke-linejoin="round"/>
166
- <rect x="110" y="110" width="72" height="72" fill="none" stroke="currentColor" stroke-width="5.5" stroke-linejoin="round"/>
167
- <rect x="90" y="90" width="20" height="20" fill="var(--accent, #3d8b5e)"/>
168
- <rect x="84" y="84" width="6" height="6" fill="var(--accent, #3d8b5e)" opacity="0.6"/>
169
- <rect x="110" y="84" width="6" height="6" fill="var(--accent, #3d8b5e)" opacity="0.6"/>
170
- <rect x="84" y="110" width="6" height="6" fill="var(--accent, #3d8b5e)" opacity="0.6"/>
171
- <rect x="110" y="110" width="6" height="6" fill="var(--accent, #3d8b5e)" opacity="0.6"/>
172
- <line x1="32" y1="40" x2="76" y2="40" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" opacity="0.15"/>
173
- <line x1="32" y1="52" x2="66" y2="52" stroke="var(--accent, #3d8b5e)" stroke-width="2.5" stroke-linecap="round" opacity="0.7"/>
174
- <line x1="32" y1="64" x2="72" y2="64" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" opacity="0.15"/>
175
- <line x1="124" y1="40" x2="168" y2="40" stroke="var(--accent, #3d8b5e)" stroke-width="2.5" stroke-linecap="round" opacity="0.7"/>
176
- <line x1="124" y1="52" x2="158" y2="52" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" opacity="0.15"/>
177
- <line x1="124" y1="64" x2="164" y2="64" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" opacity="0.15"/>
178
- <line x1="32" y1="132" x2="76" y2="132" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" opacity="0.15"/>
179
- <line x1="32" y1="144" x2="62" y2="144" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" opacity="0.15"/>
180
- <line x1="32" y1="156" x2="70" y2="156" stroke="var(--accent, #3d8b5e)" stroke-width="2.5" stroke-linecap="round" opacity="0.7"/>
181
- <line x1="124" y1="132" x2="168" y2="132" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" opacity="0.15"/>
182
- <line x1="124" y1="144" x2="160" y2="144" stroke="var(--accent, #3d8b5e)" stroke-width="2.5" stroke-linecap="round" opacity="0.7"/>
183
- <line x1="124" y1="156" x2="152" y2="156" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" opacity="0.15"/>
163
+ <defs>
164
+ <filter id="cpub-hero-tex" x="-5%" y="-5%" width="110%" height="110%">
165
+ <feTurbulence type="fractalNoise" baseFrequency="0.04" numOctaves="4" result="noise"/>
166
+ <feDisplacementMap in="SourceGraphic" in2="noise" scale="2"/>
167
+ </filter>
168
+ </defs>
169
+ <g filter="url(#cpub-hero-tex)">
170
+ <rect x="18" y="18" width="72" height="72" fill="none" stroke="currentColor" stroke-width="5.5" stroke-linejoin="round"/>
171
+ <rect x="110" y="18" width="72" height="72" fill="none" stroke="currentColor" stroke-width="5.5" stroke-linejoin="round"/>
172
+ <rect x="18" y="110" width="72" height="72" fill="none" stroke="currentColor" stroke-width="5.5" stroke-linejoin="round"/>
173
+ <rect x="110" y="110" width="72" height="72" fill="none" stroke="currentColor" stroke-width="5.5" stroke-linejoin="round"/>
174
+ <rect x="90" y="90" width="20" height="20" fill="var(--accent, #3d8b5e)"/>
175
+ <rect x="84" y="84" width="6" height="6" fill="var(--accent, #3d8b5e)" opacity="0.6"/>
176
+ <rect x="110" y="84" width="6" height="6" fill="var(--accent, #3d8b5e)" opacity="0.6"/>
177
+ <rect x="84" y="110" width="6" height="6" fill="var(--accent, #3d8b5e)" opacity="0.6"/>
178
+ <rect x="110" y="110" width="6" height="6" fill="var(--accent, #3d8b5e)" opacity="0.6"/>
179
+ <line x1="32" y1="40" x2="76" y2="40" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" opacity="0.15"/>
180
+ <line x1="32" y1="52" x2="66" y2="52" stroke="var(--accent, #3d8b5e)" stroke-width="2.5" stroke-linecap="round" opacity="0.7"/>
181
+ <line x1="32" y1="64" x2="72" y2="64" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" opacity="0.15"/>
182
+ <line x1="124" y1="40" x2="168" y2="40" stroke="var(--accent, #3d8b5e)" stroke-width="2.5" stroke-linecap="round" opacity="0.7"/>
183
+ <line x1="124" y1="52" x2="158" y2="52" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" opacity="0.15"/>
184
+ <line x1="124" y1="64" x2="164" y2="64" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" opacity="0.15"/>
185
+ <line x1="32" y1="132" x2="76" y2="132" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" opacity="0.15"/>
186
+ <line x1="32" y1="144" x2="62" y2="144" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" opacity="0.15"/>
187
+ <line x1="32" y1="156" x2="70" y2="156" stroke="var(--accent, #3d8b5e)" stroke-width="2.5" stroke-linecap="round" opacity="0.7"/>
188
+ <line x1="124" y1="132" x2="168" y2="132" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" opacity="0.15"/>
189
+ <line x1="124" y1="144" x2="160" y2="144" stroke="var(--accent, #3d8b5e)" stroke-width="2.5" stroke-linecap="round" opacity="0.7"/>
190
+ <line x1="124" y1="156" x2="152" y2="156" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" opacity="0.15"/>
191
+ </g>
184
192
  </svg>
185
193
  </div>
186
194
  </div>
@@ -19,13 +19,46 @@ export default defineEventHandler(async (event) => {
19
19
  const page = pages.find((p) => p.slug === pageSlug);
20
20
  if (!page) throw createError({ statusCode: 404, statusMessage: 'Page not found' });
21
21
 
22
- // Render markdown to HTML with TOC extraction
23
- const rendered = await renderMarkdown(page.content ?? '');
22
+ // Handle dual-format content: BlockTuple[] (new) or markdown string (legacy)
23
+ const content = page.content;
24
24
 
25
+ if (Array.isArray(content)) {
26
+ // New BlockTuple format — extract text for TOC generation
27
+ const headings = extractHeadingsFromBlocks(content as [string, Record<string, unknown>][]);
28
+ return {
29
+ ...page,
30
+ content,
31
+ html: null, // Client renders blocks directly
32
+ toc: headings,
33
+ frontmatter: {},
34
+ format: 'blocks',
35
+ };
36
+ }
37
+
38
+ // Legacy markdown format
39
+ const rendered = await renderMarkdown((content as string) ?? '');
25
40
  return {
26
41
  ...page,
27
42
  html: rendered.html,
28
43
  toc: rendered.toc,
29
44
  frontmatter: rendered.frontmatter,
45
+ format: 'markdown',
30
46
  };
31
47
  });
48
+
49
+ /** Extract TOC headings from BlockTuple array */
50
+ function extractHeadingsFromBlocks(
51
+ blocks: [string, Record<string, unknown>][],
52
+ ): Array<{ id: string; text: string; level: number }> {
53
+ const headings: Array<{ id: string; text: string; level: number }> = [];
54
+ for (const [type, content] of blocks) {
55
+ if (type === 'heading' && content.text) {
56
+ const text = String(content.text);
57
+ const level = (content.level as number) || 2;
58
+ // Must match BlockHeadingView.vue slugification exactly
59
+ const id = text.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');
60
+ headings.push({ id, text, level });
61
+ }
62
+ }
63
+ return headings;
64
+ }
@@ -30,5 +30,18 @@ export default defineEventHandler(async (event) => {
30
30
  throw createError({ statusCode: 404, statusMessage: 'No version found for docs site' });
31
31
  }
32
32
 
33
- return listDocsPages(db, version.id);
33
+ const pages = await listDocsPages(db, version.id);
34
+
35
+ // Parse content: if stored as JSON string (BlockTuple[]), parse back to array
36
+ return pages.map((page) => {
37
+ let content: string | unknown[] = page.content ?? '';
38
+ if (typeof content === 'string' && content.startsWith('[')) {
39
+ try {
40
+ content = JSON.parse(content);
41
+ } catch {
42
+ // Not valid JSON — keep as markdown string
43
+ }
44
+ }
45
+ return { ...page, content };
46
+ });
34
47
  });
@@ -0,0 +1,101 @@
1
+ /**
2
+ * Migration endpoint: Convert docs pages from markdown (string) to BlockTuple[] (JSON).
3
+ *
4
+ * POST /api/docs/migrate-content
5
+ *
6
+ * Requires admin auth. Converts all docsPages where content is a markdown string
7
+ * into BlockTuple[] JSON format using markdownToBlockTuples.
8
+ *
9
+ * This is a one-time migration. After running, the content column can be altered to JSONB.
10
+ *
11
+ * Run this BEFORE altering the column type:
12
+ * ALTER TABLE docs_pages ALTER COLUMN content TYPE jsonb USING content::jsonb;
13
+ */
14
+ import { markdownToBlockTuples } from '@commonpub/editor';
15
+ import { docsPages } from '@commonpub/schema';
16
+ import { eq } from 'drizzle-orm';
17
+
18
+ export default defineEventHandler(async (event) => {
19
+ const user = requireAuth(event);
20
+
21
+ // Only allow admins to run migration
22
+ if (!user.role || user.role !== 'admin') {
23
+ throw createError({ statusCode: 403, statusMessage: 'Admin only' });
24
+ }
25
+
26
+ const db = useDB();
27
+
28
+ // Fetch all pages
29
+ const allPages = await db.select().from(docsPages);
30
+
31
+ let converted = 0;
32
+ let skipped = 0;
33
+ let errored = 0;
34
+ const errors: Array<{ pageId: string; title: string; error: string }> = [];
35
+
36
+ for (const page of allPages) {
37
+ const content = page.content ?? '';
38
+
39
+ // Skip if already JSON (starts with [ for BlockTuple array)
40
+ if (typeof content === 'string' && content.startsWith('[')) {
41
+ try {
42
+ JSON.parse(content);
43
+ skipped++;
44
+ continue;
45
+ } catch {
46
+ // Not valid JSON, proceed with conversion
47
+ }
48
+ }
49
+
50
+ // Skip empty content
51
+ if (!content || (typeof content === 'string' && !content.trim())) {
52
+ // Set empty content to an empty paragraph block
53
+ try {
54
+ await db
55
+ .update(docsPages)
56
+ .set({ content: JSON.stringify([['paragraph', { html: '' }]]) })
57
+ .where(eq(docsPages.id, page.id));
58
+ converted++;
59
+ } catch (err: unknown) {
60
+ errored++;
61
+ errors.push({
62
+ pageId: page.id,
63
+ title: page.title,
64
+ error: err instanceof Error ? err.message : 'Unknown error',
65
+ });
66
+ }
67
+ continue;
68
+ }
69
+
70
+ // Convert markdown to BlockTuples
71
+ try {
72
+ const blocks = markdownToBlockTuples(content as string);
73
+ const jsonContent = JSON.stringify(blocks);
74
+
75
+ await db
76
+ .update(docsPages)
77
+ .set({ content: jsonContent })
78
+ .where(eq(docsPages.id, page.id));
79
+
80
+ converted++;
81
+ } catch (err: unknown) {
82
+ errored++;
83
+ errors.push({
84
+ pageId: page.id,
85
+ title: page.title,
86
+ error: err instanceof Error ? err.message : 'Unknown error',
87
+ });
88
+ }
89
+ }
90
+
91
+ return {
92
+ total: allPages.length,
93
+ converted,
94
+ skipped,
95
+ errored,
96
+ errors,
97
+ nextStep: errored === 0
98
+ ? 'All pages converted. You can now run: ALTER TABLE docs_pages ALTER COLUMN content TYPE jsonb USING content::jsonb;'
99
+ : 'Some pages failed conversion. Fix errors and re-run.',
100
+ };
101
+ });