NowPage supports hierarchical URLs via parent_page_id chains, but three capabilities are missing:
/prds/vault, you need a parent page called prds — but that page must have custom HTML. If you don't write content for it, /prds/ is a blank page. Users avoid creating groups because the parent page is wasted effort.PageManager.tsx shows page rows with name + status + link. No columns, no sorting, no search, no folder info, no dates. Once you have 30+ pages, the list view is useless and you default to the grid (which is slow to scan at scale).domain/group/page/subpage must manually set parent_page_id on each page.What's broken today:
hc_type doesn't include "group" — no semantic distinction| Component | Status | Location |
|---|---|---|
parent_page_id chain | Working | pages table, app/api/pages/route.ts |
full_path resolution | Working | app/serve/[[...slug]]/route.ts lines 300-322 |
| Three-level URL support | Working | full_path = "group/page/subpage" resolves correctly |
| Grid view | Working | PageCardGrid.tsx — thumbnail cards |
| List view | Minimal | PageManager.tsx — bare rows, no columns/sort/search |
| Tree view | Working | PageTree.tsx — expandable hierarchy, drag-reparent |
| Folder metadata | Working | folders table, folder_id on pages (org only, not routing) |
| View mode switcher | Working | DashboardContent.tsx line 636 — grid/list/tree buttons |
| Neural registry | Working | Domain-wide only via is_neural_registry flag |
| Component | Impact |
|---|---|
| Auto-render template for group pages | Groups require custom HTML or they're blank |
hc_type: "group" in HC metadata | No semantic distinction for group pages |
| Focused neural registries | Registries are domain-wide, not group-scoped |
| Table view with sortable columns | List view unusable at scale |
| Search across pages | No way to find a page by name/slug/path |
| "Create group" guided flow | Must manually set parent_page_id per-page |
| Breadcrumb display in table | No hierarchy visibility in list/table view |
| Pattern | Example | Use Case | parent_page_id |
|---|---|---|---|
| Flat | domain.com/rick-meekins |
Most pages. Single artifact, no grouping needed. | null |
| Grouped | domain.com/prds/vault |
Related pages under a category. The group page (/prds/) serves as an index with its own content or auto-rendered. |
Page's parent = group page |
| Nested | domain.com/prds/vault/appendix |
Sub-pages within a grouped page. For multi-page templates, deep-dive sections, appendices. | Page's parent = page within group |
The serve route (app/serve/[[...slug]]/route.ts) already handles all three:
Request: domain.com/prds/vault/appendix
1. Try full_path = "prds/vault/appendix" → match? serve it
2. Try slug = "prds/vault/appendix" → match? serve it
3. 404
The full_path is built automatically from the parent_page_id chain when a page is created/updated in app/api/pages/route.ts.
A page is considered a "group page" when ALL of these are true:
parent_page_id references to it exist)html_content that is empty/placeholderhc_type: "group" in its HC metadata (explicit opt-in even with content){
"hc_version": "1.3.3",
"hc_type": "group",
"title": "Platform PRDs",
"group_principle": "These PRDs define the NowPage platform build sequence.
Each phase unlocks a multiplier on the one before it.",
"neural_registry": true,
"child_order": "manual",
"gate": null
}
New fields:
| Field | Type | Description |
|---|---|---|
hc_type | "group" | Identifies this as a group page |
group_principle | string | The "why" statement displayed at top of auto-render |
neural_registry | boolean | If true, this group's context injects into child page MCP requests |
child_order | "manual" | "alpha" | "newest" | "oldest" | How children are sorted in auto-render |
gate | null | "password" | Optional password gate on the group page |
MasteryMade design system. Same fonts (Bebas Neue / DM Sans / DM Mono), same color palette (cream/ink/amber). The template structure:
The template renders from child page metadata. No LLM calls, no generation. Pure data assembly:
interface GroupChildData {
page_name: string // from pages table
slug: string // from pages table
full_path: string // from pages table
description: string // from HC metadata in html_content (parsed)
tags: Tag[] // from page_tags join
created_at: string // from pages table
updated_at: string // from pages table
is_published: boolean // from pages table
children_count: number // count of this page's own children
}
<script id="hc-metadata" type="application/json"> from each child page's html_content to get title and description. If no HC metadata exists, fall back to page_name and show no description.The auto-render happens in the serve route, not as stored HTML:
// app/serve/[[...slug]]/route.ts
// After finding the page...
if (isGroupPage(page)) {
const children = await getChildPages(page.id, supabase)
const metadata = parseHCMetadata(page.html_content)
const html = renderGroupTemplate({
title: page.page_name,
principle: metadata?.group_principle,
children,
childOrder: metadata?.child_order || 'manual',
domain: domain.full_domain,
groupSlug: page.slug,
updatedAt: page.updated_at
})
return new Response(html, {
headers: { 'Content-Type': 'text/html' }
})
}
function isGroupPage(page: Page): boolean {
const metadata = parseHCMetadata(page.html_content)
if (metadata?.hc_type === 'group') return true
if (!page.html_content || page.html_content.trim().length < 100) {
// Check if it has children
return true // caller must verify children exist
}
return false
}
If a group page has hc_type: "group" AND substantial html_content, serve the custom HTML. The auto-render is a fallback for empty groups, not a replacement for authored content. This means:
hc_type: "group" flag controls neural registry behavior regardless of contentA page with is_neural_registry: true serves as a domain-wide context source. MCP tools can request /api/registry?domain=X and get all registry page content concatenated.
When a group page has neural_registry: true in its HC metadata:
MCP endpoint change:
GET /api/registry?domain=X&page=prds/vault
Response:
{
"domain_registry": "...", // existing domain-wide context
"group_registry": "...", // NEW: /prds/ group context
"group_principle": "These PRDs...", // NEW: the group's "why"
"siblings": [ // NEW: other pages in the group
{ "slug": "teams", "title": "Teams PRD" },
{ "slug": "mcp", "title": "MCP PRD" }
]
}
For nested pages (domain/group/page/subpage), the MCP endpoint walks up the parent_page_id chain and collects registries at each level:
subpage → parent page → group page → domain
↑ group registry
↑ (no registry, skip)
↑ (the requested page)
This is a single recursive query, not multiple round trips:
WITH RECURSIVE ancestors AS (
SELECT id, parent_page_id, html_content, page_name
FROM pages WHERE id = $requested_page_id
UNION ALL
SELECT p.id, p.parent_page_id, p.html_content, p.page_name
FROM pages p
JOIN ancestors a ON p.id = a.parent_page_id
)
SELECT * FROM ancestors WHERE id != $requested_page_id;
neural_registry: true in their HC metadata.The current PageManager.tsx (168 lines) becomes PageDataTable.tsx. The view mode switcher in DashboardContent.tsx stays as-is — viewMode === 'list' renders the new component.
| Column | Source | Width | Sortable | Notes |
|---|---|---|---|---|
| Name | page_name | flex | Yes | Bold. Shows hierarchy indent for children. |
| Path | full_path or slug | 200px | Yes | Monospace. Clickable link to live URL. |
| Folder | folder?.name | 100px | Yes | Gray if "—" (no folder). |
| Status | is_published, is_homepage | 80px | Yes | Badge: Home (blue), Live (green), Draft (gray). |
| Tags | tags[] | 120px | No | Color dots. Hover for names. |
| Updated | updated_at | 100px | Yes (default desc) | Relative: "2h ago", "Mar 7". |
| Actions | — | 80px | No | View / Edit / Delete icons. |
Text input above the table. Filters on:
page_name (case-insensitive contains)slug (case-insensitive contains)full_path (case-insensitive contains)filteredPages array. No API call needed.const [sortColumn, setSortColumn] = useState<
'name' | 'path' | 'folder' | 'status' | 'updated'
>('updated')
const [sortDirection, setSortDirection] = useState<
'asc' | 'desc'
>('desc')
Click column header to sort. Click again to toggle direction. Active column header shows arrow indicator.
Pages with parent_page_id show indented in the table when sorted by name or path:
Vault PRD /prds/vault
Vault Appendix /prds/vault/appendix
Teams PRD /prds/teams
Rick Meekins /rick-meekins
full_path depth (count of / separators).| Action | Behavior |
|---|---|
| View | Opens https://{domain}/{full_path} in new tab |
| Edit | Calls onEditPage(page) (existing handler) |
| Delete | Calls onDeletePage(page.id) (existing handler) with confirm |
Below 768px:
interface PageDataTableProps {
pages: (Page & { domain?: Domain })[]
domains: Domain[]
onEditPage: (page: Page) => void
onDeletePage: (pageId: string) => void
onViewPage: (page: Page) => void
}
DashboardContent.tsx where <PageManager> is rendered today (line 724-730).Add a "Create Group" option to the + New Page / + Build Page area:
Clicking "+ Group" opens a minimal dialog:
This creates a page with:
page_name = Group Nameslug = the entered slughtml_content = minimal HC metadata JSON only (no body HTML)hc_type = "group" in metadatagroup_principle = entered text (optional)neural_registry = checkbox valueThe existing tree view already supports drag-to-reparent. Add a context menu option:
Right-click page → "Move to Group..." → dropdown of existing group pages
parent_page_id and rebuilds full_path — already working in the API.In the table view, add a "Group" column option or inline action:
In all views (grid, table, tree):
| View | How groups appear |
|---|---|
| Grid | Group card has a folder icon + child count badge. Thumbnail shows auto-rendered template preview. |
| Table | Group rows have bold name + child count. Children indented below. |
| Tree | Already shows hierarchy. Group pages get a distinct icon (folder vs document). |
When editing a page that's inside a group, show breadcrumb navigation:
domain.com / prds / vault
↑ clickable (goes to group page editor)
When a reader visits domain.com/prds/:
When a reader is on a child page (domain.com/prds/vault):
show_nav: true)Decision: defer child nav injection to a future iteration. It requires modifying served HTML, which is risky. The group page itself provides sufficient navigation.
| URL | What they see |
|---|---|
domain.com/prds/ | Auto-rendered group index with principle + child list |
domain.com/prds/vault | The Vault PRD (custom HTML, served as today) |
domain.com/prds/vault/appendix | Sub-page of Vault (custom HTML, served as today) |
domain.com/rick-meekins | Flat page (no group, served as today) |
No dependencies, immediate value
PageDataTable.tsx with columns, sorting, search<PageManager> reference in DashboardContent.tsxFiles changed: components/PageDataTable.tsx (new), components/DashboardContent.tsx (swap component)
Files removed: components/PageManager.tsx (replaced)
Effort: 2-3 hours
Serve route + HC metadata
hc_type: "group" to HC metadata specparseHCMetadata() utility (extract JSON from <script id="hc-metadata">)renderGroupTemplate() in serve route (static HTML builder)isGroupPage() check in serve route before normal page serveFiles changed: app/serve/[[...slug]]/route.ts, new utility lib/hc-metadata.ts
No migrations. All data is in existing html_content and pages table.
Effort: 3-4 hours
Dashboard dialog
GroupCreateDialog.tsx componentPOST /api/pages with HC metadata in html_contentFiles changed: components/GroupCreateDialog.tsx (new), components/DashboardContent.tsx, components/PageDataTable.tsx, components/PageCardGrid.tsx
Effort: 2-3 hours
MCP endpoint
/api/registry endpointneural_registry: truegroup_principle and siblings in responseFiles changed: app/api/registry/route.ts (or wherever registry endpoint lives)
Effort: 2 hours
| Excluded | Why |
|---|---|
| Folder-based URL routing | parent_page_id chain already handles this. Folders stay as dashboard metadata. |
| Child page navigation injection | Modifying served HTML at render time is risky. Group page provides navigation. |
| Configurable auto-render themes | One template (MasteryMade). No theme picker. |
| Pagination on group pages | Groups won't have 100+ children. Simple list is sufficient. |
| Drag-drop in table view | Tree view already handles reparenting. Table is for scanning. |
| Fourth view mode | Table replaces list. No new button needed. |
/prds/ auto-renders with principle box and child list when no custom HTML exists