scaffold-frontend-page
Skill16 starsChanged 3 days agoApache-2.0View on GitHub
Scaffold a new Nuxt 3 list page with StructuralScreen/Column layout, Pinia-Colada data fetching, card grid, create modal, and NuxtPage outlet for nested detail routes. Use when user says "create a new page", "scaffold a frontend page", "add a list page", "new resource page", "generate a Vue page for X", or "build a page like agents/roles". Do NOT use for detail pages with tabs (use scaffold-frontend-subpage), individual components (use scaffold-frontend-component), or composables only (use scaffold-composable). Generates page + card + i18n following agents.vue and roles.vue patterns.
We split this skill into the rules it gives an agent
17 rules. About 0 are common, 0 are distinctive, and 17 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈0
Distinctive · estimate
This author's own choices. Worth a look.
17
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Very specific: line 13
Read the frontend scope guide: packages/web/CLAUDE.md
- Very specific: line 21
Search packages/web/sdk/client/ for the resource's SDK functions and DTO types.
- Very specific: line 24
Look up the actual DTO type name in packages/web/sdk/client/types.gen.ts — types may be named {Resource}Response, Full{Resource}Dto, or …
- Very specific: line 29
If composables don't exist yet, create them first using the patterns from /scaffold-composable.
- Very specific: line 104
<NuxtPage />: MUST be placed as a sibling of StructuralColumn inside StructuralScreen. - Very specific: line 106
Card grid: Use grid grid-cols-2 gap-4 2xl:grid-cols-2 for consistent card layout.
- Very specific: line 108
Navigation: Always use useTenantPath() (wraps useLocalePath() and injects the [tenant] segment).
- Very specific: line 113
Create packages/web/components/{Resource}/Card.vue following this exact pattern:
- Very specific: line 178
Add translation keys in all 4 locale files (packages/web/i18n/locales/{de,en,fr,it}.yaml):
- Very specific: line 197
Check Nuxt file-based routing resolves the new page: filename must be {resource}s.vue (plural) in pages/[tenant]/service/
- Very specific: line 199
Verify NuxtPage renders nested routes: create a placeholder pages/[tenant]/service/{resource}s/ directory if detail routes are planned
- Very specific: line 201
Check sidebar registration: useSuite() → useApps() fetches service definitions from the API — the new resource may need API-side …
- Very specific: line 203
Verify i18n keys exist in ALL 4 locale files (de.yaml, en.yaml, fr.yaml, it.yaml)
- Very specific: line 204
If unsure about Nuxt routing conventions, use mcp__nuxt__get-documentation-page with /docs/4.x/guide/directory-structure/pages or …
- Very specific: line 210
Tailwind only: No custom CSS (exception: scoped :deep() for PrimeVue overrides)
- Very specific: line 211
SDK types for props: Import from @core/sdk/client, never define manually
- Very specific: line 214
useTenantPath(): Always for navigation -- router.push(tenantPath('/service/path')) (auto-imported)
≈ “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 New Frontend Page
Generate a list page for a new resource. The resource name should be provided via $ARGUMENTS.
Before You Start
- Read the frontend scope guide:
packages/web/CLAUDE.md - Study these reference pages:
- Simple list:
packages/web/pages/[tenant]/service/roles.vue - Complex list with grouping:
packages/web/pages/[tenant]/service/agents.vue - Knowledge list:
packages/web/pages/[tenant]/service/knowledge.vue
- Simple list:
Step 1: Check SDK Availability
Search packages/web/sdk/client/ for the resource's SDK functions and DTO types. If they don't exist, warn the user to
run /generate-sdk first.
Look up the actual DTO type name in packages/web/sdk/client/types.gen.ts — types may be named {Resource}Response,
Full{Resource}Dto, or {Resource}Dto depending on the API schema.
Step 2: Create Composables
If composables don't exist yet, create them first using the patterns from /scaffold-composable. At minimum you need:
composables/{resource}/use{Resource}s.ts— List querycomposables/{resource}/useCreate{Resource}.ts— Create mutation (if applicable)
Step 3: Create the List Page
Create packages/web/pages/[tenant]/service/{resource}s.vue:
<template>
<StructuralScreen>
<StructuralColumn
:title="t('<resource>.title')"
:loading="<resource>sAreLoading"
>
<div class="flex flex-col gap-2">
<!-- Create button (top right) -->
<div class="flex w-full justify-end">
<Button
:label="t('<resource>.create_new')"
icon="pi pi-plus"
@click="createModalOpen = true"
/>
</div>
<!-- Card grid -->
<div class="grid grid-cols-2 gap-4 2xl:grid-cols-2">
<<Resource>Card
v-for="item in <resource>s"
:key="item.id"
:<resource>="item"
@click="() => toDetail(item)"
/>
</div>
</div>
<!-- Create modal -->
<Dialog
v-model:visible="createModalOpen"
modal
:header="t('<resource>.create_new')"
>
<<Resource>Create @close="createModalOpen = false" />
</Dialog>
</StructuralColumn>
<!-- Nested detail page outlet -->
<NuxtPage />
</StructuralScreen>
</template>
<script setup lang="ts">
import type { <Resource>Dto } from '@core/sdk/client'
const router = useRouter()
const tenantPath = useTenantPath()
const { t } = useI18n()
const { <resource>s, <resource>sAreLoading } = use<Resource>s()
const createModalOpen = ref(false)
const toDetail = (item: <Resource>Dto) => {
router.push(tenantPath(`/service/<resource>s/${item.id}/overview`))
}
</script>Key Structural Elements
<StructuralScreen>: Top-level wrapper. Providesh-[calc(100vh-50px)]viewport,bg-surface-50 dark:bg-surface-950, horizontal flex at2xl:.<StructuralColumn>: Card container with rounded-3xl, loading progress bar, title/close button. Props:title,loading,size,closeRoute,childColumn.<NuxtPage />: MUST be placed as a sibling of StructuralColumn inside StructuralScreen. This renders nested detail routes as a second column.- Card grid: Use
grid grid-cols-2 gap-4 2xl:grid-cols-2for consistent card layout. - Create button: Top-right aligned with
flex w-full justify-end. - Navigation: Always use
useTenantPath()(wrapsuseLocalePath()and injects the[tenant]segment); paths are written without locale or tenant (/service/roles).
Step 4: Create Card Component
Create packages/web/components/{Resource}/Card.vue following this exact pattern:
<template>
<div
class="flex cursor-pointer flex-col gap-3 rounded-xl border border-surface-200 p-4 hover:bg-surface-100 dark:border-surface-800 hover:dark:bg-surface-800"
:class="{ 'bg-surface-100 dark:bg-surface-800': isActive }"
@click="emit('click', <resource>)"
>
<!-- Header: Icon + Title + Status -->
<div class="flex items-center justify-between gap-4">
<div class="flex items-center justify-start gap-2">
<div class="flex items-center justify-center rounded-full bg-white p-3 dark:bg-surface-900">
<Icon :name="icon" size="1.5em" />
</div>
<div>
<h3 class="font-semibold opacity-80">{{ <resource>.name }}</h3>
<p class="text-xs font-light opacity-70">{{ <resource>.id }}</p>
</div>
</div>
<div class="flex items-center gap-2">
<Tag :value="statusLabel" :severity="statusSeverity" />
</div>
</div>
<!-- Description -->
<div>
<span class="text-xs">{{ <resource>.description }}</span>
</div>
</div>
</template>
<script setup lang="ts">
import type { <Resource>Dto } from '@core/sdk/client'
const props = defineProps<{
<resource>: <Resource>Dto
}>()
const emit = defineEmits<{
click: [<resource>: <Resource>Dto]
}>()
const route = useRoute()
const { t } = useI18n()
const isActive = computed(() => {
return route.params.<resource>_id === props.<resource>.id
})
</script>Card Design Rules
- Container:
rounded-xl border border-surface-200 p-4with hover and dark mode variants - Active state:
bg-surface-100 dark:bg-surface-800when route matches - Icon container:
rounded-full bg-white p-3 dark:bg-surface-900withIcon size="1.5em" - Title:
font-semibold opacity-80 - Subtitle:
text-xs font-light opacity-70 - Description:
text-xs - Status: PrimeVue
Tagwith severity (success,danger,secondary) - Delete button:
Button icon="pi pi-trash" severity="secondary" text rounded size="small"with@click.stop
Step 5: Add i18n Keys
Add translation keys in all 4 locale files (packages/web/i18n/locales/{de,en,fr,it}.yaml):
{resource}:
title: "{Resource}s"
create_new: "Create {Resource}"
list:
empty: "No {resource}s found"
delete:
title: "Delete {Resource}"
confirmMessage: "Are you sure you want to delete this {resource}?"
button: "Delete"
success: "{Resource} deleted successfully"
error: "Failed to delete {resource}"
cancel: "Cancel"Step 6: Verify
- Check Nuxt file-based routing resolves the new page: filename must be
{resource}s.vue(plural) inpages/[tenant]/service/ - Verify
NuxtPagerenders nested routes: create a placeholderpages/[tenant]/service/{resource}s/directory if detail routes are planned - Check sidebar registration:
useSuite()→useApps()fetches service definitions from the API — the new resource may need API-side registration - Verify i18n keys exist in ALL 4 locale files (
de.yaml,en.yaml,fr.yaml,it.yaml) - If unsure about Nuxt routing conventions, use
mcp__nuxt__get-documentation-pagewith/docs/4.x/guide/directory-structure/pagesormcp__context7__query-docswith library/websites/nuxt
Key Conventions
- PrimeVue components only:
Button,Tag,Dialog,DataTable-- never raw HTML for interactive elements - Tailwind only: No custom CSS (exception: scoped
:deep()for PrimeVue overrides) - SDK types for props: Import from
@core/sdk/client, never define manually - i18n all text:
{{ t('key.path') }}for everything user-visible - Pinia-Colada:
defineQuery/defineMutation-- never raw fetch or global stores useTenantPath(): Always for navigation --router.push(tenantPath('/service/path'))(auto-imported)useConfirm()+useToast(): For delete confirmations and success/error messages
Examples
Input: $ARGUMENTS = "pipeline"
Output files created:
packages/web/pages/[tenant]/service/pipelines.vue-- List page with card gridpackages/web/components/Pipeline/Card.vue-- Resource card componentpackages/web/composables/pipeline/usePipelines.ts-- List query composablepackages/web/composables/pipeline/useCreatePipeline.ts-- Create mutation composable- i18n keys added to all 4 locale files (
de.yaml,en.yaml,fr.yaml,it.yaml)
Input: $ARGUMENTS = "connector"
Output: Same structure with connector replacing pipeline -- connectors.vue, Connector/Card.vue,
useConnectors.ts, etc.
Troubleshooting
| Problem | Cause | Fix |
| SDK types not found for resource | SDK not generated yet | Tell user to run /generate-sdk first |
| Page not appearing in sidebar | Not registered in service config | Check useSuite() / useApps() API service definitions |
| Cards not rendering | Composable returns empty array | Verify SDK endpoint URL and check browser Network tab for API errors |
| i18n keys showing raw paths | Missing translation keys | Ensure keys were added to ALL 4 locale files (de, en, fr, it) |
| Route not matching / 404 | File naming mismatch | Verify filename matches Nuxt file-based routing: {resource}s.vue (plural) |
| Dark mode broken on cards | Missing dark: variant | Every bg-* class must have a corresponding dark:bg-* class |
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-subpageSkill
- 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.

