diff --git a/Plugin.php b/Plugin.php
index 519a3c6..48906fc 100644
--- a/Plugin.php
+++ b/Plugin.php
@@ -2,15 +2,19 @@
 
 namespace Winter\Blocks;
 
+use Backend\Classes\NavigationManager;
 use Backend\Classes\WidgetManager;
+use Backend\Facades\Backend;
+use Backend\Models\UserRole;
 use Cms\Classes\AutoDatasource;
 use Cms\Classes\Theme;
-use Event;
 use System\Classes\PluginBase;
+use Winter\Blocks\Classes\Block as BlockModel;
 use Winter\Blocks\Classes\BlockManager;
 use Winter\Blocks\Classes\BlocksDatasource;
-use Winter\Blocks\Classes\Block as BlockModel;
 use Winter\Blocks\FormWidgets\Block;
+use Winter\Storm\Support\Facades\Config;
+use Winter\Storm\Support\Facades\Event;
 
 /**
  * Blocks Plugin Information File
@@ -103,6 +107,27 @@ public function boot(): void
     {
         $this->extendThemeDatasource();
         $this->extendControlLibraryBlocks();
+
+        if ($this->app->runningInBackend() && in_array('Cms', Config::get('cms.loadModules'))) {
+            $this->extendCms();
+        }
+    }
+
+    /**
+     * Registers any back-end permissions used by this plugin.
+     *
+     * @return array
+     */
+    public function registerPermissions()
+    {
+        return [
+            'winter.blocks.manage_blocks' => [
+                'tab'   => 'winter.blocks::lang.plugin.name',
+                'order' => 200,
+                'roles' => [UserRole::CODE_DEVELOPER, UserRole::CODE_PUBLISHER],
+                'label' => 'winter.blocks::lang.blocks.manage_blocks'
+            ],
+        ];
     }
 
     /**
@@ -176,4 +201,21 @@ protected function extendControlLibraryBlocks(): void
             }
         });
     }
+
+    /**
+     * Extend the CMS to implement the BlocksController as a child of the CMS
+     */
+    public function extendCms(): void
+    {
+        Event::listen('backend.menu.extendItems', function (NavigationManager $manager) {
+            $manager->addSideMenuItem('winter.cms', 'cms', 'blocks', [
+                'label'       => 'winter.blocks::lang.plugin.name',
+                'icon'        => 'icon-cubes',
+                'url'         => Backend::url('winter/blocks/blockscontroller'),
+                // TODO: Make good
+                'attributes'  => 'onclick="window.location.href = this.querySelector(\'a\').href;"',
+                'permissions' => ['winter.blocks.manage_blocks']
+            ]);
+        });
+    }
 }
diff --git a/assets/dist/js/winter.cmspage.extension.js b/assets/dist/js/winter.cmspage.extension.js
new file mode 100644
index 0000000..60195eb
--- /dev/null
+++ b/assets/dist/js/winter.cmspage.extension.js
@@ -0,0 +1 @@
+(()=>{var t;(t=window.jQuery).wn.cmsPage.updateModifiedCounter=function(){var n={page:{menu:"pages",count:0},partial:{menu:"partials",count:0},layout:{menu:"layouts",count:0},content:{menu:"content",count:0},asset:{menu:"assets",count:0},block:{menu:"blocks",count:0}};t("> div.tab-content > div.tab-pane[data-modified]","#cms-master-tabs").each((function(){var e=t("> form > input[name=templateType]",this).val();n[e].count++})),t.each(n,(function(n,e){t.wn.sideNav.setCounter("cms/"+e.menu,e.count)}))}})();
\ No newline at end of file
diff --git a/assets/src/js/winter.cmspage.extension.js b/assets/src/js/winter.cmspage.extension.js
new file mode 100644
index 0000000..8318d4f
--- /dev/null
+++ b/assets/src/js/winter.cmspage.extension.js
@@ -0,0 +1,21 @@
+(($) => {
+    $.wn.cmsPage.updateModifiedCounter = function () {
+        var counters = {
+            page: {menu: 'pages', count: 0},
+            partial: {menu: 'partials', count: 0},
+            layout: {menu: 'layouts', count: 0},
+            content: {menu: 'content', count: 0},
+            asset: {menu: 'assets', count: 0},
+            block: {menu: 'blocks', count: 0},
+        }
+
+        $('> div.tab-content > div.tab-pane[data-modified]', '#cms-master-tabs').each(function () {
+            var inputType = $('> form > input[name=templateType]', this).val();
+            counters[inputType].count++;
+        });
+
+        $.each(counters, function (type, data) {
+            $.wn.sideNav.setCounter('cms/' + data.menu, data.count);
+        });
+    };
+})(window.jQuery);
diff --git a/classes/Block.php b/classes/Block.php
index 9962b2c..4f928eb 100644
--- a/classes/Block.php
+++ b/classes/Block.php
@@ -26,6 +26,16 @@ class Block extends CmsCompoundObject
      */
     protected $allowedExtensions = ['block'];
 
+    /**
+     * @var array The attributes that are mass assignable.
+     */
+    protected $fillable = [
+        'markup',
+        'settings',
+        'code',
+        'yaml'
+    ];
+
     protected PartialStack $partialStack;
 
     public function __construct(array $attributes = [])
diff --git a/classes/BlockParser.php b/classes/BlockParser.php
index 6e7d9d0..a2cc42d 100644
--- a/classes/BlockParser.php
+++ b/classes/BlockParser.php
@@ -15,7 +15,9 @@ class BlockParser extends SectionParser
      */
     public static function parseSettings(string $settings): array
     {
-        return Yaml::parse($settings);
+        $parsed = Yaml::parse($settings);
+        // Ensure that the parsed settings returns an array (errors return input string)
+        return is_array($parsed) ? $parsed : [];
     }
 
     /**
@@ -23,6 +25,62 @@ public static function parseSettings(string $settings): array
      */
     public static function renderSettings(array $data): string
     {
-        return Yaml::render($data);
+        return is_string($data['yaml']) ? $data['yaml'] : Yaml::render($data);
+    }
+
+    /**
+     * Parses Halcyon section content.
+     * The expected file format is following:
+     *
+     *     INI settings section
+     *     ==
+     *     PHP code section
+     *     ==
+     *     Twig markup section
+     *
+     * If the content has only 2 sections they are parsed as settings and markup.
+     * If there is only a single section, it is parsed as markup.
+     *
+     * Returns an array with the following elements: (array|null) 'settings',
+     * (string|null) 'markup', (string|null) 'code'.
+     */
+    public static function parse(string $content, array $options = []): array
+    {
+        $sectionOptions = array_merge([
+            'isCompoundObject' => true
+        ], $options);
+        extract($sectionOptions);
+
+        $result = [
+            'settings' => [],
+            'code'     => null,
+            'markup'   => null,
+            'yaml'     => null
+        ];
+
+        if (!isset($isCompoundObject) || $isCompoundObject === false || !strlen($content)) {
+            return $result;
+        }
+
+        $sections = static::parseIntoSections($content);
+        $count = count($sections);
+        foreach ($sections as &$section) {
+            $section = trim($section);
+        }
+
+        if ($count >= 3) {
+            $result['yaml'] = $sections[0];
+            $result['settings'] = static::parseSettings($sections[0]);
+            $result['code'] = static::parseCode($sections[1]);
+            $result['markup'] = static::parseMarkup($sections[2]);
+        } elseif ($count == 2) {
+            $result['yaml'] = $sections[0];
+            $result['settings'] = static::parseSettings($sections[0]);
+            $result['markup'] = static::parseMarkup($sections[1]);
+        } elseif ($count == 1) {
+            $result['markup'] = static::parseMarkup($sections[0]);
+        }
+
+        return $result;
     }
 }
diff --git a/classes/BlockProcessor.php b/classes/BlockProcessor.php
index 7794ab4..f8efa97 100644
--- a/classes/BlockProcessor.php
+++ b/classes/BlockProcessor.php
@@ -32,7 +32,8 @@ protected function parseTemplateContent($query, $result, $fileName)
             'content' => $content,
             'mtime' => array_get($result, 'mtime'),
             'markup' => $processed['markup'],
-            'code' => $processed['code']
+            'code' => $processed['code'],
+            'yaml' => $processed['yaml'],
         ] + $processed['settings'];
     }
 
diff --git a/controllers/BlocksController.php b/controllers/BlocksController.php
new file mode 100644
index 0000000..1d2b6db
--- /dev/null
+++ b/controllers/BlocksController.php
@@ -0,0 +1,238 @@
+<?php
+
+namespace Winter\Blocks\Controllers;
+
+use Backend\Classes\Controller;
+use Backend\Classes\NavigationManager;
+use Backend\Facades\Backend;
+use Backend\Facades\BackendMenu;
+use Cms\Classes\Theme;
+use Cms\Controllers\Index as CmsIndexController;
+use Cms\Widgets\TemplateList;
+use Illuminate\Support\Facades\Lang;
+use Illuminate\Support\Facades\Request;
+use Winter\Blocks\Classes\Block;
+use Winter\Storm\Exception\ApplicationException;
+use Winter\Storm\Support\Facades\Config;
+use Winter\Storm\Support\Facades\Flash;
+
+/**
+ * Blocks Controller Backend Controller
+ */
+class BlocksController extends CmsIndexController
+{
+    protected $theme;
+
+    /**
+     * @var array Permissions required to view this page.
+     */
+    public $requiredPermissions = [
+        'blocks.manage_blocks',
+    ];
+
+    public function __construct()
+    {
+        Controller::__construct();
+
+        BackendMenu::setContext('Winter.Cms', 'cms', 'blocks');
+
+        try {
+            if (!($theme = Theme::getEditTheme())) {
+                throw new ApplicationException(Lang::get('cms::lang.theme.edit.not_found'));
+            }
+
+            $this->theme = $theme;
+
+            new TemplateList($this, 'blockList', function () use ($theme) {
+                return Block::listInTheme($theme, true);
+            });
+        } catch (\Exception $ex) {
+            $this->handleError($ex);
+        }
+
+        // Dynamically re-write the cms menu item urls to allow the user to return back to those pages
+        BackendMenu::registerCallback(function (NavigationManager $navigationManager) {
+            foreach ($navigationManager->getMainMenuItem('Winter.Cms', 'cms')->sideMenu as $menuItem) {
+                if ($menuItem->url === 'javascript:;') {
+                    $menuItem->url = Backend::url('cms#' . $menuItem->code);
+                    $menuItem->attributes = 'onclick="window.location.href = this.querySelector(\'a\').href;"';
+                }
+            }
+        });
+    }
+
+    /**
+     * Index page action
+     * @return void
+     */
+    public function index()
+    {
+        parent::index();
+        $this->addJs('/plugins/winter/blocks/assets/dist/js/winter.cmspage.extension.js', 'core');
+    }
+
+    /**
+     * Resolves a template type to its class name
+     * @param string $type
+     * @return string
+     */
+    protected function resolveTypeClassName($type)
+    {
+        if ($type !== 'block') {
+            throw new ApplicationException(Lang::get('cms::lang.template.invalid_type'));
+        }
+
+        return Block::class;
+    }
+
+    /**
+     * Returns the text for a template tab
+     * @param string $type
+     * @param string $template
+     * @return string
+     */
+    protected function getTabTitle($type, $template)
+    {
+        if ($type !== 'block') {
+            throw new ApplicationException(Lang::get('cms::lang.template.invalid_type'));
+        }
+
+        return $template->getFileName() ?? Lang::get('winter.blocks::lang.editor.new');
+    }
+
+    /**
+     * Returns a form widget for a specified template type.
+     * @param string $type
+     * @param string $template
+     * @param string $alias
+     * @return Backend\Widgets\Form
+     */
+    protected function makeTemplateFormWidget($type, $template, $alias = null)
+    {
+        if ($type !== 'block') {
+            throw new ApplicationException(Lang::get('cms::lang.template.not_found'));
+        }
+
+        $formConfig = '~/plugins/winter/blocks/controllers/blockscontroller/block_fields.yaml';
+
+        $widgetConfig = $this->makeConfig($formConfig);
+
+        $ext = pathinfo($template->fileName, PATHINFO_EXTENSION);
+        if ($type === 'content') {
+            switch ($ext) {
+                case 'htm':
+                    $type = 'richeditor';
+                    break;
+                case 'md':
+                    $type = 'markdown';
+                    break;
+                default:
+                    $type = 'codeeditor';
+                    break;
+            }
+            array_set($widgetConfig->secondaryTabs, 'fields.markup.type', $type);
+        }
+
+        $lang = 'php';
+        if (array_get($widgetConfig->secondaryTabs, 'fields.markup.type') === 'codeeditor') {
+            switch ($ext) {
+                case 'htm':
+                    $lang = 'twig';
+                    break;
+                case 'html':
+                    $lang = 'html';
+                    break;
+                case 'css':
+                    $lang = 'css';
+                    break;
+                case 'js':
+                case 'json':
+                    $lang = 'javascript';
+                    break;
+            }
+        }
+
+        $widgetConfig->model = $template;
+        $widgetConfig->alias = $alias ?: 'form'.studly_case($type).md5($template->exists ? $template->getFileName() : uniqid());
+
+        return $this->makeWidget('Backend\Widgets\Form', $widgetConfig);
+    }
+
+    /**
+     * Saves the template currently open
+     * @return array
+     */
+    public function onSave()
+    {
+        $this->validateRequestTheme();
+        $type = Request::input('templateType');
+        $templatePath = trim(Request::input('templatePath'));
+        $template = $templatePath ? $this->loadTemplate($type, $templatePath) : $this->createTemplate($type);
+        $formWidget = $this->makeTemplateFormWidget($type, $template);
+
+        $saveData = $formWidget->getSaveData();
+        $postData = post();
+        $templateData = [];
+
+        $settings = array_get($saveData, 'settings', []) + Request::input('settings', []);
+        $settings = $this->upgradeSettings($settings, $template->settings);
+
+        if ($settings) {
+            $templateData['settings'] = $settings;
+        }
+
+        $fields = ['markup', 'code', 'fileName', 'content', 'yaml'];
+
+        foreach ($fields as $field) {
+            if (array_key_exists($field, $saveData)) {
+                $templateData[$field] = $saveData[$field];
+            }
+            elseif (array_key_exists($field, $postData)) {
+                $templateData[$field] = $postData[$field];
+            }
+        }
+
+        if (!empty($templateData['markup']) && Config::get('cms.convertLineEndings', false) === true) {
+            $templateData['markup'] = $this->convertLineEndings($templateData['markup']);
+        }
+
+        if (!empty($templateData['code']) && Config::get('cms.convertLineEndings', false) === true) {
+            $templateData['code'] = $this->convertLineEndings($templateData['code']);
+        }
+
+        if (
+            !Request::input('templateForceSave') && $template->mtime
+            && Request::input('templateMtime') != $template->mtime
+        ) {
+            throw new ApplicationException('mtime-mismatch');
+        }
+
+        $template->attributes = [];
+        $template->fill($templateData);
+
+        $template->save();
+
+        /**
+         * @event cms.template.save
+         * Fires after a CMS template (page|partial|layout|content|asset) has been saved.
+         *
+         * Example usage:
+         *
+         *     Event::listen('cms.template.save', function ((\Cms\Controllers\Index) $controller, (mixed) $templateObject, (string) $type) {
+         *         \Log::info("A $type has been saved");
+         *     });
+         *
+         * Or
+         *
+         *     $CmsIndexController->bindEvent('template.save', function ((mixed) $templateObject, (string) $type) {
+         *         \Log::info("A $type has been saved");
+         *     });
+         *
+         */
+        $this->fireSystemEvent('cms.template.save', [$template, $type]);
+
+        Flash::success(Lang::get('cms::lang.template.saved'));
+
+        return $this->getUpdateResponse($template, $type);
+    }
+}
diff --git a/controllers/blockscontroller/_button_commit.php b/controllers/blockscontroller/_button_commit.php
new file mode 100644
index 0000000..90a4abd
--- /dev/null
+++ b/controllers/blockscontroller/_button_commit.php
@@ -0,0 +1,14 @@
+<button
+    type="button"
+    class="
+        btn btn-danger wn-icon-download
+        <?php if (!$canCommit): ?>
+            hide
+        <?php endif ?>
+    "
+    data-request="onCommit"
+    data-request-confirm="<?= e(trans('cms::lang.editor.commit_confirm')) ?>"
+    data-load-indicator="<?= e(trans('cms::lang.editor.committing')) ?>"
+    data-control="commit-button">
+    <?= e(trans('cms::lang.editor.commit')) ?>
+</button>
diff --git a/controllers/blockscontroller/_button_lastmodified.php b/controllers/blockscontroller/_button_lastmodified.php
new file mode 100644
index 0000000..daff6f3
--- /dev/null
+++ b/controllers/blockscontroller/_button_lastmodified.php
@@ -0,0 +1,8 @@
+<?php if (isset($lastModified)): ?>
+    <span
+        class="btn empty wn-icon-calendar"
+        title="<?= e(trans('backend::lang.media.last_modified')) ?>: <?= $lastModified ?>"
+        data-toggle="tooltip"
+        data-placement="right">
+    </span>
+<?php endif; ?>
\ No newline at end of file
diff --git a/controllers/blockscontroller/_button_reset.php b/controllers/blockscontroller/_button_reset.php
new file mode 100644
index 0000000..a5a8c87
--- /dev/null
+++ b/controllers/blockscontroller/_button_reset.php
@@ -0,0 +1,14 @@
+<button
+    type="button"
+    class="
+        btn btn-danger wn-icon-bomb
+        <?php if (!$canReset): ?>
+            hide
+        <?php endif ?>
+    "
+    data-request="onReset"
+    data-request-confirm="<?= e(trans('cms::lang.editor.reset_confirm')) ?>"
+    data-load-indicator="<?= e(trans('cms::lang.editor.resetting')) ?>"
+    data-control="reset-button">
+    <?= e(trans('cms::lang.editor.reset')) ?>
+</button>
diff --git a/controllers/blockscontroller/_common_toolbar_actions.php b/controllers/blockscontroller/_common_toolbar_actions.php
new file mode 100644
index 0000000..2e923a1
--- /dev/null
+++ b/controllers/blockscontroller/_common_toolbar_actions.php
@@ -0,0 +1,19 @@
+<?= $this->makePartial('button_commit'); ?>
+
+<?= $this->makePartial('button_reset'); ?>
+
+<button
+    type="button"
+    class="
+        btn btn-danger empty wn-icon-trash-o
+        <?php if (!$templatePath): ?>
+            hide
+        <?php endif ?>
+    "
+    data-request="onDelete"
+    data-request-confirm="<?= e(trans('cms::lang.' . $toolbarSource . '.delete_confirm_single')) ?>"
+    data-request-success="$.wn.cmsPage.updateTemplateList('<?= $toolbarSource ?>'); $(this).trigger('close.oc.tab', [{force: true}])"
+    data-control="delete-button">
+</button>
+
+<?= $this->makePartial('button_lastmodified'); ?>
diff --git a/controllers/blockscontroller/_concurrency_resolve_form.php b/controllers/blockscontroller/_concurrency_resolve_form.php
new file mode 100644
index 0000000..03ec6ca
--- /dev/null
+++ b/controllers/blockscontroller/_concurrency_resolve_form.php
@@ -0,0 +1,29 @@
+<?= Form::open(['onsubmit'=>'return false']) ?>
+    <div class="modal-header">
+        <button type="button" class="close" data-dismiss="popup">&times;</button>
+        <h4 class="modal-title"><?= e(trans('backend::lang.form.concurrency_file_changed_title')) ?></h4>
+    </div>
+    <div class="modal-body">
+        <p><?= e(trans('backend::lang.form.concurrency_file_changed_description')) ?></p>
+    </div>
+    <div class="modal-footer">
+        <button
+            type="submit"
+            data-action="reload"
+            class="btn btn-primary">
+            <?= e(trans('backend::lang.form.reload')) ?>
+        </button>
+        <button
+            type="submit"
+            data-action="save"
+            class="btn btn-primary">
+            <?= e(trans('backend::lang.form.save')) ?>
+        </button>
+        <button
+            type="button"
+            class="btn btn-default"
+            data-dismiss="popup">
+            <?= e(trans('backend::lang.form.cancel')) ?>
+        </button>
+    </div>
+<?= Form::close() ?>
diff --git a/controllers/blockscontroller/_form_page.php b/controllers/blockscontroller/_form_page.php
new file mode 100644
index 0000000..74d1b2f
--- /dev/null
+++ b/controllers/blockscontroller/_form_page.php
@@ -0,0 +1,16 @@
+<?= Form::open([
+        'class' => 'layout',
+        'data-change-monitor' => 'true',
+        'data-window-close-confirm' => e(trans('backend::lang.form.confirm_tab_close')),
+        'data-inspector-external-parameters' => true
+]) ?>
+    <?= $form->render() ?>
+
+    <input type="hidden" value="<?= e($form->alias) ?>" name="formWidgetAlias" />
+    <input type="hidden" value="<?= ($templateType) ?>" name="templateType" />
+    <input type="hidden" value="<?= ($templatePath) ?>" name="templatePath" />
+    <input type="hidden" value="<?= ($templateTheme) ?>" name="theme" />
+    <input type="hidden" value="<?= ($templateMtime) ?>" name="templateMtime" />
+    <input type="hidden" value="0" name="templateForceSave" />
+
+<?= Form::close() ?>
\ No newline at end of file
diff --git a/controllers/blockscontroller/_partial_toolbar.php b/controllers/blockscontroller/_partial_toolbar.php
new file mode 100644
index 0000000..094da3b
--- /dev/null
+++ b/controllers/blockscontroller/_partial_toolbar.php
@@ -0,0 +1,12 @@
+<div class="form-buttons loading-indicator-container">
+    <a
+        href="javascript:;"
+        class="btn btn-primary wn-icon-check save"
+        data-request="onSave"
+        data-load-indicator="<?= e(trans('backend::lang.form.saving')) ?>"
+        data-hotkey="ctrl+s, cmd+s">
+        <?= e(trans('backend::lang.form.save')) ?>
+    </a>
+
+    <?= $this->makePartial('common_toolbar_actions', ['toolbarSource' => 'block']); ?>
+</div>
diff --git a/controllers/blockscontroller/_safemode_notice.php b/controllers/blockscontroller/_safemode_notice.php
new file mode 100644
index 0000000..b9e320c
--- /dev/null
+++ b/controllers/blockscontroller/_safemode_notice.php
@@ -0,0 +1,6 @@
+<div class="callout callout-warning no-subheader">
+    <div class="header" style="border-radius: 0">
+        <i class="icon-warning"></i>
+        <h3><?= e(trans('cms::lang.cms_object.safe_mode_enabled')) ?></h3>
+    </div>
+</div>
diff --git a/controllers/blockscontroller/_sidepanel.php b/controllers/blockscontroller/_sidepanel.php
new file mode 100644
index 0000000..0223688
--- /dev/null
+++ b/controllers/blockscontroller/_sidepanel.php
@@ -0,0 +1,21 @@
+<?php
+    $visibleCount = 0;
+?>
+<div class="layout control-scrollpanel" id="cms-side-panel">
+    <div class="layout-cell">
+        <div class="layout-relative fix-button-container">
+            <?php if ($this->user->hasAccess('winter.blocks.manage_blocks')): ?>
+                <!-- Partials -->
+                <form
+                    role="form"
+                    class="layout <?= ++$visibleCount == 1 ? '' : 'hide' ?>"
+                    data-content-id="blocks"
+                    data-template-type="block"
+                    data-type-icon="wn-icon-tags"
+                    onsubmit="return false">
+                    <?= $this->widget->blockList->render() ?>
+                </form>
+            <?php endif ?>
+        </div>
+    </div>
+</div>
diff --git a/controllers/blockscontroller/block_fields.yaml b/controllers/blockscontroller/block_fields.yaml
new file mode 100644
index 0000000..8223579
--- /dev/null
+++ b/controllers/blockscontroller/block_fields.yaml
@@ -0,0 +1,45 @@
+# ===================================
+#  Form Field Definitions
+# ===================================
+
+fields:
+    fileName:
+        span: left
+        label: cms::lang.editor.filename
+        attributes:
+            default-focus: 1
+
+    toolbar:
+        type: partial
+        path: partial_toolbar
+        cssClass: collapse-visible
+
+tabs:
+    cssClass: master-area
+
+secondaryTabs:
+    stretch: true
+    fields:
+        yaml:
+            tab: winter.blocks::lang.editor.settings
+            stretch: true
+            type: codeeditor
+            language: yaml
+
+        markup:
+            tab: cms::lang.editor.markup
+            stretch: true
+            type: codeeditor
+            language: twig
+
+        safemode_notice:
+            tab: cms::lang.editor.code
+            type: partial
+            hidden: true
+            cssClass: p-b-0
+
+        code:
+            tab: cms::lang.editor.code
+            stretch: true
+            type: codeeditor
+            language: php
diff --git a/controllers/blockscontroller/config_block_list.yaml b/controllers/blockscontroller/config_block_list.yaml
new file mode 100644
index 0000000..be56179
--- /dev/null
+++ b/controllers/blockscontroller/config_block_list.yaml
@@ -0,0 +1,8 @@
+# ===================================
+#  Configures the partial list widget
+# ===================================
+
+descriptionProperty: description
+noRecordsMessage: 'cms::lang.partial.no_list_records'
+deleteConfirmation: 'cms::lang.partial.delete_confirm_multiple'
+itemType: block
diff --git a/controllers/blockscontroller/index.php b/controllers/blockscontroller/index.php
new file mode 100644
index 0000000..138c663
--- /dev/null
+++ b/controllers/blockscontroller/index.php
@@ -0,0 +1,31 @@
+<?= Block::put('sidepanel') ?>
+    <?php if (!$this->fatalError): ?>
+        <?= $this->makePartial('sidepanel') ?>
+    <?php endif ?>
+<?= Block::endPut() ?>
+
+<?= Block::put('body') ?>
+    <?php if (!$this->fatalError): ?>
+        <div
+            data-control="tab"
+            data-closable
+            data-close-confirmation="<?= e(trans('backend::lang.form.confirm_tab_close')) ?>"
+            data-pane-classes="layout-cell"
+            data-max-title-symbols="15"
+            data-title-as-file-names="true"
+            class="layout control-tabs master-tabs fancy-layout wn-logo-transparent"
+            id="cms-master-tabs">
+
+            <div class="layout-row min-size">
+                <div class="tabs-container">
+                    <ul class="nav nav-tabs"></ul>
+                </div>
+            </div>
+            <div class="tab-content layout-row">
+            </div>
+
+        </div>
+    <?php else: ?>
+        <p class="flash-message static error"><?= e(trans($this->fatalError)) ?></p>
+    <?php endif ?>
+<?= Block::endPut() ?>
diff --git a/lang/en/lang.php b/lang/en/lang.php
index 724a88a..abfce55 100644
--- a/lang/en/lang.php
+++ b/lang/en/lang.php
@@ -23,6 +23,10 @@
     'tabs' => [
         'display' => 'Display',
     ],
+    'editor' => [
+        'settings' => 'Settings',
+        'new' => 'New Block',
+    ],
     'blocks' => [
         'button' => [
             'name' => 'Button',
@@ -101,6 +105,7 @@
             'description' => 'Embed a YouTube video',
             'youtube_id' => 'YouTube Video ID',
         ],
+        'manage_blocks' => 'Manage Blocks',
     ],
     'fields' => [
         'actions_prompt' => 'Add action',
@@ -120,4 +125,10 @@
             'right' => 'Right',
         ],
     ],
+    'models' => [
+        'blockscontroller' => [
+            'label' => 'Blocks Controller',
+            'label_plural' => 'Blocks Controllers',
+        ],
+    ],
 ];
diff --git a/winter.mix.js b/winter.mix.js
index 082f4b8..ac70e1d 100644
--- a/winter.mix.js
+++ b/winter.mix.js
@@ -3,4 +3,5 @@ const mix = require('laravel-mix');
 mix
     .setPublicPath(__dirname)
     .js('assets/src/js/blocks.js', 'assets/dist/js/blocks.js')
+    .js('assets/src/js/winter.cmspage.extension.js', 'assets/dist/js/winter.cmspage.extension.js')
     .less('formwidgets/blocks/assets/less/blocks.less', 'formwidgets/blocks/assets/css/blocks.css');