This document is for a version of Craft CMS that is no longer supported. Please refer to the latest version →

In Craft, all HTML rendering is done via templates – even for the control panel itself. There are no PHP-based view files.

If your plugin needs its own templates, you can place them in a templates/ folder within your plugin’s folder (ex: craft/plugins/cocktailrecipes/templates/).

# Plugin Template Paths, Explained

To manually render a template from your plugin’s PHP code, call Craft\TemplatesService::render() (opens new window):


The first segment of the path you pass into TemplatesService::render() is your plugin handle; everything after it is the path to your template, relative from your plugin’s templates/ folder.

So, if you were to call craft()->templates->render('cocktailrecipes/settings'), for example, Craft would check the following locations, in this order:

  1. craft/app/templates/cocktailrecipes/settings.html
  2. craft/app/templates/cocktailrecipes/settings/index.html
  3. craft/plugins/cocktailrecipes/templates/settings.html
  4. craft/plugins/cocktailrecipes/templates/settings/index.html

As you can see, the templates/ folder segment is assumed, so there’s no need to include it when calling render().

Craft will only automatically look for templates within the plugin folders for control panel requests. If you are going to be calling TemplatesService::render() from a front end site request, you will need to manually tell Craft where to find your template using Craft\PathService::setTemplatesPath() (opens new window) first:

$oldPath = craft()->path->getTemplatesPath();
$newPath = craft()->path->getPluginsPath().'pluginHandle/templates';
$html = craft()->templates->render('path/to/template');

If you do this, you won’t be needing to begin the template path with your plugin handle.

# Giving your Plugin its own Control Panel Section

If you want to give your plugin its own control panel section, add this to your primary plugin class:

namespace Craft;

class CocktailRecipesPlugin extends BasePlugin
    // ...

    public function hasCpSection()
        return true;

With that set, your plugin will show up in the control panel navigation. Clicking on it will take you to admin/PluginHandle, which will route to index.html within your plugin’s templates/ folder.

Your plugin’s control panel section can have as many pages as you’d like. To link to other pages, use the {{ url() }} template function just like you would in your site’s templates (ex: href="{{ url('cocktailrecipes/all') }}).

# Extending the Control Panel Layout

Most of the time you’ll want your plugin’s templates to look like the rest of the control panel. To do that, they must extend the _layouts/cp layout.

The _layouts/cp layout expects two variables to be set: title and content. title is used to set the page’s <title> and <h1> tag values, and content defines the HTML that should show up in the #main div.

{% extends "_layouts/cp" %}

{% set title = "Cocktail Recipes"|t %}

{% set content %}
{% endset %}

If your plugin’s control panel section has its own sub-navigation, you can define that by setting the tabs variable in your template:

{% set tabs = {
  recent: { label: "Recent"|t, url: url('cocktailrecipes') },
  new:    { label: "Add a New Recipe"|t, url: url('cocktailrecipes/new') }
} %}

Tell the control panel template which tab should be selected by setting the selectedTab variable:

{% set selectedTab = 'recent' %}

You can also add breadcrumbs to your pages by setting the crumbs variable:

{% set crumbs = [
  { label: "Cocktail Recipes"|t, url: url('cocktailrecipes') },
  { label: recipe.groupName|t, url: url('cocktailrecipes/recipes/'~recipe.groupHandle) }
] %}

# Dynamic URL Routing

By default, incoming requests are routed to a template with the same path as the request URI (possibly with .html or /index.html appended to it). Most of the time this works well, but it falls short for dynamic URLs, such as URLs where one of the segments is an ID or a slug. For example, you might want to route URLs that look like admin/cocktailrecipes/123 to templates/_edit.html.

You can accomplish this by registering routes. Craft gives plugins a chance to register new control panel routes via the registerCpRoutes hook. Simply add a new method to your plugin called registerCpRoutes():

namespace Craft;

class CocktailRecipesPlugin extends BasePlugin
    // ...

    public function registerCpRoutes()
        return array(
            'cocktailrecipes/(?P<recipeId>\d+)' => 'cocktailrecipes/_edit',

As you can see, the method returns an array of routes. The keys are regular expressions that the request URI will be matched against, and the values are template paths to be loaded when a successful match occurs.

If your regular expression includes any named subpatterns, e.g. (?P<recipeId>\d+), their match values will become available to the template as variables of the same name. So in this example, if the URI was admin/cocktailrecipes/123, the cocktailrecipes/_edit template would get loaded, and a {{ recipeId }} variable would be available to it, with the value 123.