Skip to main content
Glama

AI Code Toolkit

by AgiFlow
template-conventions.md28.1 kB
# Template Conventions Comprehensive guide for creating templates and understanding the scaffolding system in scaffold-mcp. ## Table of Contents - [Overview](#overview) - [Template Structure](#template-structure) - [Configuration Files](#configuration-files) - [scaffold.yaml](#scaffoldyaml) - [Variables Schema](#variables-schema) - [Include Patterns](#include-patterns) - [Liquid Template Syntax](#liquid-template-syntax) - [Variables](#variables) - [Filters](#filters) - [Control Flow](#control-flow) - [Comments](#comments) - [File Naming Conventions](#file-naming-conventions) - [Best Practices](#best-practices) - [Examples](#examples) --- ## Overview scaffold-mcp uses a powerful templating system based on **LiquidJS** for variable replacement and **YAML** for configuration. Templates support: - **Two scaffolding modes**: Boilerplates (new projects) and Features (add to existing projects) - **Dynamic variable replacement** in file content and paths - **Conditional includes** for optional files - **Path mapping** for flexible file placement - **Custom generators** for advanced logic --- ## Template Structure ### Basic Directory Layout ``` templates/ └── my-template/ # Template name (kebab-case recommended) ├── scaffold.yaml # Template configuration (required) ├── generators/ # Custom generators (optional) │ ├── package.json │ └── myGenerator.ts └── src/ # Template files (.liquid extension for templates) ├── package.json.liquid ├── tsconfig.json.liquid └── app/ └── page.tsx.liquid ``` ### Template File Extensions - **`.liquid` extension**: Files with this extension are processed for variable replacement, and the extension is automatically stripped in the output - **No extension**: Files without `.liquid` are copied as-is (useful for binary files, images, etc.) **Example:** ``` Template: src/app/{{ componentName }}/Component.tsx.liquid Output: src/app/Button/Component.tsx ``` ### Template Organization Templates use a **flat structure** in the `templates/` directory: ``` templates/ ├── nextjs-15/ │ ├── scaffold.yaml │ ├── generators/ │ └── src/ ├── react-vite/ │ ├── scaffold.yaml │ └── src/ └── express-api/ ├── scaffold.yaml └── src/ ``` Each template is a self-contained directory with its own `scaffold.yaml` configuration. --- ## Configuration Files ### scaffold.yaml The main configuration file defining boilerplates and features. #### Full Schema ```yaml # Boilerplate configuration (for creating new projects) boilerplate: # Single boilerplate name: nextjs-15-app description: Next.js 15 application with App Router, TypeScript, and Tailwind CSS # Target folder where projects will be created targetFolder: apps # Optional: Detailed instructions shown after scaffolding instruction: | Next.js 15 application created successfully. File purposes: - package.json: NPM dependencies and scripts - src/app/page.tsx: Main page component - src/app/layout.tsx: Root layout How to use: 1. Run `pnpm install` to install dependencies 2. Run `pnpm dev` to start development server 3. Open http://localhost:3000 Design patterns: - Use App Router for routing (src/app/) - Server Components by default - Client Components with 'use client' directive # Variable schema (JSON Schema format) variables_schema: type: object properties: projectName: type: string description: Project directory name example: my-app packageName: type: string description: NPM package name example: "@myorg/my-app" description: type: string description: Project description example: My Next.js application port: type: number description: Development server port example: 3000 default: 3000 required: - projectName - packageName additionalProperties: false # Files to include in the scaffolded project includes: - package.json - tsconfig.json - next.config.js - src/app/layout.tsx - src/app/page.tsx - public/favicon.ico # Optional: Custom generator for advanced logic generator: nextjsGenerator.ts # Features configuration (for adding to existing projects) features: # Array of features - name: scaffold-nextjs-page description: Add a new page to Next.js App Router application instruction: | Next.js page created successfully. The page follows the App Router structure and includes: - page.tsx: Main page component - layout.tsx: Optional layout (if withLayout=true) Access the page at: /your-page-path variables_schema: type: object properties: pagePath: type: string description: Page route path (e.g., "dashboard/settings") example: dashboard/settings pageTitle: type: string description: Page title for metadata example: Settings withLayout: type: boolean description: Include layout.tsx file default: false required: - pagePath - pageTitle additionalProperties: false # Conditional includes includes: - src/app/page/page.tsx - src/app/page/layout.tsx?withLayout=true # Optional: File patterns this feature works with (for documentation) patterns: - src/app/**/page.tsx - src/app/**/layout.tsx - name: scaffold-nextjs-component description: Add a React component with tests variables_schema: type: object properties: componentName: type: string description: Component name (PascalCase) example: Button withTests: type: boolean description: Include test file default: true withStorybook: type: boolean description: Include Storybook story default: false required: - componentName includes: - src/components/{{ componentName }}/{{ componentName }}.tsx - src/components/{{ componentName }}/{{ componentName }}.test.tsx?withTests=true - src/components/{{ componentName }}/{{ componentName }}.stories.tsx?withStorybook=true ``` #### Multiple Boilerplates You can define multiple boilerplates in a single template: ```yaml boilerplate: - name: nextjs-15-basic description: Basic Next.js 15 app targetFolder: apps variables_schema: # ... includes: # ... - name: nextjs-15-with-auth description: Next.js 15 app with authentication targetFolder: apps variables_schema: # ... includes: # ... ``` ### Variables Schema The `variables_schema` uses **JSON Schema** format for validation. #### Supported Types ```yaml variables_schema: type: object properties: # String name: type: string description: User-friendly description example: my-app minLength: 1 maxLength: 50 pattern: "^[a-z][a-z0-9-]*$" # Regex validation # Number port: type: number description: Port number example: 3000 minimum: 1000 maximum: 9999 default: 3000 # Boolean withTests: type: boolean description: Include tests default: true # Array features: type: array description: Features to include items: type: string example: ["auth", "database"] default: [] # Enum framework: type: string description: Framework to use enum: ["react", "vue", "angular"] default: react required: - name additionalProperties: false # Reject unknown properties ``` #### Built-in Variables These variables are automatically provided by the scaffold system: **For Boilerplates:** - `projectName`: Project directory name (from user input) - `packageName`: NPM package name (from user input) **For Features:** - `projectName`: Name of the target project - `appPath`: Absolute path to the project directory - `appName`: Same as `projectName` **Usage:** ```yaml # You can reference built-in variables in templates includes: - src/config/{{ projectName }}.ts ``` ### Include Patterns The `includes` array supports advanced syntax for flexible file inclusion. #### Basic Include Copy a file as-is: ```yaml includes: - package.json - src/app/page.tsx - public/logo.png ``` #### Variable in Path Use Liquid syntax in paths: ```yaml includes: - src/components/{{ componentName }}/{{ componentName }}.tsx - src/services/{{ serviceName | kebabCase }}/index.ts ``` **Example:** ``` Variables: { componentName: "Button" } Template: src/components/{{ componentName }}/{{ componentName }}.tsx Result: src/components/Button/Button.tsx ``` #### Conditional Include Include files only when conditions are met: ```yaml includes: # Include only if withLayout is true - src/app/layout.tsx?withLayout=true # Include only if environment is "production" - config/prod.json?environment=production # Multiple conditions (AND logic) - src/auth/validator.ts?withAuth=true&withValidation=true ``` **Condition syntax:** - `?key=value`: Include if variable `key` equals `value` - `?key=true`: Include if variable `key` is truthy - `?key=false`: Include if variable `key` is falsy - `?key1=value1&key2=value2`: Multiple conditions (all must match) #### Path Mapping Map source file to a different target path: ```yaml includes: # Basic mapping - template.tsx->src/app/page.tsx # With variables - template.tsx->src/app/{{ pagePath }}/page.tsx # With conditions and variables - auth-layout.tsx->src/app/{{ pagePath }}/layout.tsx?withAuth=true ``` **Example:** ``` Variables: { pagePath: "dashboard/settings" } Template: template.tsx->src/app/{{ pagePath }}/page.tsx Source: template.tsx Target: src/app/dashboard/settings/page.tsx ``` #### Complex Patterns Combine all features: ```yaml includes: # Path with variables and conditions - src/components/{{ componentName }}/{{ componentName }}.test.tsx?withTests=true # Path mapping with variables and conditions - component-template.tsx->src/{{ folderPath }}/{{ componentName }}.tsx?createNew=true # Nested paths with filters - src/{{ moduleName | kebabCase }}/{{ componentName | pascalCase }}.tsx ``` --- ## Liquid Template Syntax scaffold-mcp uses **LiquidJS** for template processing. All `.liquid` files are processed for variable replacement. ### Variables #### Basic Variable Interpolation ```liquid {{ variableName }} ``` **Example template (package.json.liquid):** ```json { "name": "{{ packageName }}", "version": "{{ version }}", "description": "{{ description }}" } ``` **Variables:** ```json { "packageName": "@myorg/my-app", "version": "1.0.0", "description": "My application" } ``` **Output:** ```json { "name": "@myorg/my-app", "version": "1.0.0", "description": "My application" } ``` ### Filters LiquidJS supports filters to transform variables. scaffold-mcp includes custom filters for common case transformations. #### Built-in Custom Filters | Filter | Description | Example Input | Example Output | |--------|-------------|---------------|----------------| | `camelCase` | Convert to camelCase | `my-component` | `myComponent` | | `pascalCase` | Convert to PascalCase | `my-component` | `MyComponent` | | `titleCase` | Convert to TitleCase (alias for pascalCase) | `my component` | `MyComponent` | | `kebabCase` | Convert to kebab-case | `MyComponent` | `my-component` | | `snakeCase` | Convert to snake_case | `MyComponent` | `my_component` | | `upperCase` | Convert to UPPER_CASE | `myVariable` | `MY_VARIABLE` | | `lower` | Convert to lowercase | `MyComponent` | `mycomponent` | | `upper` | Convert to uppercase | `mycomponent` | `MYCOMPONENT` | | `pluralize` | Pluralize word | `user` | `users` | | `singularize` | Singularize word | `users` | `user` | #### Filter Usage ```liquid {{ componentName | pascalCase }} {{ serviceName | kebabCase }} {{ constantName | upperCase }} {{ tableName | pluralize }} ``` **Example:** ```typescript // Template: Component.tsx.liquid export const {{ componentName | pascalCase }} = () => { return <div>{{ componentName | titleCase }} Component</div>; }; // Variables: { componentName: "my-button" } // Output: Component.tsx export const MyButton = () => { return <div>My Button Component</div>; }; ``` #### Chaining Filters ```liquid {{ projectName | kebabCase | upper }} ``` **Example:** ``` Input: { projectName: "MyAwesomeApp" } Output: MY-AWESOME-APP ``` #### Standard Liquid Filters LiquidJS also supports standard filters: ```liquid {{ description | truncate: 50 }} # Truncate to 50 characters {{ items | join: ", " }} # Join array with separator {{ text | replace: "foo", "bar" }} # Replace text {{ number | plus: 5 }} # Add numbers {{ text | strip_html }} # Remove HTML tags {{ date | date: "%Y-%m-%d" }} # Format date ``` ### Control Flow #### Conditionals (if/elsif/else) ```liquid {% if withAuth %} import { authMiddleware } from './auth'; {% endif %} export const config = { {% if environment == "production" %} apiUrl: "https://api.example.com", {% elsif environment == "staging" %} apiUrl: "https://staging-api.example.com", {% else %} apiUrl: "http://localhost:3000", {% endif %} }; ``` #### Comparison Operators ```liquid {% if port > 8000 %} {% if name == "admin" %} {% if count != 0 %} {% if enabled %} # Truthy check {% if items.size > 0 %} ``` #### Logical Operators ```liquid {% if withAuth and withValidation %} {% if environment == "prod" or environment == "production" %} {% if not disabled %} ``` #### Loops (for) ```liquid {% for feature in features %} - {{ feature | capitalize }} {% endfor %} {% for item in items %} {{ forloop.index }}: {{ item.name }} {% endfor %} ``` **Loop variables:** - `forloop.index`: 1-based index - `forloop.index0`: 0-based index - `forloop.first`: True if first iteration - `forloop.last`: True if last iteration - `forloop.length`: Total iterations **Example:** ```typescript // Template export const FEATURES = { {% for feature in features %} {{ feature | upperCase }}: true{% if forloop.last == false %},{% endif %} {% endfor %} }; // Variables: { features: ["auth", "database", "api"] } // Output export const FEATURES = { AUTH: true, DATABASE: true, API: true }; ``` #### Unless (inverse if) ```liquid {% unless disabled %} console.log('Feature is enabled'); {% endunless %} ``` #### Case/When (switch statement) ```liquid {% case framework %} {% when "react" %} import React from 'react'; {% when "vue" %} import { defineComponent } from 'vue'; {% when "angular" %} import { Component } from '@angular/core'; {% else %} // Unknown framework {% endcase %} ``` ### Comments ```liquid {% comment %} This is a comment block. It will not appear in the output. {% endcomment %} {%- comment -%} This comment strips whitespace around it {%- endcomment -%} ``` **Note:** Liquid uses `{% comment %}` blocks, NOT `{# #}` syntax. ### Whitespace Control Use `-` to strip whitespace: ```liquid {%- if condition -%} content {%- endif -%} {{- variable -}} ``` --- ## File Naming Conventions ### Template Files 1. **Use `.liquid` extension** for files that need variable replacement 2. **Omit `.liquid`** for binary files or files that should be copied as-is 3. **Use variables in filenames** for dynamic naming **Examples:** ``` ✅ Good: package.json.liquid # Will become: package.json {{ componentName }}.tsx.liquid # Will become: Button.tsx {{ serviceName | kebabCase }}.service.ts.liquid # Will become: user-auth.service.ts ❌ Bad: package.json # Won't process variables Component.tsx.liquid # Hardcoded name, should use variable ``` ### Directory Names Use variables in directory names too: ``` Template structure: src/ components/ {{ componentName }}/ {{ componentName }}.tsx.liquid {{ componentName }}.test.tsx.liquid Variables: { componentName: "Button" } Output: src/ components/ Button/ Button.tsx Button.test.tsx ``` ### Naming Recommendations | Type | Convention | Example | |------|------------|---------| | Template directory | kebab-case | `nextjs-15`, `react-vite` | | Boilerplate name | kebab-case with prefix | `scaffold-nextjs-app` | | Feature name | kebab-case with prefix | `scaffold-nextjs-page` | | Component variables | PascalCase | `Button`, `UserProfile` | | File variables | varies by language | `user-service.ts`, `userService.ts` | | Generator files | camelCase.ts | `nextjsPageGenerator.ts` | --- ## Best Practices ### 1. Use Meaningful Variable Names ```yaml # ✅ Good: Clear, descriptive names properties: componentName: type: string description: Name of the React component (PascalCase) example: UserProfile apiEndpoint: type: string description: API endpoint URL example: https://api.example.com # ❌ Bad: Vague names properties: name: type: string url: type: string ``` ### 2. Provide Examples and Descriptions ```yaml # ✅ Good: Helpful documentation properties: routePath: type: string description: Route path for Next.js page (e.g., "dashboard/users/[id]") example: dashboard/users/[id] # ❌ Bad: No context properties: routePath: type: string ``` ### 3. Use Filters for Consistency ```liquid {# ✅ Good: Ensure consistent casing #} export const {{ componentName | pascalCase }} = () => { const className = "{{ componentName | kebabCase }}"; }; {# ❌ Bad: Rely on user input casing #} export const {{ componentName }} = () => { const className = "{{ componentName }}"; }; ``` ### 4. Validate Variable Formats ```yaml # ✅ Good: Enforce format with regex properties: componentName: type: string pattern: "^[A-Z][a-zA-Z0-9]*$" # Must be PascalCase description: Component name in PascalCase packageName: type: string pattern: "^@[a-z0-9-]+/[a-z0-9-]+$" # Must be @scope/name description: Scoped package name # ❌ Bad: No validation properties: componentName: type: string packageName: type: string ``` ### 5. Use Conditional Includes Wisely ```yaml # ✅ Good: Clear, purposeful conditions includes: - src/auth/middleware.ts?withAuth=true - src/validation/schemas.ts?withValidation=true - src/styles/custom.css?customStyles=true # ❌ Bad: Over-complicated conditions includes: - src/file.ts?feature1=true&feature2=false&environment=prod&version=2 ``` ### 6. Organize Template Files Mirror the output structure within each template: ``` # ✅ Good: Clear organization templates/nextjs-15/ scaffold.yaml src/ app/ page/ page.tsx.liquid layout.tsx.liquid components/ Button/ Button.tsx.liquid # ❌ Bad: Disorganized files templates/nextjs-15/ scaffold.yaml page.tsx.liquid layout.tsx.liquid button.tsx.liquid ``` ### 7. Document Complex Templates Add instruction field for complex scaffolds: ```yaml instruction: | Next.js page created successfully at src/app/{{ pagePath }}/ File purposes: - page.tsx: Main page component (Server Component by default) - layout.tsx: Page-specific layout (optional) How to use: 1. Access the page at http://localhost:3000/{{ pagePath }} 2. Add 'use client' directive if you need client-side interactivity 3. Export metadata object for SEO optimization Design patterns: - Use Server Components for data fetching - Use Client Components for interactivity - Colocate related files in the same directory ``` ### 8. Handle Edge Cases ```liquid {# ✅ Good: Handle missing variables #} export const config = { name: "{{ projectName }}", port: {{ port | default: 3000 }}, features: [ {% if features.size > 0 %} {% for feature in features %} "{{ feature }}"{% if forloop.last == false %},{% endif %} {% endfor %} {% endif %} ] }; {# ❌ Bad: Assume variables exist #} export const config = { name: "{{ projectName }}", port: {{ port }}, features: {{ features }} }; ``` ### 9. Test with Different Variable Combinations Create test cases: ```yaml # Test Case 1: Minimal variables variables: componentName: Button # Test Case 2: All optional features variables: componentName: UserProfile withTests: true withStorybook: true withStyles: true # Test Case 3: Edge cases variables: componentName: My-Component-Name # Test filters description: "" # Test empty strings ``` ### 10. Use Comments in Templates ```liquid {# Component definition with TypeScript props interface #} {% comment %} This component follows the compound component pattern. It exports both the main component and sub-components. {% endcomment %} interface {{ componentName }}Props { children: React.ReactNode; {% if withClassName %} className?: string; {% endif %} } export const {{ componentName }}: React.FC<{{ componentName }}Props> = ({ children, {% if withClassName %} className, {% endif %} }) => { return ( <div {% if withClassName %}className={className}{% endif %}> {children} </div> ); }; ``` --- ## Examples ### Example 1: React Component Template **scaffold.yaml:** ```yaml features: - name: scaffold-react-component description: Create a React component with optional tests and styles variables_schema: type: object properties: componentName: type: string pattern: "^[A-Z][a-zA-Z0-9]*$" description: Component name in PascalCase example: UserProfile withTests: type: boolean description: Include test file default: true withStyles: type: boolean description: Include CSS module default: false required: - componentName additionalProperties: false includes: - src/components/{{ componentName }}/{{ componentName }}.tsx - src/components/{{ componentName }}/{{ componentName }}.test.tsx?withTests=true - src/components/{{ componentName }}/{{ componentName }}.module.css?withStyles=true - src/components/{{ componentName }}/index.ts ``` **Template: src/components/{{ componentName }}/{{ componentName }}.tsx.liquid** ```typescript import React from 'react'; {% if withStyles %} import styles from './{{ componentName }}.module.css'; {% endif %} export interface {{ componentName }}Props { children?: React.ReactNode; {% if withStyles %} className?: string; {% endif %} } export const {{ componentName }}: React.FC<{{ componentName }}Props> = ({ children, {% if withStyles %} className, {% endif %} }) => { return ( <div {% if withStyles %}className={`${styles.{{ componentName | camelCase }}} ${className || ''}`}{% endif %}> {children} </div> ); }; ``` **Template: src/components/{{ componentName }}/index.ts.liquid** ```typescript export { {{ componentName }} } from './{{ componentName }}'; export type { {{ componentName }}Props } from './{{ componentName }}'; ``` ### Example 2: API Route Template **scaffold.yaml:** ```yaml features: - name: scaffold-api-route description: Create an API route with validation and authentication variables_schema: type: object properties: routeName: type: string description: Route name (e.g., "users", "posts") example: users withAuth: type: boolean description: Include authentication middleware default: true withValidation: type: boolean description: Include request validation default: true methods: type: array description: HTTP methods to support items: type: string enum: ["GET", "POST", "PUT", "DELETE"] default: ["GET", "POST"] required: - routeName includes: - src/routes/{{ routeName | kebabCase }}/route.ts - src/routes/{{ routeName | kebabCase }}/validation.ts?withValidation=true - src/routes/{{ routeName | kebabCase }}/auth.ts?withAuth=true ``` **Template: src/routes/{{ routeName | kebabCase }}/route.ts.liquid** ```typescript import { Request, Response, Router } from 'express'; {% if withAuth %} import { authMiddleware } from './auth'; {% endif %} {% if withValidation %} import { validate{{ routeName | pascalCase }}Input } from './validation'; {% endif %} const router = Router(); {% for method in methods %} {% assign methodLower = method | lower %} router.{{ methodLower }}( '/{{ routeName | kebabCase }}', {% if withAuth %}authMiddleware,{% endif %} {% if withValidation %}validate{{ routeName | pascalCase }}Input,{% endif %} async (req: Request, res: Response) => { // TODO: Implement {{ method }} /{{ routeName | kebabCase }} res.json({ message: '{{ method }} /{{ routeName | kebabCase }}' }); } ); {% endfor %} export default router; ``` ### Example 3: Configuration File Template **Template: config/app.config.ts.liquid** ```typescript export const appConfig = { name: '{{ appName }}', version: '{{ version | default: "1.0.0" }}', {% case environment %} {% when "production" %} apiUrl: 'https://api.{{ appName | kebabCase }}.com', debug: false, {% when "staging" %} apiUrl: 'https://staging-api.{{ appName | kebabCase }}.com', debug: true, {% else %} apiUrl: 'http://localhost:{{ port | default: 3000 }}', debug: true, {% endcase %} features: { {% if features.size > 0 %} {% for feature in features %} {{ feature | camelCase }}: true, {% endfor %} {% endif %} }, {% if withDatabase %} database: { host: process.env.DB_HOST || 'localhost', port: parseInt(process.env.DB_PORT || '5432'), name: '{{ appName | kebabCase }}_{{ environment }}', }, {% endif %} } as const; export type AppConfig = typeof appConfig; ``` ### Example 4: Package.json Template **Template: package.json.liquid** ```json { "name": "{{ packageName }}", "version": "{{ version | default: '0.1.0' }}", "description": "{{ description }}", "type": "module", "scripts": { "dev": "{{ devCommand | default: 'vite' }}", "build": "{{ buildCommand | default: 'vite build' }}", "preview": "{{ previewCommand | default: 'vite preview' }}"{% if withTests %}, "test": "{{ testCommand | default: 'vitest' }}"{% endif %}{% if withLint %}, "lint": "{{ lintCommand | default: 'eslint .' }}"{% endif %} }, "dependencies": { {% for dep in dependencies %} "{{ dep.name }}": "{{ dep.version }}"{% if forloop.last == false %},{% endif %} {% endfor %} }, "devDependencies": { {% for dep in devDependencies %} "{{ dep.name }}": "{{ dep.version }}"{% if forloop.last == false %},{% endif %} {% endfor %} } } ``` --- ## Summary Key takeaways for creating effective templates: 1. **Use `.liquid` extension** for files needing variable replacement 2. **Leverage filters** for consistent casing and transformations 3. **Use conditional includes** for optional features 4. **Provide comprehensive variable schemas** with examples 5. **Document complex templates** with instruction field 6. **Test with various variable combinations** to ensure robustness 7. **Follow naming conventions** for consistency 8. **Organize template files** to mirror output structure 9. **Use comments** to explain complex logic 10. **Validate inputs** with JSON Schema patterns For advanced scaffolding scenarios requiring custom logic, see [Advanced Generators Guide](./advanced-generators.md).

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/AgiFlow/aicode-toolkit'

If you have feedback or need assistance with the MCP directory API, please join our Discord server