@commonpub/layer 0.22.1 → 0.23.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/components/LayoutSlot.vue +266 -0
- package/components/sections/SectionContentFeed.vue +160 -0
- package/components/sections/SectionDivider.vue +55 -0
- package/components/sections/SectionHeading.vue +78 -0
- package/components/sections/SectionHero.vue +164 -0
- package/components/sections/SectionImage.vue +104 -0
- package/components/sections/SectionParagraph.vue +55 -0
- package/composables/useFeatures.ts +10 -0
- package/composables/useLayout.ts +132 -0
- package/package.json +7 -6
- package/pages/admin/theme/index.vue +22 -1
- package/pages/index.vue +23 -2
- package/sections/builtin/content-feed.ts +52 -0
- package/sections/builtin/divider.ts +37 -0
- package/sections/builtin/heading.ts +36 -0
- package/sections/builtin/hero.ts +67 -0
- package/sections/builtin/image.ts +67 -0
- package/sections/builtin/paragraph.ts +34 -0
- package/sections/registry.ts +61 -0
- package/server/api/admin/layouts/[id]/publish.post.ts +33 -0
- package/server/api/admin/layouts/[id]/versions/[versionId]/revert.post.ts +43 -0
- package/server/api/admin/layouts/[id]/versions/index.get.ts +29 -0
- package/server/api/admin/layouts/[id].delete.ts +34 -0
- package/server/api/admin/layouts/[id].get.ts +27 -0
- package/server/api/admin/layouts/[id].put.ts +64 -0
- package/server/api/admin/layouts/index.get.ts +25 -0
- package/server/api/admin/layouts/index.post.ts +48 -0
- package/server/api/admin/layouts/seed-homepage.post.ts +35 -0
- package/server/api/admin/themes/discover.get.ts +14 -5
- package/server/api/layouts/by-route.get.ts +87 -0
- package/server/utils/layoutCache.ts +53 -0
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GET /api/layouts/by-route?path=/some-path
|
|
3
|
+
*
|
|
4
|
+
* Public endpoint — resolves the active layout for a route, used by the
|
|
5
|
+
* `<LayoutSlot>` renderer on every page request. Returns the published
|
|
6
|
+
* version when available, otherwise the draft (during pre-publish
|
|
7
|
+
* editing — admins see drafts, end users get a 404 until published).
|
|
8
|
+
*
|
|
9
|
+
* Cached server-side for `LAYOUT_CACHE_TTL_MS` per path (see
|
|
10
|
+
* `server/utils/layoutCache.ts`). Invalidated on any layout write —
|
|
11
|
+
* the admin POST/PUT/DELETE/publish/revert handlers each call
|
|
12
|
+
* `invalidateLayoutsByRouteCache()` before returning.
|
|
13
|
+
*
|
|
14
|
+
* Gated by `features.layoutEngine`. Returns 404 when off so the legacy
|
|
15
|
+
* homepage section renderer keeps working.
|
|
16
|
+
*/
|
|
17
|
+
import { getLayoutByScope, type LayoutRecord, type LayoutScope } from '@commonpub/server';
|
|
18
|
+
import {
|
|
19
|
+
LAYOUT_CACHE_TTL_MS,
|
|
20
|
+
getLayoutCacheEntry,
|
|
21
|
+
setLayoutCacheEntry,
|
|
22
|
+
} from '../../utils/layoutCache';
|
|
23
|
+
|
|
24
|
+
// Re-export for callers that import the invalidator from this file
|
|
25
|
+
// (kept for backwards compat after the session-158 refactor that moved
|
|
26
|
+
// the cache into utils/). New code should import from utils/layoutCache.
|
|
27
|
+
export { invalidateLayoutsByRouteCache } from '../../utils/layoutCache';
|
|
28
|
+
|
|
29
|
+
interface PublicLayoutSlice {
|
|
30
|
+
/** Zones → rows → enabled+visible sections only. Strips draft metadata. */
|
|
31
|
+
zones: LayoutRecord['zones'];
|
|
32
|
+
/** Page meta for custom pages; null for routes. */
|
|
33
|
+
pageMeta: LayoutRecord['pageMeta'];
|
|
34
|
+
/** State so SSR can mark `noindex` on drafts. */
|
|
35
|
+
state: LayoutRecord['state'];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export default defineEventHandler(async (event): Promise<PublicLayoutSlice | null> => {
|
|
39
|
+
const config = useConfig();
|
|
40
|
+
if (!(config.features as unknown as Record<string, boolean>).layoutEngine) {
|
|
41
|
+
// Feature off — return 404 so the legacy renderer stays in charge
|
|
42
|
+
throw createError({ statusCode: 404, statusMessage: 'Layout engine not enabled' });
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const { path } = parseQueryParams(event, layoutsByRoutePathSchema);
|
|
46
|
+
const cacheKey = path;
|
|
47
|
+
const hit = getLayoutCacheEntry<PublicLayoutSlice>(cacheKey);
|
|
48
|
+
const now = Date.now();
|
|
49
|
+
if (hit && now - hit.at < LAYOUT_CACHE_TTL_MS) {
|
|
50
|
+
return hit.value;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const db = useDB();
|
|
54
|
+
// Try route scope first, then custom-page (custom pages shadow routes)
|
|
55
|
+
const scope: LayoutScope = isCustomPagePath(path)
|
|
56
|
+
? { type: 'custom-page', path }
|
|
57
|
+
: { type: 'route', path };
|
|
58
|
+
|
|
59
|
+
const layout = await getLayoutByScope(db, scope);
|
|
60
|
+
const value: PublicLayoutSlice | null = layout
|
|
61
|
+
? {
|
|
62
|
+
zones: layout.zones,
|
|
63
|
+
pageMeta: layout.pageMeta,
|
|
64
|
+
state: layout.state,
|
|
65
|
+
}
|
|
66
|
+
: null;
|
|
67
|
+
|
|
68
|
+
setLayoutCacheEntry(cacheKey, value, now);
|
|
69
|
+
return value;
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Heuristic: paths NOT in the file-routes manifest are candidate
|
|
74
|
+
* custom-page paths. For Phase 1, we leave this as `false` (always
|
|
75
|
+
* resolve as route scope) — custom-page support lands in Phase 2 once
|
|
76
|
+
* the catch-all + conflict detection is in.
|
|
77
|
+
*/
|
|
78
|
+
function isCustomPagePath(_path: string): boolean {
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Inline Zod since the public-API surface keeps validators close to handlers
|
|
83
|
+
// (matches the pattern used elsewhere — e.g. /api/profile/theme.put.ts).
|
|
84
|
+
import { z } from 'zod';
|
|
85
|
+
const layoutsByRoutePathSchema = z.object({
|
|
86
|
+
path: z.string().min(1).max(512).regex(/^\/[a-zA-Z0-9._~/-]*$/, 'Path must start with /'),
|
|
87
|
+
});
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared layout-resolution cache.
|
|
3
|
+
*
|
|
4
|
+
* The `/api/layouts/by-route` endpoint reads from this; every admin
|
|
5
|
+
* layout write handler MUST invalidate after success (otherwise SSR
|
|
6
|
+
* serves stale data for up to TTL_MS after a save).
|
|
7
|
+
*
|
|
8
|
+
* Kept here (utils/) rather than inline in `api/layouts/by-route.get.ts`
|
|
9
|
+
* so the admin write routes can import the invalidator without
|
|
10
|
+
* cross-file-handler imports (nitro's route-discovery treats `*.get.ts`
|
|
11
|
+
* + `*.post.ts` as handlers, not regular modules — utility shared state
|
|
12
|
+
* belongs in `utils/`).
|
|
13
|
+
*
|
|
14
|
+
* Per-process map → 60s TTL bounds staleness across pods. The Phase 10
|
|
15
|
+
* performance pass (`docs/plans/layout-and-pages.md` §10) replaces this
|
|
16
|
+
* with ETag-based revalidation; the in-memory cache is sufficient for
|
|
17
|
+
* Phase 1c volumes.
|
|
18
|
+
*/
|
|
19
|
+
export interface LayoutCacheEntry<T> {
|
|
20
|
+
value: T | null;
|
|
21
|
+
at: number;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export const LAYOUT_CACHE_TTL_MS = 60_000;
|
|
25
|
+
const cache = new Map<string, LayoutCacheEntry<unknown>>();
|
|
26
|
+
|
|
27
|
+
export function getLayoutCacheEntry<T>(key: string): LayoutCacheEntry<T> | undefined {
|
|
28
|
+
return cache.get(key) as LayoutCacheEntry<T> | undefined;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function setLayoutCacheEntry<T>(key: string, value: T | null, at: number = Date.now()): void {
|
|
32
|
+
cache.set(key, { value, at });
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Drop every cached entry. Called from every admin layout write
|
|
37
|
+
* handler (POST/PUT/DELETE on layouts, publish, revert, seed) BEFORE
|
|
38
|
+
* the response is sent. A more selective `invalidate(scopeKey)` is
|
|
39
|
+
* possible — but the cache is tiny (one entry per ever-visited route)
|
|
40
|
+
* and full-clear is the safer default while the editor is still
|
|
41
|
+
* coalescing draft saves (Phase 7+).
|
|
42
|
+
*/
|
|
43
|
+
export function invalidateLayoutsByRouteCache(): void {
|
|
44
|
+
cache.clear();
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Cache size — for test-only inspection (verify a write actually
|
|
49
|
+
* cleared the cache rather than just calling a no-op function).
|
|
50
|
+
*/
|
|
51
|
+
export function _layoutCacheSize(): number {
|
|
52
|
+
return cache.size;
|
|
53
|
+
}
|