scaffold-frontend-subpage
Skill16 starsChanged 3 days agoApache-2.0View on GitHub
Scaffold a detail wrapper with SelectButton tab navigation and nested tab subpages. Creates the tab bar, NavItem routing, StructuralColumn content, and overview tab following the [agent_class]-[agent_id].vue pattern. Use when user says "add a detail page", "create subpage with tabs", "scaffold detail view", "add tab navigation", "create nested route page", or "build a detail page like agents". Do NOT use for list pages (use scaffold-frontend-page), individual components (use scaffold-frontend-component), or composables only (use scaffold-composable). Generates wrapper + tab pages + composable + i18n.
We split this skill into the rules it gives an agent
10 rules. About 0 are common, 2 are distinctive, and 8 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈2
Distinctive · estimate
This author's own choices. Worth a look.
8
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Very specific: line 14
Read the frontend scope guide: packages/web/CLAUDE.md
- Distinctive: line 37
Ensure a single-item composable exists:
- Very specific: line 43
Follow the pattern from composables/agent/useAgentInstance.ts:
- Very specific: line 158
Create tab pages in packages/web/pages/[tenant]/service/{resource}s/[{resource}_id]/:
- Very specific: line 202
<StructuralColumn>: Every tab page wraps content in a StructuralColumn. - Very specific: line 243
Check the nested route resolves: pages/[tenant]/service/{resource}s/[{resource}_id].vue must be inside a directory matching the parent list …
- Very specific: line 245
Verify tab navigation works: each NavItem path must match a .vue file in the [{resource}_id]/ directory
- Very specific: line 246
Verify isActive() returns a closure (function returning boolean), not a boolean value
- Distinctive: line 247
Verify i18n keys exist in ALL 4 locale files for navigation labels and field labels
- Very specific: line 248
If unsure about Nuxt nested routing, use mcp__nuxt__get-documentation-page with /docs/4.x/guide/directory-structure/pages or …
≈ “Common” is an estimate: at least 1 in 5 of the 308,232 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
Scaffold a Frontend Subpage (Detail + Tabs)
Generate a detail wrapper page with tab navigation and tab content pages. The resource name should be provided via
$ARGUMENTS.
Before You Start
- Read the frontend scope guide:
packages/web/CLAUDE.md - Study these reference files:
- Detail wrapper with tabs:
packages/web/pages/[tenant]/service/agents/[agent_class]-[agent_id].vue - Tab content page:
packages/web/pages/[tenant]/service/agents/[agent_class]-[agent_id]/overview.vue - Another wrapper:
packages/web/pages/[tenant]/service/threads/[thread_id].vue - NavItem type:
packages/web/types/NavItem.ts
- Detail wrapper with tabs:
Architecture: How Nested Routing Works
pages/[tenant]/service/{resource}s.vue <- List page (has NuxtPage outlet)
pages/[tenant]/service/{resource}s/
[{resource}_id].vue <- Detail WRAPPER (tab bar + NuxtPage)
overview.vue <- Tab content page
configuration.vue <- Tab content page
other-tab.vue <- Tab content pageThe list page renders as the left column. When a user clicks an item, the detail wrapper renders as a second column via
NuxtPage. The wrapper shows the tab bar and renders the active tab's content via its own nested NuxtPage.
Step 1: Create Composable (if needed)
Ensure a single-item composable exists:
composables/{resource}/use{Resource}.ts <- Fetches by route param IDFollow the pattern from composables/agent/useAgentInstance.ts:
export const use<Resource> = defineQuery(() => {
const route = useRoute()
const isRouteReady = useRouteReady('<resource>_id')
const { data: <resource>, isPending: <resource>IsLoading } = useQuery<Full<Resource>Dto>({
key: () => ['<resource>s', route.params.<resource>_id as string],
staleTime: minutesToMilliseconds(5),
enabled: isRouteReady,
query: async () => {
return await get<Resource>({
composable: '$fetch',
path: { <resource>_id: route.params.<resource>_id as string },
})
},
})
return { <resource>, <resource>IsLoading }
})Step 2: Create the Detail Wrapper
Create packages/web/pages/[tenant]/service/{resource}s/[{resource}_id].vue:
<template>
<div class="flex flex-col gap-2">
<SelectButton
v-if="navItems"
:model-value="activeNavItem"
:options="navItems"
data-key="key"
option-label="name"
size="small"
@update:model-value="toNavItem"
/>
<div class="flex gap-8">
<NuxtPage />
</div>
</div>
</template>
<script setup lang="ts">
import type { NavItem } from '@core/types/NavItem'
const router = useRouter()
const route = useRoute()
const tenantPath = useTenantPath()
const { t } = useI18n()
const { <resource> } = use<Resource>()
const subPath = (path: string) => {
return `/service/<resource>s/${route.params.<resource>_id}/${path}`
}
const isActive = (path: string) => {
return () => {
const localizedPath = tenantPath(subPath(path))
return route.path.startsWith(localizedPath)
}
}
const navItems = computed<NavItem[]>(() => {
return [
{ name: t('<resource>.navigation.overview'), key: 'overview', path: subPath('overview'), isActive: isActive('overview') },
{ name: t('<resource>.navigation.configuration'), key: 'configuration', path: subPath('configuration'), isActive: isActive('configuration') },
// Add more tabs as needed
]
})
const toNavItem = (navItem: NavItem | null) => {
if (navItem) {
router.push(tenantPath(navItem.path))
}
}
const activeNavItem = computed<NavItem | undefined>(() => {
return navItems.value?.filter(navItem => navItem.isActive())[0]
})
</script>Key Patterns in the Wrapper
SelectButtonfor tabs — NOT PrimeVueTabView. Tabs are route-based, not component-based.NavItemtype:{ name: string, key: string, path: string, isActive: () => boolean }subPath()helper: Builds child route paths from current params.isActive()helper: Returns a function that checksroute.path.startsWith(localizedPath).NuxtPage: Renders the active tab content as a nested route.- Conditional tabs: Tabs can be added conditionally based on resource state (e.g., show chat tab only if
is_conversational).
Dynamic Route Params
For compound identifiers (like agents), use [agent_class]-[agent_id].vue:
const subPath = (path: string) => {
return `/service/agents/${route.params.agent_class}-${route.params.agent_id}/${path}`
}For simple identifiers, use [{resource}_id].vue:
const subPath = (path: string) => {
return `/service/<resource>s/${route.params.<resource>_id}/${path}`
}Step 3: Create Tab Content Pages
Create tab pages in packages/web/pages/[tenant]/service/{resource}s/[{resource}_id]/:
Overview Tab (overview.vue)
<template>
<StructuralColumn
:title="<resource>?.name"
close-route="/service/<resource>s"
:loading="<resource>IsLoading"
size="large"
>
<div class="flex flex-col gap-12">
<!-- Description -->
<span class="mb-4 block text-sm text-surface-500 dark:text-surface-400">
{{ <resource>?.description }}
</span>
<!-- Detail fields in a grid -->
<Panel>
<div class="grid grid-cols-2 gap-4 xl:grid-cols-4">
<div class="flex flex-col items-start gap-2">
<span class="font-semibold">{{ t('<resource>.overview.name') }}</span>
<Tag :value="<resource>?.name" severity="secondary" />
</div>
<div class="flex flex-col items-start gap-2">
<span class="font-semibold">{{ t('<resource>.overview.status') }}</span>
<Tag :value="statusLabel" :severity="statusSeverity" />
</div>
<!-- More fields... -->
</div>
</Panel>
</div>
</StructuralColumn>
</template>
<script setup lang="ts">
const { <resource>, <resource>IsLoading } = use<Resource>()
const { t } = useI18n()
</script>Key Patterns in Tab Pages
<StructuralColumn>: Every tab page wraps content in a StructuralColumn.close-route: Points back to the list page (e.g.,/service/{resource}s). Shows an X button.size="large": Most detail pages uselarge(1440px). Usenormal(920px) for simpler content.:loading: Pass the composable's loading state. Content waits for loading to complete.childColumn: Set totrueif this is a nested subpage within another column.- Field display pattern:
font-semiboldlabel +Tag severity="secondary"value, in a grid.
Step 4: Add i18n Keys
{resource}:
navigation:
overview: "Overview"
configuration: "Configuration"
# Add more tabs
overview:
name: "Name"
status: "Status"
# Add more field labelsStep 5: Create File Structure
Final directory structure:
packages/web/
├── pages/[tenant]/service/
│ ├── {resource}s.vue <- List page (from /scaffold-frontend-page)
│ └── {resource}s/
│ └── [{resource}_id].vue <- Detail wrapper (tab bar)
│ ├── overview.vue <- Overview tab
│ ├── configuration.vue <- Config tab (optional)
│ └── other-tab.vue <- More tabs as needed
└── composables/{resource}/
├── use{Resource}s.ts <- List query
└── use{Resource}.ts <- Single item query (route param)Step 6: Verify
- Check the nested route resolves:
pages/[tenant]/service/{resource}s/[{resource}_id].vuemust be inside a directory matching the parent list page filename ({resource}s/) - Verify tab navigation works: each NavItem
pathmust match a.vuefile in the[{resource}_id]/directory - Verify
isActive()returns a closure (function returning boolean), not a boolean value - Verify i18n keys exist in ALL 4 locale files for navigation labels and field labels
- If unsure about Nuxt nested routing, use
mcp__nuxt__get-documentation-pagewith/docs/4.x/guide/directory-structure/pagesormcp__context7__query-docswith library/websites/nuxt
Key Conventions
- Tabs are routes, not components: Each tab is a separate
.vuefile in the nested directory - SelectButton, not TabView: Tab navigation uses PrimeVue
SelectButtonwithNavItemtype - Loading gates content: StructuralColumn doesn't render children until
loading === false - Close navigates to list:
close-routeon StructuralColumn always points back to the parent list - Composable reuse: The same
use{Resource}()composable is shared between wrapper and tab pages (Pinia-Colada deduplicates the query)
Examples
Input: $ARGUMENTS = "pipeline"
Output files created:
packages/web/pages/[tenant]/service/pipelines/[pipeline_id].vue-- Detail wrapper with tab barpackages/web/pages/[tenant]/service/pipelines/[pipeline_id]/overview.vue-- Overview tabpackages/web/pages/[tenant]/service/pipelines/[pipeline_id]/configuration.vue-- Configuration tabpackages/web/composables/pipeline/usePipeline.ts-- Single-item query composable- i18n keys added for navigation labels and field labels in all 4 locales
Input: $ARGUMENTS = "connector with tabs: overview, settings, logs"
Output: Wrapper at [connector_id].vue plus three tab files (overview.vue, settings.vue, logs.vue) with
NavItem entries for each tab.
Troubleshooting
| Problem | Cause | Fix |
| Tab content not rendering | Missing NuxtPage in wrapper | Ensure wrapper template has NuxtPage inside the flex container |
| Tab bar not highlighting active tab | isActive() path mismatch | Verify subPath() builds the correct URL with route.params values |
| Detail page not appearing as second column | Parent list page missing NuxtPage | Ensure the parent list page ({resource}s.vue) has NuxtPage as sibling of StructuralColumn |
| Route params undefined | Filename bracket syntax wrong | Dynamic route file must be named [{resource}_id].vue with square brackets |
| Composable fetches on every tab switch | Not using staleTime | Set staleTime: minutesToMilliseconds(5) in the query options |
| Close button navigates to wrong page | Incorrect close-route prop | Set close-route="/service/{resource}s" (plural, pointing to list page) |
In this repository50 other files it gives its agents.
- CLAUDE.mdCLAUDE.md
- docs/CLAUDE.mdCLAUDE.md
- .github/actions/CLAUDE.mdCLAUDE.md
- arc42Skill
- audit-frontendSkill
- bot-frameworkSkill
- create-or-audit-claude-mdSkill
- create-or-audit-skillSkill
- create-or-audit-subagentSkill
- create-prSkill
- dagster-pipelinesSkill
- debug-agentSkill
- debug-frontendSkill
- debug-pipelineSkill
- design-systemSkill
- docker-devSkill
- document-decisionSkill
- document-featureSkill
- explainSkill
- generate-sdkSkill
- i18nSkill
- implement-feedback-from-prSkill
- likec4-dslSkill
- lintSkill
- merge-mainSkill
- nats-eventsSkill
- plan-issueSkill
- pr-demo-videoSkill
- primevue-lookupSkill
- rclone-guideSkill
- reflectSkill
- review-diffSkill
- scaffold-agentSkill
- scaffold-api-endpointSkill
- scaffold-api-repositorySkill
- scaffold-api-serviceSkill
- scaffold-bot-handlerSkill
- scaffold-composableSkill
- scaffold-event-displaySkill
- scaffold-frontend-componentSkill
- scaffold-frontend-pageSkill
- scaffold-pipelineSkill
- setup-bot-connectionSkill
- splice-issueSkill
- sync-agent-docsSkill
- test-scopeSkill
- update-docSkill
- verify-sdk-syncSkill
- write-docSkill
- write-issueSkill
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

